API

스프링 스케줄러와 배치 #1 - Spring Scheduler(@Scheduled), TaskScheduler

letsDoDev 2025. 3. 9. 19:28

스케줄러란?

특정 작업을 예약된 시점에 수행 가능하도록 스프링 프레임워크에서 제공하는 기능

사옹 예 ) 예약 알림, 이메일 전송, 피드 알림 등등 

 

- 프로젝트 설정

 

- 스케줄러 사용 시 옵션 정리

fixedRate 이전 작업 시작 시점 기준, 주기적으로 실행 fixedRate = 5000 작업 시작 후 5초마다 실행
fixedDelay 이전 작업 종료 시점 기준, 주기적으로 실행 fixedDelay = 5000 작업 종료 후 5초 뒤 실행
initialDelay 작업 시작 전 대기 시간 설정 fixedRate = 5000, initialDelay = 10000 10초 대기 후 5초마다 실행
cron Cron 표현식을 사용해 일정 예약 cron = "0 0/5 * * * *" 매 5분마다 실행
zone 타임존 설정 (기본은 시스템 타임존 사용) cron = "0 0 12 * * ?", zone = "America/New_York" 뉴욕 시간 기준, 매일 오후 12시 실행
└ cron 표현식
초(0-59) 분(0-59) 시(0-23) 일(1-31) 월(1-12) 요일(0-7, 일=0 또는 7)
0 0 12 * * ? 매일 오후 12시 실행
0 0/5 * * * * 매 5분마다 실행
0 0 8-17 * * MON-FRI 월-금 오전 8시~오후 5시 매 정시 실행
0 0 0 1 * ? 매달 1일 자정 실행
0 0 0 * * ? 매일 자정 실행

 

- @Scheduler VS TaskScheduler 차이

스케줄 방식 고정된 주기로 실행 (fixedRate, cron 등) 동적으로 실행 시간 설정 가능
시간 설정 시점 애플리케이션 시작 시점 런타임에 동적 시간 설정 가능
동적 스케줄링 미지원 (시간 변경 불가) 지원 (RequestBody 등 외부 값 반영)
주기 설정 방법 @Scheduled(fixedRate = 1000) 등 사용 taskScheduler.schedule(task, time) 사용
비동기 처리 별도 설정 필요 (@Async 추가) 별도 설정 필요(@Async)
동시 실행 작업 수 단일 스레드(순차 실행) 멀티 스레드 처리 가능
(풀 크기 조절 -> ThreadPoolTaskScheduler 활용)
사용 사례 정해진 시간마다 실행(매일 자정, 매 5초마다) 예약된 시간 기반 작업(요청 시간 1분 후 실행)
유연성 낮음 — 설정된 시간에만 실행 높음 — 동적 시간 설정, 작업 취소 가능
주요 어노테이션/메소드 @Scheduled taskScheduler.schedule()
예시 고정 시간에 로그 남기기, 매일 데이터 백업 사용자가 요청한 시간에 메일 발송, 특정 시간 이후 작업 실행

 

- 비동기 처리 관련 차이

ThreadPoolTaskScheduler(config) + TaskScheduler
+ @Aysync
정기적인 작업 + 동시성 제어 + 비동기 처리 복잡한 작업
+ 대규모 작업 시 아용
@Async + TaskScheduler 동적 작업 예약 + 비동기 처리 복잡한 작업 시 사용
@Async + @Scheduled 고정된 주기 작업 (크론 표현식 사용)
+ 비동기 처리
간단한 작업 시 사용

 

여기서 더 깊게 보자!

ThreadPoolTaskScheduler(config) + TaskScheduler + @Async 와 @Async + TaskScheduler 의 차이

