개발놀이터
@Async 어노테이션 본문
꽤 전에 동기 비동기 프로그래밍에 대해서 이론적으로 학습하고 최근에 실습할 기회가 생겼습니다.
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를 비동기로 처리하는 것에 대해서 포스팅하도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
'Spring > Spring' 카테고리의 다른 글
스프링 부트 3.0 마이그레이션 가이드 (0) | 2023.06.02 |
---|---|
스프링에서 동시성 문제 해결하기 (0) | 2023.05.21 |
Spring에서 예외를 관리하는 방법 (@ControllerAdvice, @ExceptionHandler) (0) | 2022.08.04 |
트랜잭션과 @Transactional (0) | 2022.06.21 |
커넥션 풀 (Connection pool) (0) | 2022.06.21 |