[Spring Boot] @Async를 활용한 비동기 처리와 멀티 스레드

Sungjin Cho·2024년 7월 25일
0

Spring Boot

목록 보기
8/15
post-thumbnail

Sprinb Boot @Async와 Thread

비동기 처리를 하는 이유

비동기 처리를 생각하게 된 이유는 현재 Spring Batch를 사용하면서 여러 가지의 Job을 처리하는데 날짜에 따른 할인율, 폐기 여부의 업데이트는 매일 자정, 즉 하루에 한 번만 업데이트 되기 때문에 비동기 처리에 대해 고려할 필요가 없었다. 하지만 텔레그램 메시지 전송 같은 경우에는 초 단위로 테이블을 풀링해서 데이터를 확인하고 주문이 들어오거나 재고가 떨어지면 메시지를 전송해야 하기 떄문에 비동기 처리가 필요한 상황이었다.

기존 사용하고 있는 Spring Boot가 아닌 다른 프레임워크의 java 코드에서는 thread를 직접 구현해서 멀티 스레드 방식으로 이러한 처리를 진행하였지만 Spring Boot에서는 Async 어노테이션으로 간단하게 비동기 실행을 처리할 수 있다.

그렇다면 Spring Boot에서도 스레드를 상속받아 클래스를 구현할 수 있는가?

가능하다. 하지만 Spring Boot에서는 비동기 방식을 Async로 간단하게 지원하는만큼 Async를 사용해서 얻는 장점이 더 많다.

@Async

차이점

  • 추상화 수준: @AsyncExecutor는 스레드를 추상화하여 비동기 작업을 간편하게 수행할 수 있도록 한다. 직접 스레드를 구현할 필요 없이 어노테이션과 설정만으로 비동기 처리를 구현할 수 있다.
  • 스레드 관리: 스프링이 스레드 풀을 관리하고 재사용한다. 개발자는 스레드 풀의 크기나 동작을 설정할 수 있지만, 개별 스레드를 직접 생성하거나 관리할 필요는 없다.
  • 에러 처리: 스프링은 비동기 작업의 에러 처리를 위한 여러 가지 메커니즘을 제공한다. 예를 들어, @Async 메서드에서 발생한 예외를 처리하는 AsyncUncaughtExceptionHandler를 설정할 수 있다.

장점

  • 간편성: 설정과 어노테이션만으로 비동기 작업을 쉽게 구현할 수 있다.
  • 효율성: 스프링이 스레드 풀을 효율적으로 관리하므로 자원을 효과적으로 사용할 수 있다.
  • 유지보수성: 코드가 간결하고 명확하여 유지보수가 쉽다.
  • 확장성: 스레드 풀의 설정을 통해 쉽게 확장할 수 있다. 필요에 따라 스레드 풀의 크기나 동작 방식을 조정할 수 있다.

단점

  • 추상화에 따른 제약: 매우 세밀한 스레드 관리가 필요한 경우, 추상화된 계층이 오히려 제약이 될 수 있다.
  • 의존성: 스프링 프레임워크에 의존적이다.

직접 스레드 구현

차이점

  • 추상화 수준: 직접 스레드를 구현하여 사용하면 모든 스레드 관리를 직접 수행해야 한다. 이는 더 낮은 수준의 추상화를 의미한다.
  • 스레드 관리: 개발자가 직접 스레드를 생성, 시작, 중지 및 관리해야 한다. 스레드 풀을 직접 구현할 수 있지만, 이는 추가적인 복잡성을 수반한다.
  • 에러 처리: 에러 처리를 위한 별도의 메커니즘을 직접 구현해야 한다.

장점

  • 유연성: 스레드를 직접 제어할 수 있어 매우 세밀한 조정이 가능하다. 특정한 요구사항이 있을 때 유용할 수 있다.
  • 프레임워크 독립성: 스프링에 의존하지 않으므로 다른 프레임워크나 라이브러리와 함께 사용할 수 있다.

단점

  • 복잡성: 스레드 관리, 동기화, 에러 처리 등 모든 것을 직접 구현해야 하므로 복잡성이 크게 증가한다.
  • 버그 발생 가능성: 직접 스레드를 관리하는 경우 동기화 문제나 자원 경합(deadlock) 등의 버그가 발생할 가능성이 높다.
  • 효율성 저하: 스레드 풀이 아닌 개별 스레드를 생성하고 관리하면 자원 사용 효율이 떨어질 수 있다.

결론

@AsyncExecutor를 사용하는 것이 더 일반적이고 효율적인 이유는 추상화된 스레드 관리와 간편한 설정 덕분에 복잡성을 줄일 수 있기 때문이다. 직접 스레드를 구현하는 것은 매우 세밀한 제어가 필요하거나, 특정한 프레임워크 의존성을 피해야 할 때 유용할 수 있다. 하지만 일반적인 애플리케이션 개발에서는 @AsyncExecutor를 사용하는 것이 더 권장된다.

@Async를 사용한 비동기 처리

  1. @EnableAsync 을 메인 어플리케이션에 붙여 Spring에 @Async 를 사용할 수 있도록 설정
  2. 비동기 동작이 필요한 메서드에 @Async 를 붙인다.
  3. 필요한 경우 @Configuration을 통해 AsyncConfiguration을 설정한다.

예제 코드

메인 애플리케이션

// 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 어노테이션을 활용해 간단하게 처리를 할 수 있었다.

0개의 댓글