본문 바로가기
프로젝트/파이널프로젝트-대학 행정 그룹웨어

스프링 스케줄러를 사용해서 매일 밤 12시에 쿼리 실행하기

by moca7 2024. 12. 1.

 

 

 

ㅁ 참고

- https://moca7.tistory.com/333 스프링 스케줄러 글

- https://moca7.tistory.com/340 스프링부트 스케줄러 글

 

 

 

 

ㅁ 스프링의 경우

 

(1) 매번 묵시적으로 실행시키고자 하는 작업들을 정의할 클래스를 만들기

- 그리고 그 클래스를 빈으로 등록한다. 빈등록은 3가지 방법이 있었다.

 

(2) 해당 클래스 내에 각 작업별 메소드를 작성하기

- 반환형은 반드시 void여야 하고, 매개변수는 없어야 한다. 메소드명은 상관없다.

- 메소드 위에 @Scheduled 어노테이션을 부여한다.

 

(3) servlet-context.xml에서 스케줄링을 사용하기 위해 task를 추가하기

 

 

 

 

 

ㅁ 스프링 부트의 경우

 

- (3)번 대신 스프링 부트 프로젝트를 만들면 기본으로 만들어지는 xxxApplication.java에 @EnableScheduling를 붙인다.

- xxxApplication.java는 부트 프로젝트를 구동시켜주는 클래스다.

- @EnableScheduling이 (스프링)servlet-context.xml의 <task:annotation-driven />를 대체한다.

 

 

 

 

 

ㅁ 실제 적용

 

 

(1) com.br.gdcampus.scheduler 패키지 생성

 

 

 

- 스케줄러는 유지보수성과 응집도를 고려했을 때 기존의 컨트롤러와 별도의 클래스로 분리하는 것이 좋다.

- 가독성 향상 : 컨트롤러의 코드가 복잡해지지 않아 역할이 명확하다.

- 의존성 분리 : 컨트롤러와 스케줄러 간 의존성을 최소화할 수 있다.

 

 

 

 

 

(2) com.br.gdcampus.scheduler에 ReservationScheduler (일반) 클래스 생성

 

 
package com.br.gdcampus.scheduler;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import com.br.gdcampus.service.ReservationService;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j // 로그 사용
@RequiredArgsConstructor // final 필드에 대해 매개변수 생성자가 만들어져서 주입된다.
@Component // 빈 등록
public class ReservationScheduler {

 
  private final ReservationService reservationService;
 
 
  @Scheduled(cron="0 0/1 * * * *") // 1분마다 실행되는지 확인용 메소드.
  public void execute1() {
    log.debug("1분마다 매번 실행됨");
  }

 
}
 

 

 

- 텅 비어있는 ReservationScheduler 클래스를 채운다.

 

 

(1) @Slf4j

- 로그를 찍어보기 위한 어노테이션이다.

 

(2) @RequiredArgsConstructor

- final 필드에 대한 매개변수 생성자가 만들어지고, 매개변수 생성자 주입에 의해 생성된 객체가 대입된다.

 

(3) @Component

- 이 클래스를 빈으로 등록한다.

 

(4) 서비스 타입의 필드를 final로 선언 

 

(5) 각 작업별 메소드 생성

- 반환형은 void, 매개변수는 없어야 하고, 메소드명은 상관없다.

- 메소드 위에 @Scheduled(cron="0 0/1 * * * *") 어노테이션을 붙였다. 매 1분마다 실행된다.

 

 

 

 

 

(3-1) 스프링의 경우, servlet-context.xml에서 스케줄링을 사용하기 위해 task를 추가하기

 

 
<?xml version="1.0" encoding="UTF-8"?>

  <!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
 
  <!-- Enables the Spring MVC @Controller programming model -->
  <annotation-driven />

  <!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
  <resources mapping="/resources/**" location="/resources/" />
  <resources mapping="/upload/**" location="file:///upload/" />

  <!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
  <beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <beans:property name="prefix" value="/WEB-INF/views/" />
    <beans:property name="suffix" value=".jsp" />
  </beans:bean>
 
  <context:component-scan base-package="com.br.spring" />
 
 
  <!-- webSocket 관련 등록 구문 -->
  <beans:bean class="com.br.spring.handler.ChatEchoHandler" id="chatEchoHandler" />
  <websocket:handlers>
    <websocket:mapping handler="chatEchoHandler" path="/chat" />
    <websocket:handshake-interceptors>
      <beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor" />
    </websocket:handshake-interceptors>
    <websocket:sockjs />
  </websocket:handlers>
 
 
  <!-- interceptor 관련 등록 구문 -->
  <interceptors>
    <interceptor>
      <mapping path="/member/myinfo.do" />
      <mapping path="/board/regist.do" />
           
      <beans:bean class="com.br.spring.interceptor.LoginCheckInterceptor" id="LoginCheckInterceptor"/>
    </interceptor>
   
    <!--
    <interceptor>
   
    </interceptor>
    -->
  </interceptors>
 
 
  <!-- Scheduler -->
  <task:annotation-driven />
 
