[BooTakHae] Scheduler 도입기

Kim Hyen Su·2024년 5월 11일
0

BooTakHae

목록 보기
9/22
post-thumbnail

개요

이커머스 프로젝트를 진행하면서 주문 엔티티의 상태를 주기적으로 변경되도록 구현해줘야 했습니다.

이를 위해서 사용한 것이 Spring Boot에서 지원하는 스케줄러 입니다.

스케줄러는 일정에 따라서 작업을 수행하여 데이터베이스에서 데이터를 처리하고, 정기적으로 메모리 안에 캐시를 업데이트 하는 등의 작업을 수행할 수 있습니다.

해당 포스팅에서는 Spring Boot Scheduler 사용 방법을 학습한 뒤 프로젝트에 적용해보고 테스트 해보겠습니다.

📜 참고 문서

Scheduler_학습

Spring Boot Scheduler를 사용하기 위해서 별도의 dependency를 추가할 필요 없이 Spring Context Library에서 기본적으로 제공되는 것을 사용하면 됩니다.

Scheduler_사용법

Spring_Scheduling_tasks_docs

Spring 에서 제공하는 GuideLine을 따라하면 Scheduler 사용법이 간단하게 명시되어있습니다.

1. Scheduler 활성화

작업을 예약하기 위해서는 Spring Boot Scheduler를 사용할 애플리케이션 클래스 상단에 @EnableScheduling 어노테이션을 사용해야 활성화됩니다.

@Configuration 
@EnableScheduling
public class Application { 
	...
}

이제 해당 애플리케이션 내에 어디서든 스케줄링을 사용할 수 있습니다.

2. @Scheduled 추가

모든 작업에 대해서 예약을 수행하기 위해서는 메서드 상단에 @Scheduled 어노테이션을 추가해줘야 합니다.

@Scheduled 어노테이션은 다양한 속성을 가지며, 이를 통해 다양한 스케줄링 동작을 구현할 수 있습니다.

내부적으로 사용되는 모든 값들은 MilliSeconds 기준의 형식으로 표현됐습니다.(MS)

1) fixedDelay

해당 로직 완료 후 고정된 지연 시간이 지난 뒤 동일한 작업을 실행하는 경우와 같이 작업을 반복적으로 사용할 때, fixedDelay 속성을 사용할 수 있습니다.

@Scheduled(fixedDelay = 5000) 
public  void  printUsers () { 
   List<User> users = userRepo.findAll(); 
   users.stream().forEach(System.out::print); 
}

2) fixedRate

해당 로직이 완료될 때까지 기다리지 않고 특정 시간마다 작업을 실행하고 싶은 경우, fixedRate 속성을 사용할 수 있습니다.

@Scheduled(fixedRate = 5000) 
public  void  printUsers () { 
   List<User> users = userRepo.findAll(); 
   users.stream().forEach(System.out::print); 
}

3) initialDelay

첫번째 작업을 수행하기 전에 초기 시간 지연을 두고 진행하기 위해 initialDelay 속성을 사용할 수 있습니다.

@Scheduled(fixedDelay = 5000,initialDealy = 5000) 
public  void  printUsers () { 
   List<User> users = userRepo.findAll(); 
   users.stream().forEach(System.out::print); 
}

4) fixedDelayString & fixedRateString

문자열의 형태로 값을 설정하는 속성입니다.

fixedDealyString — 1시간 지연

@Scheduled (fixedDelayString = "PT1H" ) 
public void printUsers() { 
   List < User > users = userRepo.findAll(); 
   users.stream().forEach( System .out :: print); 
}

fixedRateString — 5초 지연

@Scheduled (fixedDelayString = "PT1H" ) 
public void printUsers() { 
   List < User > users = userRepo.findAll(); 
   users.stream().forEach( System .out :: print); 
}

💡 Ref : 위의 모든 속성들은 외부 설정 파일에서 값을 가져올 수 있습니다.

@Scheduled (fixedDelay = "${application.fixed.deplay}" ) 
@Scheduled (fixedRate = "${application.fixed.rate}" ) 
@Scheduled (fixedDelayString = "${application.fixed.deplay.string}" ) 
@Scheduled (fixedRateString = "${application.fixed.rate.string}" )

5) cron 표현식

예약된 간격이 너무 작거나 단순한 경우에 구체적인 간격 표현을 위해서 cron 표현식을 사용합니다.

cron 표현식을 사용 시 6개 또는 7개의 필드가 있는 문자열을 사용하여 표현됩니다.

@Scheduled(cron = "* * * * * *")

위 cron 표현식에서 사용된 모든 필드의 의미를 확인해보면, 다음과 같습니다.

위 필드의 갯수에서 6개의 필드만 사용하는 경우, 연도 필드가 무시됩니다.

Field Name      Required   Allowed Values      Allowed Special Characters
SEC             YES         0-59                , - * /
MIN             YES         0-59                , - * /
HRS             YES         0-23                , - * /
Day of Month    YES         1-31                , - * ? / L W   
MON             YES         1-12 or JAN-DEC     , - * /
Day of Week     YES         1-7 or SUN-SAT      , - * ? / L #  
YEAR            NO          empty, 1970-2099    , - * /

