@Async를 붙였는데 비동기로 동작하지 않는 코드를 발견했다.
원인을 찾아보니 두 가지 문제가 있었다.
🔍 상황
자사 서비스에서 여러 제휴사로 API를 호출하는 구조다.
BenzService, BmwService, FerrariService 등 제휴사별 서비스가 있고,
각 서비스에서 callExternalApi()를 호출할 때 비동기로 실행하고자 했다.
문제의 코드
@Async를 붙였지만, 10개 제휴사 호출이 순차적으로 실행되고 있었다.
같은 요청 흐름에서 순차 실행되니 처리 시간이 길어지고,
트랜잭션 범위가 넓게 잡혀 있으면 커넥션 점유 시간도 길어질 수 있었다.
💥 원인 1: Self-invocation — 프록시를 타지 않는다
@Async는 Spring AOP 기반으로, 프록시 객체를 통해 동작한다.
init() 안에서 callExternalApi()를 호출하면 같은 객체 내부 호출이라 프록시를 거치지 않는다.
@Async가 붙어있어도 실제로는 동기로 실행된다.
@Transactional, @Cacheable 등 프록시 기반 AOP 어노테이션은 전부 같은 제약이 있다.같은 클래스 내부 호출은 프록시를 거치지 않으므로 어노테이션이 무시된다.
@Async가 동작하기 위한 조건
💥 원인 2: 트랜잭션 커밋 전에 콜백이 올 수 있다
self-invocation을 고쳐서 비동기로 분리하더라도, 또 다른 문제가 있었다.
@Transactional이 걸려 있으면 INSERT는 했지만 commit 전이라
다른 트랜잭션(콜백 처리)에서 해당 row가 보이지 않을 수 있다.
DB INSERT 커밋 시점과 외부 콜백 도착 시점의 race condition이다.
✅ 해결: 별도 빈으로 분리 + 트랜잭션 경계 분리
1단계: @Async 메서드를 별도 빈으로 분리
2단계: 트랜잭션 커밋 후 비동기 호출 (race condition 방지)
관리번호 INSERT가 커밋된 후에 외부 API를 호출해야 콜백 race를 막을 수 있다.
@TransactionalEventListener를 사용하면 커밋 이후 시점에 비동기 호출을 걸 수 있다.
@Async는 어노테이션만 붙이면 끝이 아니다.설정 클래스에
@EnableAsync가 있어야 활성화된다.
🔧 비동기 동작 확인하는 방법
로그에 Thread.currentThread().getName()을 찍어 확인할 수 있다.
[http-nio-8080-exec-1] Benz API 호출[http-nio-8080-exec-1] Bmw API 호출[http-nio-8080-exec-1] Ferrari API 호출[async-pool-1] Benz API 호출[async-pool-2] Bmw API 호출[async-pool-3] Ferrari API 호출
스레드 이름은 설정한 Executor의 threadNamePrefix에 따라 달라진다.
📝 정리
문제는 두 가지였다.
@Async는 어노테이션만 붙이면 끝이 아니다.프록시 기반이므로 self-invocation은 불가하고,
외부 시스템 호출이라면 트랜잭션 커밋 시점까지 고려해야 한다.
'Programming > Java & Spring 관련 내용 정리' 카테고리의 다른 글
| [Java] 인터페이스를 통해 if문의 향연을 고쳐보았다. (0) | 2023.09.11 |
|---|---|
| [Spring] 멀티 쓰레드 환경에서 자원이 공유돼 버렸다. (0) | 2023.05.31 |
| [Spring] Thread-safe 에 대해서 고찰해 보다. (0) | 2023.03.20 |
| [Spring] 멀티모듈 구성 방법 (0) | 2022.10.28 |
| [Spring Batch] 우리 정산 시스템은 오류를 어떻게 처리하고 있을까 (0) | 2022.10.28 |