일단, @Async가 뭘까?
@Async는
Spring에서 제공하며, Thread Pool을 활용하여
비동기를 지원하는 어노테이션이다.
@Async가 제대로 동작하지 않는 상황이 발생했다!
이번에도 어김없이 등장하신 개발실 실장님 🙌
최근 누군가가 개발한 코드 중에서
@Async 어노테이션을 붙인 코드가 제대로 동작하지 않아서
비동기 처리가 되고 있지 않은데
원인이 무엇인지 확인해 보라고 하셨다.
회사코드라 그대로 옮길 수 없지만,
대략 큰 뼈대 내용은 다음과 같다.
public class BenzService extends CarService{
public void init(CarDto carDto) {
// 생략 ..
callExternalApi(carDto, apikey);
}
@Async // 이 부분 주목!
public void callExternalApi(CarDto cardto, String apiKey) {
// 생략 ..
}
}
A사(자사 서비스)에서 B,C,D...제휴사로 API를 호출하는 상황이고,
이때 BenzService, BmwService, FerrariService 등
여러 service에서 callExternalApi() 메소드를 '비동기' 로 호출하고자 한다.
(서비스는 총 10개라고 가정)
각 서비스에서 callExternalApi() 메소드 호출 시
Car 테이블에 DB 컬럼을 INSERT 한다.
각 제휴사에 API를 호출하는 순간 DB에 INSERT를 하는 것이며,
이 때 각 제휴사로 호출한 API에 대해 고유한 관리번호가 생기게 된다.
그리고 이 관리번호를 제휴사에 함께 전달한다.
각 제휴사는 A사(자사 서비스)로부터 API를 호출받은 뒤,
내부처리를 통해 다시 A사(자사 서비스)로 고유 관리번호에 해당하는 차의 상태값을 응답한다.
A사(자사 서비스)는 각 제휴사로부터 API를 통해 응답받은 차의 상태값을
위에서 INSERT 한 Car 테이블에서
고유한 관리번호로 컬럼을 찾아 UPDATE 시킨다.
그런데 위 이미지처럼 의도 했던 '비동기'가 제대로 동작하지 않으면 어떻게 될까?
위 코드에서 분명 callExternalApi() 메소드 위에 @Async 어노테이션을 붙였기 때문에
비동기로 동작할 것이라 생각했지만...
각 제휴사로 API를 보내고 처리하는 속도가 5-6초 이상 걸렸으며,
이렇게 특정 API가 시간을 오래 잡고 있으니
DB 커넥션 풀이 모자라는 현상이 나타나 간헐적인 에러가 발생하는 것을 통해
결론적으로 비동기가 제대로 동작하지 않았음을 예측해 볼 수 있었다.
(1) 비동기가 아니기 때문에
각 제휴사로 보내는 API에 대한 DB INSERT 10개가 하나의 커넥션풀로 묶이게 되고
(2) 간혹 Car 테이블에 다 INSERT 되기도 전에
어떤 제휴사로부터 API 응답이 빠르게 오는 경우
(3) 아직 INSERT 중이라서 바로 DB UPDATE를 하지 못하고
대기를 해야 하는 상황이 간헐적으로 발생한 것이었다.
지금은 서비스가 10개라고 예시를 들었지만
만약 서비스가 20개, 30개가 된다면?
더 큰 문제가 발생하기에 반드시 수정이 되어야 하는 상황이었다.
왜 @Async가 제대로 동작하지 않았을까?
결론을 먼저 말하면
비동기가 제대로 동작하게 하려면 아래와 같이 수정되어야 한다.
그 이유는, self-invocation(자가 호출)해서는 안 되기 때문이다.
👉 같은 클래스 내부의 메소드를 호출하는 것은 동작하지 않는다.
아래를 홈페이지를 살펴보면 다음과 같다.
https://dzone.com/articles/effective-advice-on-spring-async-part-1
정리하면 @Async는
- 프로시 객체를 생성하며
- AOP가 적용되어 Spring context에 등록되어 있는 빈 객체의 메소드가 호출되었을 때 스프링이 확인할 수 있으며
@Async가 적용되어 있다면 스프링이 메소드를 가로채서
다른 스레드(풀)에서 실행시켜준다.
이때 @Async가 제대로 동작하기 위해서 몇 가지 졔약 조건이 있다.
- 같은 클래스 내부의 메소드를 호출하는 것은 동작하지 않는다. (자가 호출 👉 Nope.)
- public 메소드에만 적용된다. (private 👉 Nope.)
왜 이런 제약 조건이 있는지는 위 내용에서 확인할 수 있다.
자가호출이 불가한 이유는
Spring context에 등록된 빈의 메소드 호출이어야 프록시를 적용받을 수 있어서
내부 메소드 호출은 프록시 영향을 받지 않기 때문이다.
아래 코드를 보면
init() 메소드 내부에서 callExternalApi() 메소드를 호출하면 👉 프록시를 타지 않는다.
내부호출이기 때문이다.
그렇다면 public으로 선언되어야 하는 이유는 무엇일까?
위에서 스프링이 @Async가 적용된 메소드를 가로챈다고 했는데
가로챈 후 다른 클래스에서 호출이 가능해야 하므로
private method는 사용할 수 없는 것이다.
어떻게 하면 처음부터 버그를 빨리 알아챌 수 있었을까?
(1) @Async를 사용할 때는
혹시 자가 호출은 아닌지, 같은 클래스에서 메소드를 호출하고 있는 것은 아닌지 확인했었어야 했다.
이는 @Async가 내부적으로 어떻게 돌아가는지
프록시 객체가 어떻게 동작하는지 알고 있었어야 한다는 의미이기도 하겠다.
(그냥 선언만 한다고 해서 다가 아니라는....!)
(2) 비동기로 실행하고자 하는 메소드를 호출해 보고
로그를 확인하면서 Thread Name을 확인했었어야 했다.
예를 들어 비동기라면 Thread Name이 [Task-01], [Task-02], [Task-03].... 이렇게 분리되어야 했을 텐데
동기라면 [Task-00]라는 하나의 Thread Name 으로 코드가 계속 진행됐었을 것이다.
'Programming > Java & Spring 관련 내용 정리' 카테고리의 다른 글
[Java] 인터페이스 사용하여 필터링 기능을 구현해 보았다. (0) | 2023.08.11 |
---|---|
[Spring] 멀티 쓰레드 환경에서 자원이 공유돼 버렸다. (0) | 2023.05.31 |
[Spring] Thread-safe 에 대해서 고찰해 보다. (0) | 2023.03.20 |
[코드리뷰 받은 내용 정리] 코드를 작성할 때 '이것'을 생각해보자! (0) | 2023.02.13 |
[Spring] 멀티모듈 구성 방법 (0) | 2022.10.28 |