회원가입 시 사용하는 이메일으로 인증번호를 보내는데 요청 과정이 굉장히 오래 걸렸습니다. 처음에는 정말 별 생각 없었습니다. 그런갑다. Front-end 친구가 🤸♀️ "이메일 요청은 좀 오래 걸리더라고" 할 때도 내가 코드를 더럽게 짰나?까지만 생각했습니다. 근데 여기서 더 줄일 코드가 있나, 싶단말이에요. 그래서 그냥, 그냥 그런갑다.
그런갑다
느린 이유를 생각해봤습니다. 이메일 요청을 하고 그게 완료가 되면 응답이 가겠죠. 다른 요청들과 마찬가지로 말이에요. 근데 이메일은 발송 시간이 있습니다. 그래서 더 오래 걸리고, 발송이 된 후에야 응답이 가기때문에 요청이 느릴 수밖에 없는 것입니다.
그런갑다
동기 비동기라는 걸 아시나요?
동기와 비동기는 데이터를 받는 방식입니다.
동기: synchronous (동시에 일어나는)
비동기: Asynchronous (동시에 일어나지 않는)
동기는 요청과 결과가 동시에 일어납니다. 요청을 하면 그 자리에서 결과가 바로 주어져야 합니다. 비동기는 요청과 결과가 동시에 일어나지 않습니다. 간단히 하자면 동기는 요청이 오면 실행 후 응답하고, 비동기는 OK👌~ 응답을 보낸 후 요청을 실행하는 것이라고 할 수 있습니다. 202 Status Code가 비동기의 예시가 될 수 있습니다.
202 ACCEPTED
: 클라이언트의 요청은 정상적이나, 서버가 아직 요청을 완료하지 못했다.
클라이언트의 요청은 정상적이나 작업이 오래걸리니 나중에 알려주겠다는 뜻입니다. 바로 비동기 작업인 것이지요. 😮😮❕❕❗ 비동기 방식은 동기보다 복잡하지만, 결과가 주어지는 데 걸리는 시간에 다른 작업을 할 수 있으므로 자원을 효율적으로 사용할 수 있다는 장점이 있습니다.
앞에서 이메일은 요청이 느릴 수밖에 없다, 라고 말했습니다. 발송 시간때문에요. 그렇다면 여기서 비동기 처리를 해야하는 것 아니겠어요❔❔❔
그리고 그 때 스쳐지나갑니다. 타 사이트에서 회원가입 시 이메일 인증 요청을 클릭하면 뜨는 창.
메일이 발송되었습니다. n분 뒤에도 안 오면 재전송을 눌러 주세요.
메일이 발송되었습니다. 비동기 처리를 할 건데 비동기 처리가 되는 시간이 넘어도 메일이 안오면 다시 요청 주세요.
Spring Boot에서 비동기 처리
그렇다면 Spring Boot에서 비동기 처리를 쉽게 하는 방법을 알아보고, 그것의 동작 원리를 알아봅시다.
@EnableAsync 애노테이션
@Configuration 애노테이션과 함께 사용하면 전체 Spring context(개념적으로 Bean들이 담겨있는 공간이라고 할 수 있습니다.)에 대해 애노테이션 중심 비동기 처리를 활성화 합니다. @EnableAsync는 여러 TaskExecutor 구현체 중, SimpleAsyncTaskexecutor을 사용합니다.
- TaskExecutor는 무엇인가요?
Task는 독립적으로 실행 가능한 작업을 말합니다. 스프링은 이런 작업들을 다양하게 실행할 수 있도록 TaskExcutor 인터페이스를 제공합니다.
다양한 TaskExecutor 구현체가 있고, @EmableAsync는 기본적으로 SimpleAsyncTaskExecutor이라는 구현체를 사용한다는 말입니다.
- SimpleAsyncTaskExecutor
SimpleAsyncTaskExecutor은 각 작업에 대해 새 스레들을 실행하는 방식입니다. (요청이 올 때마다 스레드 생성.) This implementation does not reuse threads. (스레드를 재사용하지 않습니다.)
TaskExecutor은 getAsyncExecutor()을 오버라이드함으로써 교체해줄 수 있습니다. docs에서는 AsyncConfigurer을 상속받아 오버라이드 하는 방법을 사용하고 있습니다.
@Configuration
@EnableAsync
public class ExecutorConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(7);
executor.setMaxPoolSize(42);
executor.setQueueCapacity(11);
executor.setThreadNamePrefix("MyExecutor-");
executor.initialize();
return executor;
}
...
AsyncConfigurer을 상속받지 않고, 다른 TaskExecutor을 Bean으로 등록해주는 방법도 가능합니다. 또는 복수의 TaskExecutor을 @Bean으로 등록한 뒤, 메서드 레벨에서 원하는 TaskExecutor를 선택할 수도 있습니다. @Async("threadPoolTaskExecutor")
@Configuration
@EnableAsync
public class ExecutorConfig {
@Bean(name = "threadPoolTaskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(7);
executor.setMaxPoolSize(42);
executor.setQueueCapacity(11);
executor.setThreadNamePrefix("MyExecutor-");
executor.initialize();
return executor;
}
}
Executor 타입의 Bean이 하나만 등록되어 있으면 해당 Executor로 실행합니다. 여러 개의 Bean을 등록하였는데 @Async에서 Bean을 지정해주지 않으면 SimpleAsyncTaskExecutor가 사용됩니다.
다양한 테스트를하신 분의 글을 참고했습니다. (AsyncCconfigruer구현하고 또다른 Executor을 Bean으로 등록한 경우 등의 테스트를 하셨습니다. 😮)
CorePoolSize: 기본 실행 대기하는 Thread의 수
MaxPoolSize: 동시 동작하는 최대 Thread의 수
QueueCapacity: MaxPoolSize 초과 요청 시 해당 요청을 Queue에 저장하는데, 이 때 최대 수용 가능한 Queue의 수 (초과 요청은 Queue에 저장되어 있다가 Thread에 자리가 생기면 하나씩 빠져나가 동작한다.)
ThreadNamePrefix: 생성되는 Trhead의 접두사 지정
만약 SimpleAsyncTaskExecutor을 그대로 사용하고 싶은 경우 앞에서 말했듯이 @Configuration과 @EnableAsync만 함께 사용해 주면 됩니다.
@Configuration
@EnableAsync
public class ExecutorConfig {
}
* 위에서 예시를 든 TreadPoolTaskExecutor말고도 다른 TaskExecutor들이 있으며, 그들을 사용해도 됩니다.
- TreadPoolTaskExecutor: Thread Pool이 무엇일까?
Thread Pool. Thread Pool은 간단하게 말하면 스레드를 미리 만들어 놓은 하나의 풀장이라고 할 수 있는데요. 그렇다면 이런 풀장은 왜 필요할까요? 그냥 그때그때 만들어서 쓰면 안 될까요?
스레드가 생성될 때 OS는 메모리공간을 확보해주고 그 메모리를 스레드에 할당해 줍니다. 스레드를 생성하고 확보하는 데 드는 비용을 무시할 수는 없기에 요청이 들어올 때마다 스레드를 생성한다면 프로그램에 영향을 줄 수 있죠. 따라서 Thread Pool에 스레드를 미리 만들어 놓는 것입니다.
SimpleAsyncTaskExecutor은 요청이 될 때 스레드를 생성, ThreadPoolExecutor은 미리 지정한 만큼의 스레드를 생성한다,라고 보시면 되겠습니다.
@Async 애노테이션
- 여긴 아직 정리안 했어욯
메소드 위에 @Async 어노테이션만 추가하면 method는 비동기로 처리됩니다.
@Async
@Async는 비동기적으로 처리할 수 있게끔 스프링에서 제공하는 애노테이션입니다. 해당 애노테이션을 붙이게 되면 각기 다른 스레드로 실행이 됩니다. 호출자는 해당 메서드가 완료되는 것을 기다릴 필요가 없지요. 이 애노테이션을 사용하기 위해선 @EnableAsync가 달려있는 configuration 클래스가 우선으로 필요합니다.
@EnableAsyncs는 스프링의 @Async 애노테이션과 EJB 3.1 java.ejb.Asynchronous를 감지한다고합니다.
@Async
@Async
는 두가지 제약 사항이 있습니다.
1. public 메서드에만 적용야 한다.
-> 메서드가 public이어야 프록시가 될 수 있기 때문이다.
2. 같은 클래스 안에서 @Async
메서드를 호출할 경우는 작동하지 않는다.
-> 프록시를 우회하고 해당 메서드를 직접 호출하기 때문이다.
스프링 부트에서 `@Async`를 사용하기 위해서는 `@EnableAsync` 어노테이션을 먼저 선언해야 합니다. @Async는 AOP에 의해 동작하는데,
// 왜 AOP로 동작하나여
@Async 어노테이션이 선언된 메서드는 비동기 메서드로 동작하게 됩니다.
@A
- public 메서드에만 적용해야한다.
: 메소드가 public이어야 프록시가 될 수 있다. - 같은 클래스 안에서 async 메소드를 호출하면 작동하지 않는다.
: 프록시를 우회하고 해당 메소드를 직접 호출하기때문에 작동하지 않는다.
리턴타입이 없는 메소드는 비
https://brunch.co.kr/@springboot/267#comment 자세하게설명해놓음지금은구현이잘되
비동기는 결과를 완성하기 전에 일단 반환을 한다. 그럼 클라이언트는 결과값을 어떻게 받을까?
메서드를 호출한 이후 어느정도 시간이 지난 후 다시 결과를 조회하는 게 첫 번째 방법이다. 메서드를 호출한 이후 클라이언트는 다른 작업을 수행할 수 있다. (Non-Blocking) 그렇게 수행하다가 메서드의 결과를 알고싶을때 다른 메서드르 호출한다. 그럼 클라이언트 입장에서는 non-blocking 상황이 아니다. 데이터를 조회하는 순간에는 블록킹으로 동작한다.
이를 해결하기 위해서는 콜백함수를 구현해야한다. 메서드를 제공하는 곳에서 결과가 완성되면 클라이언트로 결과가 나왔다고 알려주는 방법이다.
> private 메서드는 @Async를 적용해도 비동기로 동작하지않는다.
> Sync(동기), Async(비동기)
스프링부트에서 @Async를 사용하기 위해서는 @EnableAsync 어노테이션을 먼저 선언해야 한다. @Async는 AOP에 의해 동작하는데, @Async 어노테이션이 선언된 메서드는 비동기 메서드로 동작하게된다.
@Async로 선언된 메서드는 리턴 타입에 따라 내부적으로 상이하게 동작한다.
- void
return 결과가 없는 경우
https://brunch.co.kr/@springboot/401
인터페이스가 있더라도 cglib의 프록시 객체가 주입된다. 각각의 용도에 맞게 잘 사용하면 되겠다.
이렇게 오늘은 Spring의 AOP 매커니즘에 대해서 알아봤다!
인터페이스가 있으면 주입이 안되나바?
http://wonwoo.ml/index.php/post/1576
왠지오류남.
```
The bean 'userServiceImpl' could not be injected as a 'com.dsm.MelanEtumos.impl.UserServiceImpl' because it is a JDK dynamic proxy that implements:
com.dsm.MelanEtumos.service.UserService
Action:
Consider injecting the bean as one of its interfaces or forcing the use of CGLib-based proxies by setting proxyTargetClass=true on @EnableAsync and/or @EnableCaching.
Process finished with exit code 1
available: expected at least 1 bean which qualifies as autowire candidate. <- @ENtity
```
비동기이지만 예외 처리는 해야겠습니다
맞죠. 제가 지금 메일 발송을 하고 있습니다. 그런데 주용한 건 client에 예외가 안 가요. 웅앵.가야하자나요~~
그 메서드의 리턴타입에 따른...예압. Future일 경우에는 예외처리가 쉽다고 하네여. Future.get()메서드를 한 번 찾아보시길 권합니다.
그
런
데!
return 타입이 void일때는요????
우리는 AsyncUncaughtExceptionHandler인터페이스를 구현해야합니다. @Overrid한 handlerUncaughtException(...)은 잡하지 않은 비동기 예외가 발생할때호출됩니다.
공통 Exception Handler가 AsyncUncaughtExceptionHandler을 구현하게 해 비동기 예외 상황에 대한 처리를 할 수 있습니다.
@Slf4j
@RestControllerAdvice
public class ExceptionAdvice implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable e, Method method, Object... params) {
log.error("Method name : {}, param Count : {}\n\nException Cause -{}", method.getName(), params.length, ex.getMessage());
}
...
}
AsyncConfigurer인터페이스를 구현한 클래스에서 getAsyncUncaughtExceptionHandler()도 오버라이드 해줘야합니다.
이렇게되는데요
제가원한건 HTTP METHOD로 CLINET에응답이ㄱ가는거엿는데
역시 그건불가능한것같습니당... 중복된 이메일 체크를 여기서도 하고 싶었는데ㅜ 지금은 그 코드를 삭제를 했는데
음 생각해보니까 다시 해야할지도요 코드는 일단 안가잖아요
음
음
네 다시 추가하겠ㅅ브니다
그럼 코드가 가지않으면 올바른 이메일인지, 이미 가입된 이메일인지 확인해주세요 라고 해야겟네요 ㅠ
R
근데그런건 알려줘야하는거아닌가?ㅠ 라는 혼란이생겼지만 추후 알게된다면 추가하도록 하겠습니다
읽어주셔서 감사합니다
읽어주셔서 감사합니다.
피드백 해 주시면 감사하겠습니다.
댓글