구분 ThreadPoolTaskScheduler(config) + TaskScheduler + @Async @Async + TaskScheduler
스레드 관리 ThreadPoolTaskScheduler를 통해 스레드 풀을 세밀하게 제어 TaskScheduler로 주기적으로 작업을 예약 가능
주기적 작업 실행 비동기 처리를 위해 @Async와 함께 사용할 수 있음 메소드가 비동기적으로 실행되도록 @Async 사용
비동기 실행 스레드 관리와 설정 가능 간단한 비동기 처리에 유용
유연성 스레드 관리와 설정 가능 간단한 비동기 처리에 유용
복잡성 설정이 더 복잡하지만, 많은 설정과 스케줄링에 적합 간단하고 직관적이나 유연성 부족 

 


 

 

Spring Boot에서는

기본적으로 @EnableScheduling과 TaskScheduler가 자동으로 설정되므로, @Configuration 클래스를 별도로 작성하지 않아도 된다.

Spring Boot는 ThreadPoolTaskScheduler 구현체를 자동으로 빈으로 등록해주기 때문에

TaskScheduler 인터페이스를 사용하여 주입할 수 있게 해준다.

  • Spring Boot는 기본적으로 ThreadPoolTaskScheduler를 사용하여 TaskScheduler 빈을 자동으로 등록
  • @Autowired로 TaskScheduler를 주입받으면, Spring이 내부적으로 ThreadPoolTaskScheduler 빈을 주입
  • 별도로 @Configuration 파일을 작성할 필요 없이 @Autowired로 바로 TaskScheduler를 사용할 수 있음

 

 

나는 간단한 테스트이므로 @Async + @Scheduled, @Async + TaskScheduler 케이스만 실습 진행할 예정


[테스트 프로젝트]

 

 

- SchedulerbatchApplication.java

package com.study.schedulerbatch;

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

@SpringBootApplication
@EnableScheduling
@EnableAsync
public class SchedulerbatchApplication {

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

}

 

- SchedulerController.java

package com.study.schedulerbatch.scheduler.controller;

import com.study.schedulerbatch.scheduler.service.SchedulerService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;

@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/scheduler/log")
public class SchedulerController {

    private final SchedulerService schedulerService;

