ㅁ 참고
- 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 요청이나 세션이 존재하지 않기 때문이다.