Java 아키텍처·Spring 용어 사전
메시징Spring 이벤트 · AFTER_COMMIT · ApplicationEventPublisher

@TransactionalEventListener

커밋 이후에만 이벤트를 처리하게 하는 리스너. 한 프로세스 안의 결합을 끊는 가장 가벼운 수단이다.

Spring 이벤트를 트랜잭션 커밋 이후에만 처리하게 하는 리스너.

왜 필요한가 — 그냥 @EventListener의 문제

@Transactional
void order() {
    repo.save(o);
    publisher.publishEvent(new OrderPlaced(o.getId()));   // 동기·같은 트랜잭션에서 실행
}

@EventListener
void sendMail(OrderPlaced e) { mailClient.send(...); }    // 여기서 실패하면?
  • ① 리스너가 실패하면 주문까지 롤백된다 (메일 때문에 주문이 취소된다)
  • ② 아직 커밋 전인데 메일이 나간다 → 롤백되면 '없는 주문' 의 메일이 발송된 상태

해결 — 커밋 이후로 미룬다

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
void sendMail(OrderPlaced e) { mailClient.send(...); }
  • BEFORE_COMMIT — 커밋 직전
  • AFTER_COMMIT — 커밋 성공 후 (기본값·가장 많이 쓴다)
  • AFTER_ROLLBACK — 롤백된 경우
  • AFTER_COMPLETION — 성공·실패 무관

AFTER_COMMIT의 함정 — 여기서 DB를 쓰면 저장되지 않는다

@TransactionalEventListener(phase = AFTER_COMMIT)
void grantPoint(OrderPlaced e) {
    pointRepo.save(new Point(...));    // 트랜잭션이 이미 끝났다 → 반영되지 않는다
}

// 새 트랜잭션을 명시해야 한다
@TransactionalEventListener(phase = AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
void grantPoint(OrderPlaced e) { pointRepo.save(...); }

비동기로 돌리려면

@Async                                    // 별도 스레드 (@EnableAsync 필요)
@TransactionalEventListener(phase = AFTER_COMMIT)
void sendMail(OrderPlaced e) { ... }

단 비동기로 만드는 순간 유실 가능성이 생긴다 — 서버가 그 사이 죽으면 이벤트가 사라진다. 반드시 도달해야 하면 Outbox 패턴으로 간다.

어디까지 이걸로 하나

한 프로세스 안의 결합 분리 → Spring 이벤트로 충분하다

  • 다른 서비스로 전달 · 유실 불가 — → Kafka/RabbitMQ + Outbox

"일단 Spring 이벤트로 경계를 만들어 두고, 필요해지면 메시지 브로커로 바꾼다" 가 실용적인 진화 경로다. 발행·구독 코드 모양이 비슷해 전환 비용이 낮다.

면접 함정

  • "@EventListener는 비동기" → 기본은 동기이고 같은 트랜잭션에서 돈다. @Async를 붙여야 비동기다.
  • "AFTER_COMMIT이면 안전하다" → 그 안의 DB 쓰기는 새 트랜잭션이 필요하고, 비동기면 유실될 수 있다.

이벤트를 언제 발행하나 — 도메인 안에서

@Entity
class Order extends AbstractAggregateRoot<Order> {   // Spring Data 제공
    void complete() {
        this.status = COMPLETED;
        registerEvent(new OrderCompleted(this.id));    // 아직 발행되지 않는다
    }
}
// repository.save(order) 시점에 모아 둔 이벤트가 발행된다

서비스가 아니라 도메인이 이벤트를 등록하게 하면, "주문이 완료되면 이벤트가 난다"는 규칙이 도메인 안에 남는다.

트랜잭션이 없으면 아예 실행되지 않는다

@TransactionalEventListener              // 트랜잭션 밖에서 발행하면 그냥 무시된다
void on(OrderPlaced e) { ... }

@TransactionalEventListener(fallbackExecution = true)   // 트랜잭션이 없어도 실행
void on(OrderPlaced e) { ... }

테스트에서 이벤트가 안 불려 당황하는 흔한 원인이다. 트랜잭션 없이 발행했기 때문이다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 메시징 — Kafka·RabbitMQ·이벤트