개발놀이터

@Async 어노테이션 본문

Spring/Spring

@Async 어노테이션

마늘냄새폴폴 2023. 5. 16. 13:15

꽤 전에 동기 비동기 프로그래밍에 대해서 이론적으로 학습하고 최근에 실습할 기회가 생겼습니다. 

 

SMTP를 이용해 2차인증을 요구하는 서비스를 개발하는데 SMTP가 동기 네트워킹이라 사용자가 이메일이 날아가는 3~4초정도를 손놓고 가만히 기다려야 하는 상황이 발생했습니다. 

 

이를 비동기 통신으로 바꾸면 좋겠다는 생각이 들어서 찾아보니 @Async를 적용하면 동기 통신을 비동기 통신으로 바꿔준다는 소리를 들었습니다. 

 

@Async

@Async는 스프링 AOP에 의해 동작하는 프록시 패턴 중 하나입니다. 해당 포스팅에선 프록시 패턴이 어떤 것인지는 다루지 않겠습니다. 

 

@Async가 설정된 메서드에 접근하면 프록시가 중간에서 샥 하고 가로채서 메인 스레드가 아닌 서브 스레드에 할당합니다. 

 

그렇기 때문에 @Async가 작동하기 위해서 몇가지 조건이 필요합니다. 

 

우선 결론부터 말하자면 @Async가 붙은 메서드가 public 타입이어야 한다는 것과 self-invocation (직접 호출) 을 피해야 합니다. 

 

이 두가지 조건에 특징이 있습니다. 바로 프록시 패턴이 작동하기 위한 조건이기 때문입니다. 

 

1. 프록시도 결국은 클래스다. 

프록시 객체도 결국은 스프링이 만든 클래스입니다. 이 프록시 객체가 Async가 붙은 메서드를 찾으려면 메서드가 private 하면 안되겠죠. 

 

때문에 @Async의 메서드는 반드시 public이어야 합니다. 

 

2. 직접 호출은 스프링의 도움을 받지 못한다. 

여기서 말하는 직접 호출은 같은 클래스에서 직접적으로 호출하는 방법을 의미합니다. 

 

public void semdMail(string to, String subject) {
	send(to, subject);
}

@Async
public void send(String to, String subject) {
	// 이메일 보내는 로직
}

 

이러한 방법과 비슷한 방법이 있습니다. 바로 동적으로 객체를 생성해서 호출하는 방식입니다. 

 

public void sendMail(String to, String subject) {
	EmailSender mailSender = new EmailSender();
    mailSender.send(to, subject);
}

@Service
@RequiredArgsConstructor
public class EmailService {
	
    private final JavaMailSender javaMailSender;
    
    public void send(String to, String subject) {
    	// 메일 보내는 로직
    }
}

 

프록시 객체를 타지 않는다는 말은 IoC의 도움을 받지 않는다는 의미이고 이는 @Async가 작동하지 않게되는 원인이 된다는 말입니다. 

 

때문에 우리는 반드시 @Async를 이용하기 위해선 이런식으로 코드를 짜야합니다. 

 

@RestController
@RequiredArgsConstructor
public class MailController {
	
    private final EmailService emailService;
    
    @GetMapping("/send")
    public String send(@RequestBody EmailDto dto) {
    	emailService.send(dto.getTo(), dto.getSubject());
        return "success";
    }
}

@Service
public class EmailService {
	
    @Async
    public void send(String to, String subject) {
    	// 메일 보내는 로직
    }
}

@Data
public class EmailDto {
	private String to;
    private String subject;
}

 

3. 메인 스레드를 사용하지 않는다? 서브 스레드를 만들어줘야한다!

앞서 @Async가 붙은 메서드를 프록시가 가로채서 서브 스레드에 할당해준다고 했는데 그렇다는 의미는 우리가 서브스레드를 직접 만들어야 한다는 의미입니다. 

 

package com.hello.capston.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
@Slf4j
public class AsyncConfiguration implements AsyncConfigurer {

    @Override
    @Bean(name = "mailExecutor")
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(5);
        executor.setQueueCapacity(10);
        executor.setThreadNamePrefix("MailExecutor-");
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> {
            log.error("Exception handler for async method : ", method.toGenericString(), " threw unexpected exception itself", ex);
        };
    }
}

 

그런다음 @Async 어노테이션의 속성에 우리가 만든 Bean 이름을 집어넣으면 됩니다. (ex. @Async("mailExecutor"))

 

 

 

마치며

이렇게 @Async에 대해서 간단하게 알아보고 주의해야할 점에 대해서 알아봤습니다. 생각보다 쉽지만 막 사용하면 원하는 결과를 얻을 수 없다는 것을 깨달았습니다. 

 

동기, 비동기 프로그래밍 방식을 아는 것은 굉장히 중요해보입니다. 동기처리를 하냐 비동기처리를 하냐의 차이 때문에 고객경험이 부정적이냐 긍정적이냐를 가를 수 있으니까요. 

 

해당 포스팅에선 어떻게 SMTP를 비동기로 처리하냐에 대한 자세한 내용은 기술하지 않았습니다. 전반적인 @Async의 내용 그리고 주의해야할 점만을 기술했습니다. 

 

기회가 된다면 SMTP를 비동기로 처리하는 것에 대해서 포스팅하도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~