사용되는 특수 문자

  1. * : "모든 가능한 값"을 의미합니다.

  2. ? : Day of Month와 Day of Week에서 "지정하지 않음"을 의미합니다.

  3. / : "점차 증가되는 값"을 표현할 때 사용합니다. 예를 들면, SEC 가 0~59의 값을 가지므로, 5/15는 5초부터 시작하여 15초 단위로 증가함을 의미합니다. → 5, 20, 35, 50

  4. , : "여러 값"을 표현할 때 사용됩니다. 예를 들면, MIN 가 0~59의 값을 가지므로, 5,10의 의미는 5분과 10분 마다 실행됨을 의미합니다. → 5,10,15,20,25 ... 10,20,30,40...

  5. - : "범위 값"을 표현할 때 사용됩니다. 예를 들면, MIN을 5-10으로 표현하면, 5분에서 10분까지 매 분 실행됨을 의미합니다.

  6. L : 사용되는 필드에 따라서 마지막을 의미합니다.

  7. # : 매월 n번째 평일을 지정합니다. ex) 매월 넷째 월요일을 의미하는 경우, 2#4로 표현할 수 있습니다.


작성된 예시

// 매15초 마다 작업 실행
@Scheduled (cron = "*/15 * * * * *" ) 

// 매번이 아닌 12분에 작업 실행 
@Scheduled (cron = "0 12 * * * *" ) 

// 오후 3시부터 3시 10분까지 매분마다 작업 실행 
@Scheduled (cron = "0 0-10 15 * * *" ) 

// 6월 월요일부터 금요일 1시 15분과 1시 45분에 작업 실행 
@ Scheduled (cron = "0 15,45 13 ? 6 MON-FRI" ) 

// 매월 말일 1시 15분에 작업 실행 
@Scheduled (cron = "0 15 13 L * ?" ) 

// 매월 세 번째 금요일 1시 15분에 작업을 실행합니다. (일-토 : 1-7) 
@Scheduled (cron = "0 15 13 ? * 6#3" )

properties 파일을 통한 값 전달

cron 표현식의 값을 하드 코딩하는 대신에 placeholder를 사용하여 application.properties 파일에서 값을 전달해줍니다.

@Scheduled(cron = " ${application.scheduler.cron.expression} " )

application.properties

application.scheduler.cron.expression : * *  * *  * *

6) Macros

cron으로 작업 시 가장 좋은 옵션은 cron 표현식이지만, Spring에서 제공하는 또다른 옵션으로 Macros를 사용하여 가독성을 높히는 방법을 제공합니다.



3. Multi-Thread를 활용한 작업 실행

기본적으로 Spring Boot Scheduler는 1개의 Thread를 사용하여 다른 작업을 실행하지만, 작업을 실행하기 위해 Multi-Thread를 사용 해야 하는 경우 하나의 스레드가 막히더라도 다른 작업의 실행을 방해하지 않도록 스프링 부트를 구성할 수 있습니다.

접근 방법으로는 application.properties 파일을 통하거나 ThreadPoolTaskScheduler의 인스턴스를 생성 후 pool 크기값을 설정하고 @Bean 메서드를 통해 인스턴스를 반환하는 2가지 방법이 있습니다.

  1. application.properties 파일을 통해서 스레드 풀 크기를 설정할 수 있습니다.
spring.task.scheduling.pool.size : 3
  1. ThreadPoolTaskScheduler 사용
@Bean 
public TaskScheduler ConcurrentTaskScheduler () { 
   ThreadPoolTaskScheduler  taskScheduler  =  new  ThreadPoolTaskScheduler (); 
   taskScheduler.setPoolSize(Integer.valueOf( 3 )); 
   //taskScheduler.setPoolSize(Integer.valueOf("${app.thread.size}"));
   return taskScheduler;
}

Scheduler_적용

📜 요구사항

  • 주문상태 변경
    • 결제 완료 1일 후 배송 시작
    • 배송 시작 1일 후 배송 완료
    • 반품 후 1일이 지난 시점에 재고 복구 및 반품 완료

구현

Application.class

@EnableScheduling
public class OrderServiceApplication {

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

스케줄러를 활성화해줍니다.

OrderService

    /**
     * 매일 낮 12시에 확인
     */
    @Scheduled(cron = "${schedule.cron}")
    @Transactional
    public void changeOrderStatus() {
        log.info("주문 상태 업데이트 실행");

		// fixme : 쿼리 최적화(하위 조건에 맞는 데이터를 조회하여 값을 변경하도록 수행)
        List<OrderEntity> orderList = orderRepository.findAll();
        
        for(OrderEntity order : orderList){
            if(order.getStatus() == Status.PAYMENT
                    && Math.abs(Duration.between(order.getCreatedAt(),LocalDateTime.now()).toDays()) >= 1){
                order.startShipping();
            }
            else if(order.getStatus() == Status.SHIPPING
                    && Math.abs(Duration.between(order.getUpdatedAt(),LocalDateTime.now()).toDays()) >= 1){
                order.completeShipping();
            }
            else if(order.getStatus() != Status.RETURN
                    && order.getReturnOrder() != null
                    && Math.abs(Duration.between(order.getReturnOrder().getCreatedAt(),LocalDateTime.now())
                    .toDays()) >= 1){

                orderProductRepository.findByOrder(order).ifPresent(
                    (op) ->{
                        ResponseProduct response = updateStock(
                                StockProcess.RESTORE,
                                op.getProductId(),
                                op.getQty()
                        );
                        log.debug("재고_복구[{} 재고 : {}]", response.getName(), response.getStock());
                    }
                );

                order.returnTheOrder();
            }
        }
    }

로깅 확인 및 정상 응답 확인

작동 여부를 확인하기 위해서 schedule.cron을 매 5초마다 수행하도록 변경해보겠습니다.

로깅을 통해서 매 5초마다 주문 상태를 조회하는 로직이 수행됩니다.


profile
백엔드 서버 엔지니어

0개의 댓글