본 포스트는 학습 중 생긴 궁금증 해결 및 아이디어 검증에 관한 내용을 정리 및 공유용도가 아닌 기록 용도로 작성하였습니다.
학습 Q&A 태그 포스팅은 동일하게 아래와 같이 진행될 예정입니다.
- 학습 중 생긴 여러 아이디어가 실현 가능한지 확인한 결과를 보관
- 학습 중 생긴 궁금증을 해소한 과정 및 결과를 보관
- 추후 개발에 해당 아이디어들을 참고하기 위한 기록 용도이므로 구조가 자유롭고 다소 두서없을 수 있으며, 자유롭게 작성
@Scheduled 메서드는 동기로 실행DefaultManagedTaskScheduler 를 생성SingleThreadScheduledExecutor 를 스레드풀로 사용@Scheduled 메서드는 기본적으로 동기로 실행되므로, 한 @Scheduled 메서드가 실행 중이면 완료시까지 다른 메서드들은 실행되지 못함@Scheduled 메서드가 비슷한 시간에 여러 개 작성되어 있을 경우, 문제가 발생할 수 있음@Scheduled 메서드는 내부적으로 ScheduledAnnotationBeanPostProcessor -> TaskScheduler 인터페이스를 기준으로 동작ThreadPoolTaskScheduler 는 TaskScheduler 인터페이스의 구현체ThreadPoolTaskScheduler 타입으로 반환TaskScheduler 는 기능이 제한적ThreadPoolTaskScheduler 를 사용하면 구체 클래스 설정 편리
@Scheduled메서드가 자동으로 등록하는 스레드풀 빈 이름
@Scheduled 메서드는 taskScheduler 라는 이름의 스레드풀 빈을 먼저 탐색 후 사용taskScheduler 로 설정하여 빈으로 등록하면, @Scheduled 메서드에서 이 스레드풀 빈을 자동으로 DI 받아서 사용SimpleAsyncTaskExecutor 를 기본 스레드풀로 지정GC 에 의해 처리가 되기는 함@Async 메서드는 내부적으로 AsyncExecutionAspectSupport -> Executor 인터페이스를 기준으로 동작Executor 타입으로 반환
@Async메서드가 자동으로 등록하는 스레드풀 빈 이름
@Async 메서드는 taskExecutor 라는 이름의 스레드풀 빈을 먼저 탐색 후 사용taskExecutor 로 설정하여 빈으로 등록하면, @Async 메서드에서 이 스레드풀 빈을 자동으로 DI 받아서 사용@Async 메서드와는 달리, @Scheduled 메서드 실행 수는 개발자가 충분히 예측 가능CorePoolSize 만 설정해줘도 충분CorePoolSize 만 조금씩 증가QueueSize 의 경우, ThreadPoolTaskScheduler 는 내부적으로 ScheduledThreadPoolExecutor 사용ScheduledThreadPoolExecutor 는 대기 큐를 자체적으로관리🎯 따라서 별도의 큐 설정은 불필요하며,
setPoolSize()로 풀 사이즈만 조정해주면 된다.
@Configuration
@EnableAsync
@EnableScheduling
public class ThreadPoolConfig {
private final int core = Runtime.getRuntime().availableProcessors();
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(core);
executor.setMaxPoolSize(core * 10);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("async-");
return executor;
}
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(core * 2);
scheduler.setThreadNamePrefix("scheduler-");
return scheduler;
}
}
taskExecutor 빈은 @Async 메서드에서 자동으로 사용@Async 메서드는 일반적으로 I/O 바운드 작업일 확률이 높으므로, maxPoolSize 를 core * 10 으로 설정queueCapacity 는 1,000 으로 설정taskScheduler 빈은 @Scheduled 메서드에 자동으로 사용@Scheduled 메서드는 예약 작업이므로, 개발자가 어느 정도 실행될지 예상 가능corePoolSize 를 core 수 만큼, 혹은 넉넉하게 core * 2 로 설정@Scheduled 로 설정한 작업이 많다면 그에 비례해 성능 테스트 및 부하 테스트 진행해가며 조율ThreadNamePrefix 를 async-, scheduler-로 구분📌 각 용도의 스레드풀을 명시적으로 지정하는 방법
// ...
public class ThreadPoolConfig implements AsyncConfigurer, SchedulingConfigurer {
// ...
// @Async 전용 스레드풀 지정
@Override
public Executor getAsyncExecutor() {
return taskExecutor();
}
// @Scheduled 전용 스레드풀 지정
@Override
public void configureTasks(ScheduledTaskRegistrar registrar) {
registrar.setScheduler(taskScheduler()); // taskScheduler() 반환 타입을 Executor 로 변경
}
}
getAsyncExecutor() 에서 생성한 스레드풀 반환@Async 메서드 전용 스레드풀로 사용configureTasks() 에서 스케쥴러에 사용할 스레드풀 지정taskScheduler() 반환 타입을 기존 ThreadPoolTaskScheduler -> Executor 로 변경registrar.setScheduler 에서 해당 스레드풀 사용하도록‼️ 처음 방식으로 충분히 구현할 수 있으므로, 추천하는 방식은 아님
💡 설정에 사용될 코어 수 제한 등의 값들은 설정 파일과
@ConfigurationProperties조합을 이용해 설정해두는 것이 효율적일 수 있다.
custom:
thread-pool:
async:
core: #{T(java.lang.Runtime).getRuntime().availableProcessors()}
max: #{T(java.lang.Runtime).getRuntime().availableProcessors() * 10}
queue-capacity: 1000
scheduler:
pool: #{T(java.lang.Runtime).getRuntime().availableProcessors() * 2}
SpEL 식을 이용해서 동적으로 CPU 코어 수 환산@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "custom.thread-pool")
public class ThreadPoolProperties {
private Async async = new Async();
private Scheduler scheduler = new Scheduler();
@Getter
@Setter
public static class Async {
private int core;
private int max;
private int queueCapacity;
}
@Getter
@Setter
public static class Scheduler {
private int pool;
}
}
ThreadPoolConfig 에서 해당 빈을 주입 받아서 사용