📌 스프링 @Async 에 대해서 알아봅시다. 약간의 AOP 배경지식도 필요합니다.
Spring AOP
스프링이 비동기 작업을 수행하기 위해선
기본적으로 런타임에서 해당 클래스에 대한 프록시가 필요하다.
용어
- Aspect: 여러 클래스를 cut across 하는 관심사의 모듈화
- 예를 들면, Transactional 어노테이션을 생각하면 됨
- Joinpoint: 프로그램 실행 시점, 메소드의 진입지점이라고 생각하면 됨
- Advice: 특정 join point 에서 aspect 에 의해 취해진 액션
- 다양한 advice type 이 있음, 가령 "around", "before", "after" ...
- 스프링 포함 많은 AOP 프레임웤에서 Advice 를 인터셉터로 모델링하고, 인터셉터 체인을 관리한다.
- Pointcut: Joint point 의 정규 표현식, Join point 가 Pointcut에 일치할때마다 해당 Pointcut에 관련된 Advice가 실행된다.
자세히 알아보기:
@Async
어노테이션
Executor 를 커스텀으로 빈으로 등록하지 않았으면,
Spring 이 자동으로 ThreadPoolTaskExecutor 를 기본값과 함께 컨테이너에 빈을 등록한다ThreadPoolTaskExecutor 은 @EnableAsync 와 Spring MVC 비동기 요청 프로세스를 동작하도록 해준다.
요약
- 호출되어진 스레드의 로직은 호출한 스레드에 영향을 안준다.
- Simply put, annotating a method of a bean with @Async will make it execute in a separate thread. In other words, the caller will not wait for the completion of the called method. - https://www.baeldung.com/spring-async
- 메소드를 비동기 실행을 할 수 있도록 마크한다.
- type 레벨에도 사용할 수 있다.
- 이 경우엔 모든 메소드가 async 로 동작한다
@Configuration
이 붙은 어노테이션 밑에 @EnableAsync
를 반드시 넣어야한다.
@Async
메소드 시그니쳐에는 어떤 파라미터 타입이 들어와도 상관없다.
- 리턴타입은 void 혹은
Futrue
이어야한다.
- Future
- CompletableFuture 같은 것을 사용하면 비동기된 메소드를 기다려 리턴값을 받을 수 있다.
더 알아보기:
실습하면서 얻은 정보:
- private 메소드이면 안된다.
- why?
- public 이어야 프록시생성 할 수 있기 때문이다
- 동일 클래스에서 호출하면 안된다
- why?
- self-invocation doesn't work because it bypasses the proxy and calls the underlying method directly.
- 클래스 내부에 Proxy 기반 동작하는 어노테이션 (@Transactional, @Async, @Cacheable, 커스텀어노테이션 등) 메소드가 있으면, 일반 bean 이 아니라 Proxy 로 래핑되서 bean 이 생성된다.
- Spring Context 가 Bean 후보들을 스캔하여 Bean 으로 생성할때 래핑하냐 마냐를 결정한다. 빈이 생성되는 과정에서 AbstractAutoProxyCreator.wrapIfNecessary() 함수가 호출되는데 이름에서 알 수 있듯이 필요한 경우에 Bean 으로 만들고자 하는 클래스를 Proxy 로 Wrap 하는 역할을 수행하는 함수이다.
트러블 슈팅
아래 순서대로 호출했는데 @Async
가 동작하지 않음
1. Controller 에서 서비스(bean) 객체 메소드 호출
2. 내부 private 메소드 호출
3. 다른 서비스(bean) `@Async` 메소드 호출
- 위 순서를 테스트하면 정상적으로 async 가 동작하지만 실무에서 동작하지 않는 문제가 있었음.
@Async
가 실행 되지 않는 원인은 해당 메소드의 클래스의 프록시 객체가 만들어지지 않았기 때문이다.
- 해당 클래스는 서비스 클래스이기 때문에 어찌됐던 간에 프록시를 만들어 사용하면 해결되었다.
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
- 위 어노테이션을 붙이면 CGLIB 프록시 객체가 만들어진다.
링크에 대한 요약도 적어주셔서 읽기 좋았습니다. 좋은 글 감사합니다 Kyu !