비동기 처리를 생각하게 된 이유는 현재 Spring Batch를 사용하면서 여러 가지의 Job을 처리하는데 날짜에 따른 할인율, 폐기 여부의 업데이트는 매일 자정, 즉 하루에 한 번만 업데이트 되기 때문에 비동기 처리에 대해 고려할 필요가 없었다. 하지만 텔레그램 메시지 전송 같은 경우에는 초 단위로 테이블을 풀링해서 데이터를 확인하고 주문이 들어오거나 재고가 떨어지면 메시지를 전송해야 하기 떄문에 비동기 처리가 필요한 상황이었다.
기존 사용하고 있는 Spring Boot가 아닌 다른 프레임워크의 java 코드에서는 thread를 직접 구현해서 멀티 스레드 방식으로 이러한 처리를 진행하였지만 Spring Boot에서는 Async 어노테이션으로 간단하게 비동기 실행을 처리할 수 있다.
가능하다. 하지만 Spring Boot에서는 비동기 방식을 Async로 간단하게 지원하는만큼 Async를 사용해서 얻는 장점이 더 많다.
차이점
@Async
와 Executor
는 스레드를 추상화하여 비동기 작업을 간편하게 수행할 수 있도록 한다. 직접 스레드를 구현할 필요 없이 어노테이션과 설정만으로 비동기 처리를 구현할 수 있다.@Async
메서드에서 발생한 예외를 처리하는 AsyncUncaughtExceptionHandler
를 설정할 수 있다.장점
단점
차이점
장점
단점
@Async
와 Executor
를 사용하는 것이 더 일반적이고 효율적인 이유는 추상화된 스레드 관리와 간편한 설정 덕분에 복잡성을 줄일 수 있기 때문이다. 직접 스레드를 구현하는 것은 매우 세밀한 제어가 필요하거나, 특정한 프레임워크 의존성을 피해야 할 때 유용할 수 있다. 하지만 일반적인 애플리케이션 개발에서는 @Async
와 Executor
를 사용하는 것이 더 권장된다.
메인 애플리케이션
// GazaposDbApplication.java
@SpringBootApplication
@EnableScheduling
@EnableAsync
public class GazaposDbApplication {
public static void main(String[] args) {
SpringApplication.run(GazaposDbApplication.class, args);
}
}
우선 메인 애플리케이션에 @EnableAsync 어노테이션을 붙여준다.
비동기 처리가 필요한 job
// TelegramJobScheduler.java
@Component
@Slf4j
public class TelegramJobScheduler {
private final JobLauncher jobLauncher;
private final Job telegramJob;
public TelegramJobScheduler(
JobLauncher jobLauncher,
@Qualifier("myTelegramJob") Job telegramJob) {
this.jobLauncher = jobLauncher;
this.telegramJob = telegramJob;
}
@Scheduled(cron = "*/10 * * * * *") // 매 초마다 실행
@Async
public void runTelegramJob() {
log.info("Starting Telegram Job");
runJob(telegramJob, "telegram");
log.info("Finished Telegram Job");
}
private void runJob(Job job, String jobName) {
try {
JobParameters jobParameters = new JobParametersBuilder()
.addLong("time", System.currentTimeMillis())
.toJobParameters();
jobLauncher.run(job, jobParameters);
} catch (Exception e) {
log.error("Error occurred while running {} job: ", jobName, e);
}
}
}
비동기 실행이 필요한 runTelegramJob 메서드에 @Async를 붙여준다.
Thread 관련한 Confiuration 설정이 필요한 경우
// AstncConfig.java
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 코어 스레드 풀 크기 설정
executor.setMaxPoolSize(20); // 최대 스레드 풀 크기 설정
executor.setQueueCapacity(500); // 작업 큐 용량 설정
executor.setThreadNamePrefix("AsyncThread-"); // 스레드 이름 접두사 설정
// 작업이 완료된 후 스레드 풀이 종료될 때까지 대기할 시간 설정 (단위: 초)
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
}
2024-07-25T11:34:00.002+09:00 INFO 19716 --- [gazapos_db] [ AsyncThread-1] c.m.gazapos.sms.TelegramJobScheduler : Starting Telegram Job
2024-07-25T11:34:00.002+09:00 INFO 19716 --- [gazapos_db] [ AsyncThread-3] c.m.g.b.scheduler.DateUpdateScheduler : Starting update expiration date job
2024-07-25T11:34:00.002+09:00 INFO 19716 --- [gazapos_db] [ AsyncThread-2] c.m.g.b.scheduler.DateUpdateScheduler : Starting update distribution date job
2024-07-25T11:34:00.385+09:00 INFO 19716 --- [gazapos_db] [ AsyncThread-3] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=updateExpirationDateJob]] launched with the following parameters: [{'time':'{value=1721874840002, type=class java.lang.Long, identifying=true}'}]
2024-07-25T11:34:00.453+09:00 INFO 19716 --- [gazapos_db] [ AsyncThread-1] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=telegramJob]] launched with the following parameters: [{'time':'{value=1721874840002, type=class java.lang.Long, identifying=true}'}]
2024-07-25T11:34:00.515+09:00 INFO 19716 --- [gazapos_db] [ AsyncThread-2] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=updateDistributionDateJob]] launched with the following parameters: [{'time':'{value=1721874840002, type=class java.lang.Long, identifying=true}'}]
2024-07-25T11:34:00.642+09:00 INFO 19716 --- [gazapos_db] [ AsyncThread-3] o.s.batch.core.job.SimpleStepHandler : Executing step: [updateExpirationDateStep]
2024-07-25T11:34:00.751+09:00 INFO 19716 --- [gazapos_db] [ AsyncThread-1] o.s.batch.core.job.SimpleStepHandler : Executing step: [sendPendingMessagesStep]
// 생략 ..
2024-07-25T11:34:01.107+09:00 INFO 19716 --- [gazapos_db] [ AsyncThread-3] c.m.g.b.scheduler.DateUpdateScheduler : Finished update expiration date job
2024-07-25T11:34:01.173+09:00 INFO 19716 --- [gazapos_db] [ AsyncThread-2] o.s.batch.core.step.AbstractStep : Step: [updateDistributionDateStep] executed in 368ms
2024-07-25T11:34:01.241+09:00 INFO 19716 --- [gazapos_db] [ AsyncThread-1] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=telegramJob]] completed with the following parameters: [{'time':'{value=1721874840002, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 713ms
2024-07-25T11:34:01.241+09:00 INFO 19716 --- [gazapos_db] [ AsyncThread-1] c.m.gazapos.sms.TelegramJobScheduler : Finished Telegram Job
2024-07-25T11:34:01.364+09:00 INFO 19716 --- [gazapos_db] [ AsyncThread-2] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=updateDistributionDateJob]] completed with the following parameters: [{'time':'{value=1721874840002, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 771ms
2024-07-25T11:34:01.364+09:00 INFO 19716 --- [gazapos_db] [ AsyncThread-2] c.m.g.b.scheduler.DateUpdateScheduler : Finished update distribution date job
로그를 찍어서 확인해보면 각각 job이 시작되고 시작된 job이 끝나기 전에 다른 job이 시작되는 것을 확인할 수 있다.
이를 통해 비동기 처리가 필요한 메서드들에 대해 @Async 어노테이션을 활용해 간단하게 처리를 할 수 있었다.