트랜잭션과 이벤트 발행
Spring의 스레드 기반 트랜잭션을 기준으로 설명한다. 트랜잭션 이벤트 기능은 Spring 4.2부터 제공된다. 리액티브 트랜잭션은 컨텍스트 전달 방식이 다르므로 별도 검토가 필요하다.
정정: 이전 페이지의 “publishEvent는 항상 큐에 등록만 한다”와 “fallbackExecution은 필수”라는 설명은 조건을 빠뜨렸다. 아래에서 리스너별 실행 시점과 트랜잭션 없는 호출을 구분한다. 관련 설명도 같은 기준으로 갱신했다.
발행 API가 실행 시점을 정하지 않는다
publishEvent()는 이벤트를 멀티캐스터에 전달한다. 호출만으로 동기·비동기·커밋 이후 실행을 결정하지 않는다. 기본 동기 멀티캐스터의 일반 리스너는 호출 스레드에서 실행되고, 비동기 구성은 별도 작업을 제출할 수 있다. ApplicationEventPublisher API
| 구성 | 실행 조건 | 주의점 |
|---|---|---|
| 일반 EventListener | 멀티캐스터 설정에 따라 실행 | 기본 구성에서는 커밋 전에도 실행 |
| EventListener + Async | 실행기에 작업 제출 | 커밋을 기다리는 조건은 없음 |
| TransactionalEventListener(AFTER_COMMIT) | 발행한 트랜잭션의 성공적인 종료 이후 | 메모리 기반 후처리, 영속 전송 기록 아님 |
일반 리스너는 트랜잭션 내부의 검증·동일 트랜잭션 DB 처리에도 사용할 수 있다. 외부 통지와 같은 요구에 커밋 이후 처리가 필요하다는 뜻이지, 일반 리스너 자체가 잘못된 것은 아니다.
이벤트는 트랜잭션 안에서 발행한다
public record JobCompleted(long jobId) {}
@Transactional
public void complete(long jobId) {
jobRepository.complete(jobId);
publisher.publishEvent(new JobCompleted(jobId));
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onCompleted(JobCompleted event) {
notificationClient.send(event.jobId());
}범용 예제다. 다른 일반 리스너가 같은 이벤트를 구독하면 그 리스너는 별도의 규칙으로 실행된다.
| phase | 실행 시점 | 실패 의미 |
|---|---|---|
| BEFORE_COMMIT | 커밋을 준비하는 단계 | 이후 커밋 실패·롤백 가능 |
| AFTER_COMMIT | 커밋 성공 뒤 | 원래 데이터를 되돌리는 단계가 아님 |
| AFTER_ROLLBACK | 롤백 뒤 | 실패한 작업의 후처리 |
| AFTER_COMPLETION | 커밋 또는 롤백 뒤 | 결과별 정책 구분 필요 |
AFTER_COMMIT은 외부 메시지 전달 성공이나 장애 복구까지 보장하지 않는다. Spring 트랜잭션 이벤트
fallback은 호출 계약을 바꾼다
기본 fallbackExecution=false에서 트랜잭션 없는 발행은 처리하지 않는다. true로 바꾸면 해당 분기도 실행하지만, 그 분기에 기다릴 커밋은 없다. 리스너에 Async가 있다면 실행기 구성도 적용된다.
수동 커밋 호출 뒤에 이벤트를 발행하는 경로는 발행을 커밋 앞으로 옮길 수 있는지 먼저 확인한다. 트랜잭션 밖 호출도 의도적으로 지원할 때만 fallback을 선택한다. 누락된 트랜잭션을 숨기는 기본 처방으로 쓰지 않는다.
이벤트에 엔티티를 담을 때 생기는 결합
불변 식별자와 필요한 값을 이벤트에 담으면 엔티티 변경·지연 로딩·다른 스레드의 영속성 컨텍스트 의존을 줄인다. 리스너에서 다시 조회한다면 최신 상태가 필요한지, 발행 당시 상태가 필요한지 먼저 정한다. record가 영속화나 깊은 불변성을 자동으로 제공하는 것은 아니다.
검증할 동작
- 커밋 전에는 외부 호출이 없다.
- 커밋 후에는 다른 DB 연결에서 필요한 데이터가 보인다.
- 롤백·트랜잭션 없는 발행은 선택한 계약대로 동작한다.
- 선언적·수동 트랜잭션의 공유 리스너가 같은 기준을 만족한다.
위 테스트는 쓰기 트랜잭션의 캐시를 재사용하지 않도록 별도 연결이나 EntityManager를 사용한다. 브로커를 대체해도 DB와 트랜잭션 리스너를 실제로 연결해야 커밋 가시성을 확인할 수 있다.