자바 Async 연습

이규훈·2024년 5월 12일

@Configuration // 이 어노테이션을 붙여야 스프링이 이 클래스를 설정 클래스로 인식한다.
public class AppConfig {

    @Bean(name ="defaultTaskExecutor") // 이 어노테이션을 붙여야 스프링이 이 메서드가 반환하는 객체를 빈으로 등록한다.
    public ThreadPoolTaskExecutor defaultTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(200); // 기본 스레드 개수
        taskExecutor.setMaxPoolSize(300); // 최대 스레드 개수
        return taskExecutor;
    }


    @Bean(name ="messagingTaskExecutor", destroyMethod = "shutdown") // destroyMethod 속성을 사용하면 빈이 소멸될 때 호출할 메서드를 지정할 수 있다.
    public ThreadPoolTaskExecutor messagingTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(10); // 기본 스레드 개수
        taskExecutor.setMaxPoolSize(100); // 최대 스레드 개수
        return taskExecutor;
    }
}

@Service
@RequiredArgsConstructor
public class AsyncService {

    private final EmailService emailService;
    public void asyncCall_1(){
        System.out.println("asyncCall_1 : {}" + Thread.currentThread().getName());
        emailService.sendEmail();
        emailService.sendEmailWithCustomThreadPool();
    }

    /**
     * 2,3번 처럼 하면 비동기 처리가 안된다. 
     * */

    public void asyncCall_2(){ // 인스턴스 선언 후 비동기처리 되는지 확인
        System.out.println("asyncCall_2 : {}" + Thread.currentThread().getName());
        EmailService emailService = new EmailService();
        emailService.sendEmail();
        emailService.sendEmailWithCustomThreadPool();
    }

    public void asyncCall_3(){ // 내부 메서드 비동기처리 되는지 확인
        System.out.println("asyncCall_3 : {}" + Thread.currentThread().getName());
        sendemail();
    }

    @Async
    public void sendemail(){
        System.out.println("sendemail : {}" + Thread.currentThread().getName());
    }

2,3번처럼 하면 비동기처리 안된다.

비동기처리를 하려면 spring의 도움을 받아야하는데, 비동기로 처리하고자 하는 메서드는
빈으로 등로되어있고 이 빈은 Async하게 하기 위해 프록시한 객체로 wrapping이 하게 되고
그래서 순수한 서비스가 아닌 wrapping된 이메일 서비스를 받게 된다.

그래서 메서드 호출하게 되면 비동기로 동작할 수 있게 Sub Thread에게 위임한다.

2번째는 빈은 사용하는 것이 아니고 그냥 호출하는 거라서 동기적으로 되는거고

3번째는 가장 실수 할 수 있는 케이스다.
@Async가 있으니 될 것이라고 생각하기 쉽니다. 그러나 안된다.
왜냐하면 이미 AsyncService는 빈을 가져와고 해당 빈 안에 있는 메서드를 다이렉트로 접근을 하게 되면 이 플로우 과정에서는 스프링의 도움(빈을 Wrapping해서 그 랩핑된 빈을 사용할 수 있도록하는 도움)을 받을 수 없다.
결국 3번은 비동기 에노테이션을 붙이지 않은 것 처럼 동기적으로 동작한다.


좀 더 자세히

2번이 안되는 이유

2번 asyncCall_2() 메서드은 비동기로 동작하지 않습니다.

그 이유는 2번에서 EmailService 인스턴스를 직접 생성하기 때문입니다.

EmailService emailService = new EmailService();

이렇게 직접 인스턴스를 생성하면, 스프링이 관리하는 빈이 아니라 단순 POJO(Plain Old Java Object) 객체입니다.

스프링의 @Async 기능은 스프링 빈에만 적용되고, 프록시 기반으로 비동기 작업을 위임합니다. 그래서 스프링 빈이 아닌 일반 객체에서는 @Async가 동작하지 않습니다.

따라서 1번 asyncCall_1() 메서드에서 사용하는 emailService만 스프링 빈이고, @Async 기능이 동작하여 비동기로 실행됩니다.

2번과 3번 모두 스프링 빈이 아닌 객체의 메서드를 호출하므로 @Async 동작이 적용되지 않아 동기적으로 실행됩니다.

정리하면, @Async가 동작하려면 스프링 빈의 메서드에 적용되어야 하며, 빈이 아닌 일반 객체에서는 동작하지 않습니다.

3번이 안되는 이유

3번 케이스의 sendemail() 메서드를 보면 이는 AsyncService 클래스 내부 메서드입니다. 스프링은 AsyncService를 빈으로 등록할 때, 내부 메서드가 아닌 외부에서 접근 가능한 메서드에 대해서만 프록시를 생성합니다. 따라서 sendemail()에 @Async를 달아도 프록시 적용이 되지 않아 비동기 동작이 적용되지 않는 것입니다.
정리하면, @Async가 동작하려면 외부에서 접근 가능한 메서드에 적용되어야 하며, 스프링이 해당 빈에 프록시를 생성할 수 있어야 합니다. 내부 메서드에는 프록시 생성이 적용되지 않기 때문에 @Async 동작도 되지 않습니다.


@Async 에너테이션 사용할때 public으로 접근제어자를 설정해줘야하는 것도 잊지 말자.

profile
개발취준생

0개의 댓글