작성하신 체크 포인트는 기술적으로 모두 옳습니다. 여기에 면접이나 토론 시 답변의 해상도를 높이기 위해 개념 간의 연결고리를 조금 더 정밀하게 다듬어 보았습니다.
@Transactional은 내부적으로 ThreadLocal을 사용하는 TransactionSynchronizationManager에 의해 트랜잭션 상태를 관리합니다. @Async가 붙은 메서드는 Spring의 TaskExecutor에 의해 완전히 다른 스레드에서 실행되므로, 기존 스레드의 ThreadLocal에 묶여 있던 트랜잭션 컨텍스트는 비동기 스레드로 전파되지 않습니다. 즉, 두 메서드가 각각 @Transactional을 가지고 있더라도 서로 완전히 독립된 트랜잭션(및 커넥션)으로 동작합니다.@Transactional 메서드가 @Async 메서드를 호출하거나 그 반대의 경우, Spring AOP의 프록시 메커니즘을 거치지 않고 this를 통한 직접 호출이 일어나므로 어노테이션이 아예 무시됩니다. 이 점을 명확히 짚어주신 것은 아주 좋습니다.이론을 넘어 실무에서 이 문제를 해결할 때 가장 자주 쓰이는 핵심 패턴을 보완해 드립니다.
비동기 작업의 유실 가능성과 트랜잭션 보장:
메인 트랜잭션은 성공했는데 비동기 작업(예: 알림 발송, 타사 API 호출) 도중 예외가 발생하면 어떻게 할 것인가? 실무에서는 메인 비동기 로직을 단순히 @Async로 호출하기보다, @TransactionalEventListener를 사용하는 것이 가장 안전한 Best Practice 중 하나입니다.
TransactionPhase.AFTER_COMMIT 활용:
비동기 작업은 대개 'DB 작업이 완전히 성공한 후' 수행되어야 합니다. 그렇지 않으면 메인 로직은 롤백되었는데 메일은 발송되어 버리는 데이터 불일치가 발생합니다. 이를 위해 스프링이 제공하는 이벤트 리스너의 AFTER_COMMIT 옵션을 사용하는 구조를 체크 포인트에 추가하면 완성도가 훨씬 높아집니다.
단순히 @Async와 @Transactional을 분리하는 것 외에, 시스템 아키텍처 관점에서 선택할 수 있는 대안들의 장단점을 비교해 보았습니다. 스터디원들과 이 표를 바탕으로 토론해 보시면 좋은 인사이트를 얻으실 수 있을 것입니다.
| 대안 | 장점 | 단점 | 적합한 시나리오 |
|---|---|---|---|
1. 별도 Bean 분리 후 @Async 호출 |
구현이 매우 간단하고 스프링 기본 기능만으로 해결 가능. | 애플리케이션 다운 시 비동기 작업 유실 위험, 트랜잭션 롤백 시 정합성 깨짐 위험. | 유실되어도 비즈니스에 치명적이지 않은 가벼운 로직 (예: 단순 로그성 데이터 쌓기) |
2. Spring Event + @TransactionalEventListener |
도메인 간 결합도를 낮추고, 메인 트랜잭션 성공 시에만 비동기 작업 실행을 보장. | 기본적으로는 인메모리 이벤트 시스템이므로, 비동기 스레드 풀 오버플로우나 서버 다운 시 이벤트 유실 가능성 존재. | 알림 발송, 단일 애플리케이션 내의 가벼운 정합성이 필요한 비동기 로직 |
| 3. Transactional Outbox Pattern + MQ | 로컬 트랜잭션과 비동기 메시지 발행의 원자성(Atomicity) 보장. 서버가 죽어도 재처리 가능 (최소 1회 전송 보장). | 인프라 복잡도 증가 (Kafka/RabbitMQ 등 필요), Outbox 테이블 조회 및 발행을 위한 폴링/CDC 비용 발생. | 결제, 주문 등 메시지 유실이 절대 용납되지 않는 핵심 비즈니스 및 MSA 환경 |
작성해주신 꼬리 질문들도 훌륭하지만, 스터디의 깊이를 한 단계 더 끌어올리기 위해 아래 질문들을 추가로 던져보시는 것을 추천합니다.
@Async 메서드의 리턴 타입이 CompletableFuture라면, 메인 스레드에서 이 미래 결과를 조합할 때 메인 트랜잭션의 커넥션 유지 시간에 어떤 영향을 미칠까요? (Connection Starvation 관점에서)"@Async는 기본적으로 ThreadPoolTaskExecutor를 사용하는데, 만약 설정을 세부적으로 하지 않고 기본 설정을 사용할 경우 어떤 장애가 발생할 수 있을까요? (Queue Capacity와 Max Pool Size의 메커니즘 관점에서)"@TransactionalEventListener를 사용할 때, 리스너 메서드 내부에서 또다시 DB를 수정(Insert/Update)해야 한다면 어떤 점을 주의해야 할까요? (힌트: 이미 커밋된 커넥션의 상태)"질문 세트를 보니 이미 아키텍처와 프레임워크의 동작 원리에 대해 깊은 고민을 하고 계신 게 느껴집니다. 혹시 이번 스터디에서 구현 편의성(1, 2번 대안)과 데이터의 절대적 안정성(3번 대안) 중 어떤 가치에 더 무게를 두고 토론을 진행할 계획이신가요?