자바 디자인 패턴 용어 사전
실무 적용ApplicationEvent · @EventListener · @TransactionalEventListener

스프링 이벤트

Observer의 스프링 구현. 기본이 동기·같은 트랜잭션이라는 점이 함정이다.

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 쓰기도 된다" → 새 트랜잭션이 없으면 반영되지 않는다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 실무 적용 심화 — 프레임워크 소스로 읽는 패턴