</beans:beans>
 

 

 

- 파이널 프로젝트는 스프링 부트 프로젝트이기 때문에 위의 servlet-context.xml은 이전에 했던 예시다.

- 마지막 부분에 <task:annotation-drven />이 작성되어 있다.

 

 

 

 

(3-2) 스프링 부트의 경우, com.br.gdcampus의 GdcampusApplication.java에 @EnableScheduling 어노테이션 추가하기

 

 
package com.br.gdcampus;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling // servlet-context.xml의 <task:annotation-driven /> 대체
@SpringBootApplication
public class GdcampusApplication {

  public static void main(String[] args) {
    SpringApplication.run(GdcampusApplication.class, args);
  }

}
 

 

 

- 클래스 위에 @EnableScheduling 어노테이션만 추가했다.

 

 

 

 

 

 

 

ㅁ 실제 적용

 

 

 

 

- DB에서 예약을 신청한 지 하루가 지났는데 "예약신청중" 상태인 데이터들을 조회한다.

6개의 데이터가 조회된다. 

 

- 실제로는 7일이 지나도록 "예약신청중"인 예약신청의 상태를 "예약신청반려"로 update할 예정이지만,

우선 1일이 지난 데이터들을 대상으로 스케줄러를 작동시켜 본다.

 

 

 

 

 

 

 
package com.br.gdcampus.scheduler;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import com.br.gdcampus.service.ReservationService;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j // 로그 사용
@RequiredArgsConstructor // final 필드에 대해 매개변수 생성자가 만들어져서 주입된다.
@Component // 빈 등록
public class ReservationScheduler {

 
  private final ReservationService reservationService;
 
 
  @Scheduled(cron="0 0/1 * * * *") // 1분마다 실행되는지 확인용 메소드.
  public void execute1() {
    log.debug("1분마다 매번 실행됨");
  }
 
 
  @Scheduled(cron="0 0/1 * * * *") // 매일 밤 12시마다 실행된다. 일주일이 지났는데도 "예약신청중" 상태인 예약 신청은 자동으로 반려처리한다.
  public void rejectReservationsOlderThan7Days() {

    log.debug("[예약] rejectReservationsOlderThan7Days 스케줄러 실행됨");
   
    int result = reservationService.rejectReservationsOlderThan7Days();
    log.debug("7일이 경과해 자동으로 반려처리 된 예약 신청 개수 : {}", result);
  }

 
}
 

 

 

- 실제로는 매일 밤 12시에 실행시킬 예정이지만, 우선 1분마다 실행되게끔 작성해본다.

 

 

 

 

 

- dao의 메소드다. 별도로 전달할 인자값은 없다.

 

 

 

 

 

- reservation-mapper.xml의 쿼리다.

실제로는 예약을 신청한 지 7일이 지났는데도 "예약신청중" 상태인 데이터들의 상태를 "예약신청반려"로 update할 예정이지만, 우선 1일이 지난 데이터들을 대상으로 해본다.

 

- TRUNC를 쓰지 않으면 날짜뿐 아니라 시간차이까지 포함하여 계산되기 때문에 정확히 24시간이 지나야 한다.

 

 

 

 

 

 

 

- 아까 DB를 조회했을 때 1일이 지나도록 "예약신청중"인 예약신청의 개수는 6개였다.

"예약신청중"인 예약신청들의 status를 "예약신청반려"로 update하는 쿼리가 정상적으로 실행되었다. 

 

 

 

 

 

 

 

 

 

 

- 실제로는 7일이 경과한 데이터들을 대상으로 매일 밤 12시에 스케줄러가 실행되게끔 하였다.

 

 

 

 

※ 스케줄러 사용시 주의점

- 스케줄러는 특정 사용자의 세션 정보에 의존하지 않는 방식으로 설계해야 한다. 

- 스케줄러가 실행되는 시점에 HTTP 요청이나 세션이 존재하지 않기 때문이다.