Post
Spring Boot 이벤트 처리 안정화: Transactional Outbox 패턴 실전 적용
문제 정의
주문 생성 후 “결제 요청 이벤트”를 발행하는 로직은 흔하지만, 아래 장애가 반복된다.
- DB는 커밋됐는데 메시지 브로커 전송 실패
- 브로커 전송은 됐는데 DB 롤백
- 재시도 과정에서 중복 이벤트 발행
즉, 로컬 트랜잭션(DB)과 외부 시스템(Kafka/RabbitMQ)의 원자성 불일치가 핵심 문제다.
선택지 비교
1) 서비스 코드에서 DB 저장 후 즉시 publish
- 장점: 코드가 단순
- 단점: 부분 실패 시 정합성 깨짐
2) 2PC/XA 트랜잭션
- 장점: 이론적으로 강한 일관성
- 단점: 운영 복잡도 높고 성능 저하, 장애 분석 어려움
3) Transactional Outbox
- 장점: DB 트랜잭션 내에서 이벤트 기록 보장
- 단점: Outbox 소비자/재처리 파이프라인 필요
현대 마이크로서비스에서는 대부분 3번이 현실적인 해법이다.
구현 핵심
1) 도메인 저장 + Outbox insert를 같은 트랜잭션으로 처리
@Transactional
public Long createOrder(CreateOrderCommand cmd) {
Order order = orderRepository.save(Order.create(cmd));
OutboxEvent event = OutboxEvent.of(
"order",
order.getId().toString(),
"OrderCreated",
objectMapper.writeValueAsString(new OrderCreatedPayload(order.getId(), order.getTotalAmount()))
);
outboxRepository.save(event);
return order.getId();
}
핵심은 publish를 하지 않고, 발행할 사실을 DB에 남기는 것.
2) 별도 퍼블리셔가 Outbox를 읽어 브로커로 전달
@Scheduled(fixedDelay = 1000)
public void publishOutbox() {
List<OutboxEvent> events = outboxRepository.findTop100ByStatusOrderByIdAsc(OutboxStatus.PENDING);
for (OutboxEvent e : events) {
try {
kafkaTemplate.send("order-events", e.getAggregateId(), e.getPayload()).get();
e.markPublished();
} catch (Exception ex) {
e.increaseRetry();
}
}
}
여기서 중요한 건 상태 전이(PENDING -> PUBLISHED)와 재시도 횟수 관리다.
3) 소비자 측 idempotency 보장
Outbox를 도입해도 중복 전달은 발생할 수 있다. 소비자는 eventId 기반 중복 방지를 반드시 가져야 한다.
성능/운영 이슈
1) Outbox 테이블 설계
권장 컬럼:
id(PK, 증가 키)event_id(UUID, 유니크)aggregate_type,aggregate_idevent_type,payloadstatus,retry_count,next_retry_atcreated_at,published_at
인덱스는 status, id 조합이 핵심이다.
2) 재시도 정책
- 즉시 재시도 3회
- 이후 지수 백오프
- 임계 초과 시 DLQ(or FAILED 상태)로 격리
무한 재시도는 장애를 숨기고 운영자 피로도를 높인다.
3) 정리(아카이빙) 정책
Outbox를 계속 쌓아두면 성능 저하가 온다.
PUBLISHED7일 이후 아카이브- 월 단위 파티셔닝 검토
- 운영 대시보드에서 pending 건수 알람
회고 및 확장 포인트
Outbox는 메시징 안정화 패턴이지만, 본질은 “실패를 전제한 설계”다. 단일 성공 경로가 아니라 실패/재시도/복구 경로를 먼저 설계하면 운영이 급격히 안정된다.
다음 단계로는 다음을 붙이면 좋다.
- CDC(Debezium) 기반 Outbox relay
- 이벤트 스키마 버전 관리
- consumer side exactly-once 유사 보장(중복 무해화)
오늘의 적용 체크리스트
- 핵심 이벤트 발행 API의 DB/브로커 원자성 점검
- Outbox 테이블/인덱스 설계 문서화
- 퍼블리셔 재시도/백오프/DLQ 정책 확정
- 소비자 중복 처리(idempotency key) 구현
- pending outbox 알람 기준 수립
댓글