    @PostMapping("/set")
    public ResponseEntity<?> setSchedulerLog(@RequestBody Map<String,Object> logObject) {
        try {
            // API 호출 시간 기록
            LocalDateTime apiTime = LocalDateTime.now();
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            String formattedTime = apiTime.format(formatter);
            log.info("[API-REQUEST-TIME (setTaskSchedulerLog)] ▶▶▶▶▶ " + formattedTime);

            // @Async + @Scheduled
            // @Scheduled 어노테이션이 붙은 메소드에는 파라미터를 사용할 수 없다고 한다ㅠㅜ
            // @Scheduled 어노테이션이 붙은 메소드는
            // schedulerService.setSchedulerLog();

            // @Async + taskScheduler
            schedulerService.setTaskSchedulerLog(logObject);

            return new ResponseEntity<Map<String,?>>(logObject, HttpStatus.CREATED);
        } catch (Exception e) {
            log.error(e.getLocalizedMessage());
            return new ResponseEntity<String>(e.getLocalizedMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

 

- SchedulerService.java

package com.study.schedulerbatch.scheduler.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@RequiredArgsConstructor
@Service
public class SchedulerService {

    private final TaskScheduler taskScheduler;
    
    /**
     * scheduler 를 이용한 API 호출 시점 + 50초 뒤 log 기록하는 메소드
     * @param
     * @return logObject
     */

// @Scheduled 옵션들 정리 *****************************************************************************
// [ 바로 실행되게끔 설정하는 경우 (서버 시작 시 바로 실행) ] => 무한 실행 위험!! - 사용 X - 정리만!!
// 1. fixedRate = 0 : 서버 시작 시 바로 실행 후, 이후 주기에 맞게 실행
// @Scheduled(fixedRate = 0) // 바로 실행되고 이후 주기마다 실행

// 2. cron 표현식에서 "0"으로 시작하는 경우
//    - 예: "0 * * * * ?" : 매분 0초에 실행되며, 서버 시작 후 첫 실행도 바로 일어남
// @Scheduled(cron = "0 * * * * ?") // 매분 0초에 실행

// 3. initialDelay = 0, fixedDelay = 0 : 서버 시작 후 즉시 실행 =
// @Scheduled(initialDelay = 0, fixedDelay = 0) // 서버 시작 후 즉시 실행
// [ 주기적 실행 옵션 경우 ]
// 1. fixedRate: 호출 시 첫 실행은 바로 발생하고, 이전 작업의 "시작 시간" 기준 - 일정 시간 간격으로 반복 실행
// @Scheduled(fixedRate = 1000) // 이전 작업 시작 후 1초 마다 반복 실행

// 2. fixedDelay: 호출 시 첫 실행은 바로 발생하고, 이전 작업의 "종료 시간" 기준, 지정된 시간 간격으로 반복 실행
// @Scheduled(fixedDelay = 1000) // 이전 작업 완료 후 1초 마다 반복 실행

// 3. initialDelay: 지정된 시간 후에 첫 번째 실행, 이후에는 fixedRate 또는 fixedDelay에 따라 실행
//  @Scheduled(initialDelay = 5000, fixedRate = 1000) // 5초 후 첫 실행, 그 후 매 1초마다 실행

// 4. cron: cron 표현식을 사용한 복잡한 스케줄링 (초단위까지 조정 가능)
//  @Scheduled(cron = "0 0/1 * * * ?") // 매 1분마다 실행
// ***************************************************************************************************

    // api 호출 후 바로 실행  + 1분 마다 다시 실행
    @Scheduled(fixedDelay = 1000*60)
    @Async
    // public void setSchedulerLog(Map<String,Object> logObject) {
    public void setSchedulerLog() {
        LocalDateTime apiTime = LocalDateTime.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String formattedTime = apiTime.format(formatter);
        String date = formattedTime;
        String content = "@Schduled 어노테이션이 붙은 메소드에는 파라미터를 사용할 수 없다고 한다 ㅠㅠ";

        Map<String,Object> logObject = new HashMap<>();
        logObject.put("date",date);
        logObject.put("content",content);

        log.info("[setSchedulerLog]");
        log.info(
                "date : " + date
                + " , content : " + content
                + " , log object : " + logObject
        );
    }

    /**
     * @Async + taskscheduler 를 이용한 API request Body 타겟 시간 + 1분 뒤 log 기록하는 메소드
     * @param logObject
     * @return logObject
     */
    public void setTaskSchedulerLog(Map<String,Object> logObject) {

        String dateStr = (String) logObject.get("date"); // 입력받은 시간
        LocalDateTime dateTime = LocalDateTime.parse(dateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));

        LocalDateTime targetDate = dateTime.plusMinutes(1); // 입력받은 시간 + 1분 계산
        String content = (String) logObject.get("content");

        // 1분 후 시간을 Instant로 변환 (TaskScheduler는 Instant 객체를 사용)
        // Instant는 절대적인 시점을 나타내므로, LocalDateTime을 Instant로 변환하여 사용
        Instant scheduledTime = targetDate.atZone(ZoneId.systemDefault()).toInstant();

        log.info("[setTaskSchedulerLog]");
        log.info("setTaskSchedulerLog - scheduledTime : " + targetDate);

        // 정해진 시간에 한 번만 실행시키는 경우
        taskScheduler.schedule(() -> {
            asyncTaskSchedulerLog(content);
        }, scheduledTime);

        // api 호출 시 첫 실행시키고, 1분마다 반복실행 시키는 경우
        /*
        taskScheduler.scheduleAtFixedRate() -> {
             asyncTaskSchedulerLog(content);
        }, 1000 * 60);
        */

        // 첫 실행자체를 정해진 시간에 실행시키고 그 이후부터 1분 마다 반복실행 시키는 경우
        /*
        taskScheduler.schedule(() -> {
            asyncTaskSchedulerLog(content);
        }, Date.from(firstExecutionTime.atZone(scheduledTime, 1000 * 60)); // 매개변수 (실행로직, Date.from(첫 실행 시작 시점, 이후 반복 주기))
        */
    }

    @Async
    public void asyncTaskSchedulerLog(String content) {
        log.info(
                "setTaskSchedulerLog - runTime : " + LocalDateTime.now()
                 + " , content : " + content
        );
    }
}

 

[ 결과 ]

025-03-09T19:08:52.336+09:00  INFO 11196 --- [schedulerbatch] [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2025-03-09T19:08:52.340+09:00  INFO 11196 --- [schedulerbatch] [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2025-03-09T19:08:52.409+09:00  WARN 11196 --- [schedulerbatch] [           main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2025-03-09T19:08:52.739+09:00  INFO 11196 --- [schedulerbatch] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2025-03-09T19:08:52.747+09:00  INFO 11196 --- [schedulerbatch] [           main] c.s.s.SchedulerbatchApplication          : Started SchedulerbatchApplication in 2.071 seconds (process running for 2.435)
2025-03-09T19:08:52.749+09:00  INFO 11196 --- [schedulerbatch] [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: []
2025-03-09T19:08:52.752+09:00  INFO 11196 --- [schedulerbatch] [         task-1] c.s.s.s.service.SchedulerService         : [setSchedulerLog]
2025-03-09T19:08:52.752+09:00  INFO 11196 --- [schedulerbatch] [         task-1] c.s.s.s.service.SchedulerService         : date : 2025-03-09 19:08:52 , content : @Schduled 어노테이션이 붙은 메소드에는 파라미터를 사용할 수 없다고 한다 ㅠㅠ , log object : {date=2025-03-09 19:08:52, content=@Schduled 어노테이션이 붙은 메소드에는 파라미터를 사용할 수 없다고 한다 ㅠㅠ}
2025-03-09T19:08:57.892+09:00  INFO 11196 --- [schedulerbatch] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2025-03-09T19:08:57.893+09:00  INFO 11196 --- [schedulerbatch] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2025-03-09T19:08:57.893+09:00  INFO 11196 --- [schedulerbatch] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
2025-03-09T19:08:57.976+09:00  INFO 11196 --- [schedulerbatch] [nio-8080-exec-1] c.s.s.s.controller.SchedulerController   : [API-REQUEST-TIME (setTaskSchedulerLog)] ▶▶▶▶▶ 2025-03-09 19:08:57
2025-03-09T19:08:57.976+09:00  INFO 11196 --- [schedulerbatch] [nio-8080-exec-1] c.s.s.s.service.SchedulerService         : [setTaskSchedulerLog]
2025-03-09T19:08:57.977+09:00  INFO 11196 --- [schedulerbatch] [nio-8080-exec-1] c.s.s.s.service.SchedulerService         : setTaskSchedulerLog - scheduledTime : 2025-03-09T19:11
2025-03-09T19:09:52.761+09:00  INFO 11196 --- [schedulerbatch] [         task-2] c.s.s.s.service.SchedulerService         : [setSchedulerLog]
2025-03-09T19:09:52.761+09:00  INFO 11196 --- [schedulerbatch] [         task-2] c.s.s.s.service.SchedulerService         : date : 2025-03-09 19:09:52 , content : @Schduled 어노테이션이 붙은 메소드에는 파라미터를 사용할 수 없다고 한다 ㅠㅠ , log object : {date=2025-03-09 19:09:52, content=@Schduled 어노테이션이 붙은 메소드에는 파라미터를 사용할 수 없다고 한다 ㅠㅠ}
2025-03-09T19:10:52.765+09:00  INFO 11196 --- [schedulerbatch] [         task-3] c.s.s.s.service.SchedulerService         : [setSchedulerLog]
2025-03-09T19:10:52.765+09:00  INFO 11196 --- [schedulerbatch] [         task-3] c.s.s.s.service.SchedulerService         : date : 2025-03-09 19:10:52 , content : @Schduled 어노테이션이 붙은 메소드에는 파라미터를 사용할 수 없다고 한다 ㅠㅠ , log object : {date=2025-03-09 19:10:52, content=@Schduled 어노테이션이 붙은 메소드에는 파라미터를 사용할 수 없다고 한다 ㅠㅠ}
2025-03-09T19:11:00.006+09:00  INFO 11196 --- [schedulerbatch] [   scheduling-1] c.s.s.s.service.SchedulerService         : setTaskSchedulerLog - runTime : 2025-03-09T19:11:00.006481500 , content : 스케줄러 테스트
2025-03-09T19:11:52.779+09:00  INFO 11196 --- [schedulerbatch] [         task-4] c.s.s.s.service.SchedulerService         : [setSchedulerLog]
2025-03-09T19:11:52.779+09:00  INFO 11196 --- [schedulerbatch] [         task-4] c.s.s.s.service.SchedulerService         : date : 2025-03-09 19:11:52 , content : @Schduled 어노테이션이 붙은 메소드에는 파라미터를 사용할 수 없다고 한다 ㅠㅠ , log object : {date=2025-03-09 19:11:52, content=@Schduled 어노테이션이 붙은 메소드에는 파라미터를 사용할 수 없다고 한다 ㅠㅠ}
2025-03-09T19:12:48.608+09:00  INFO 11196 --- [schedulerbatch] [ionShutdownHook] o.s.b.w.e.tomcat.GracefulShutdown        : Commencing graceful shutdown. Waiting for active requests to complete
2025-03-09T19:12:48.612+09:00  INFO 11196 --- [schedulerbatch] [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown        : Graceful shutdown complete
2025-03-09T19:12:48.615+09:00  INFO 11196 --- [schedulerbatch] [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2025-03-09T19:12:48.617+09:00  INFO 11196 --- [schedulerbatch] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariCP - Shutdown initiated...

 


※ 정리

@Scheduled

(1) 한 번만 실행하는 경우는 없으며, 특정 주기마다 반복 실행 시키는 것만 가능

(2) 이 어노테이션으로 실행시킬 메소드에는 파라미터를 사용할 수 없음

(3) BootApplication 실행 시 (서버 띄울 때)  자동 실행

(4) 고정된 시점의 작업

 

TaskScheduler 

(1) 한 번만 실행하는 경우 + 반복 실행시키는 경우도 가능

(2) 메소드 안에서  TaskScheduler 객체의 메소드를 실행시키는 것이기 때문에 당연히 파라미터 사용 가능

(3) API 요청 시 실행 됨 

(4) 동적으로 request 받은 시점의 작업

(5) 스레드 관리가 가능하여 대규모 어플리케이션 및 작업 시 유리 -> ThreadPoolTaskScheduler - config

    └

package com.example.scheduler.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.TaskScheduler;

@Configuration
public class SchedulerConfig {

    /**
     * ThreadPoolTaskScheduler를 Bean으로 등록하여 스케줄링 작업을 관리
     * - 대규모 작업에서 스레드 수를 효율적으로 관리
     * - ThreadPoolTaskScheduler는 스레드 풀 크기, 이름 등을 설정할 수 있어 대규모 작업 시 유용
     * 
     * @return TaskScheduler Bean
     */
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        
        // 스레드 풀의 크기 설정 (최대 10개의 스레드를 사용할 수 있도록 설정)
        scheduler.setPoolSize(10); // 스레드 풀 크기 설정
        
        // 스레드 이름에 접두사를 설정하여 어떤 스레드가 작업을 수행 중인지 확인 가능
        scheduler.setThreadNamePrefix("scheduler-task-");
        
        // 스케줄러를 초기화
        scheduler.initialize();
        
        // 초기화된 scheduler를 반환
        return scheduler;
    }
}