Observer 패턴의 스프링 구현. 기본이 동기이고 같은 트랜잭션 안에서 돈다는 점이 가장 큰 함정이다.
@Transactional
public void place(Order order) {
repository.save(order);
publisher.publishEvent(new OrderPlacedEvent(order.getId()));
// ↑ 리스너가 '지금, 같은 스레드, 같은 트랜잭션' 에서 실행된다
}
@EventListener
public void on(OrderPlacedEvent e) {
mailClient.send(...); // 3초 걸리면 트랜잭션이 3초 길어진다
}
무엇이 문제가 되나
- 리스너가 느리면 커넥션을 그만큼 오래 잡는다 → 풀 고갈
- 리스너가 예외를 던지면 주문 저장까지 롤백된다
- 메일 발송 실패로 주문이 취소되는 사고가 여기서 나온다
- 아직 커밋되지 않은 상태를 리스너가 본다
- 리스너가 다른 스레드에 알렸는데 정작 커밋이 실패할 수 있다
커밋 이후로 미룬다
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void on(OrderPlacedEvent e) { mailClient.send(...); }
- AFTER_COMMIT — 커밋 확정 후 실행 → 리스너 실패가 주문에 영향을 주지 않는다
- 단 이 시점의 DB 쓰기는 REQUIRES_NEW 가 없으면 반영되지 않는다
- (트랜잭션이 이미 끝났기 때문 — 자주 걸리는 함정)
- BEFORE_COMMIT — 같은 트랜잭션 안. 쓰기가 함께 커밋된다
- AFTER_ROLLBACK — 실패 보상 처리에 쓴다
비동기로 뺄 때 잃는 것
-
@Async 를 함께 붙이면 응답 지연이 줄지만
-
실패해도 아무도 모른다 → 별도 실패 처리·재시도가 필요하다
-
스레드가 바뀌어 ThreadLocal 기반 컨텍스트가 사라진다
- (보안 컨텍스트 · 트레이싱 ID · MDC 로그 상관관계)
-
애플리케이션이 죽으면 이벤트가 유실된다
유실이 곤란하면 이벤트를 DB 에 저장하고 별도 워커가 읽는 구조로 간다 (저장과 발행을 같은 트랜잭션에 묶는 아웃박스 방식)
이벤트를 쓰는 이유와 대가
-
얻는 것 — 주문 코드가 메일·포인트·알림을 몰라도 된다 (결합 제거)
-
대가 — 호출 흐름이 코드에서 안 보인다
- "이 이벤트를 누가 듣는가" 를 IDE 로 찾아야 한다
-
리스너가 3개를 넘어가면 흐름 추적이 급격히 어려워진다
-
이벤트 이름과 리스너 위치에 대한 팀 규칙이 필요하다
이벤트가 실제로 배달되는 경로
publishEvent(event)
- ApplicationEventMulticaster 가 리스너 목록을 조회
- 이벤트 타입에 맞는 리스너를 순서대로 '직접 호출'
- 기본 구현은 SimpleApplicationEventMulticaster (동기 실행)
큐도 브로커도 없다. 그냥 메서드 호출이다 "이벤트" 라는 이름 때문에 비동기·큐를 떠올리는 것이 오해의 출발점이다
순서를 정해야 할 때
@EventListener
@Order(1) // 낮을수록 먼저
public void first(OrderPlacedEvent e) { ... }
리스너 사이에 순서 의존이 생겼다면 설계 신호다
- 사실은 '순차 절차' 인데 이벤트로 흩어 놓은 것일 수 있다
- 순서가 중요하면 서비스 메서드로 명시적으로 부르는 편이 낫다
이벤트는 '알림' 에 적합하고, '절차' 에는 부적합하다
도메인 이벤트를 엔티티에서 모으는 방식
스프링 데이터의 @DomainEvents / @AfterDomainEventPublication
- 엔티티가 이벤트를 모아 두고, 저장(save)될 때 한꺼번에 발행된다
- 도메인 로직이 ApplicationEventPublisher 를 직접 몰라도 된다
트랜잭션 경계와 발행 시점이 맞물리므로 AFTER_COMMIT 리스너와 조합하면 안전한 알림 흐름이 만들어진다
면접 함정
- ❌ "이벤트를 발행했으니 비동기다" →
@EventListener는 동기다. - ❌ "AFTER_COMMIT이면 DB 쓰기도 된다" → 새 트랜잭션이 없으면 반영되지 않는다.