"회원 가입(INSERT)은 성공했는데, 가입 축하 메일 이벤트(send) 발송은 네트워크 오류로 실패했습니다. DB는 롤백해야 할까요?"
DB 트랜잭션과 외부 메시지 큐(Kafka, RabbitMQ) 발행은 하나의 트랜잭션으로 묶을 수 없습니다(Dual Write Problem).
이를 해결하기 위해 **"보내야 할 메시지도 일단 DB에 같이 저장(Commit)하고, 나중에 꺼내서 보내자"**는 것이 아웃박스 패턴입니다.
핵심은 "DB에 저장된 메시지를 어떻게 꺼내서 브로커로 보낼 것인가?" 입니다.
1. 방식 1: 폴링 퍼블리셔 (Polling Publisher) - "심플한 스케줄러"
가장 직관적이고 구현하기 쉬운 방식입니다. 별도의 테이블(outbox)에 이벤트를 저장하고, 스케줄러가 주기적으로 이 테이블을 조회해서 메시지를 보냅니다.
동작 순서
- 트랜잭션 범위 내:
- 비즈니스 데이터 저장 (INSERT INTO members ...)
- 발송할 이벤트 저장 (INSERT INTO outbox ...) -> 여기까지 원자성 보장
- 비동기 스케줄러:
- SELECT * FROM outbox WHERE processed = false 조회
- Kafka로 메시지 발행
- 성공 시 UPDATE outbox SET processed = true (또는 삭제)
코드 예시 (Spring Scheduler)
Java
// 1. 비즈니스 로직 (회원 가입)
@Transactional
public void join(Member member) {
memberRepository.save(member);
// 이벤트를 바로 Kafka로 안 보내고, DB에 저장함
OutboxEvent event = new OutboxEvent("MemberCreated", member.getId());
outboxRepository.save(event);
}
// 2. 폴링 스케줄러 (별도 스레드)
@Scheduled(fixedDelay = 1000) // 1초마다 실행
public void publishEvents() {
List<OutboxEvent> events = outboxRepository.findAllByProcessedFalse();
for (OutboxEvent event : events) {
try {
// 실제 메시지 발행
kafkaProducer.send(event.getTopic(), event.getPayload());
// 발행 성공 처리
event.setProcessed(true);
outboxRepository.save(event);
} catch (Exception e) {
// 실패 시 재시도 로직
}
}
}
장단점
- 장점: 별도의 인프라 없이 DB와 애플리케이션 코드만으로 구현 가능합니다. 구현 난이도가 낮습니다.
- 단점:
- DB 부하: 스케줄러가 계속 DB를 찌르기(Polling) 때문에 데이터가 많아지면 DB 성능에 영향을 줍니다.
- 지연(Latency): 스케줄링 주기(예: 1초)만큼 메시지 발행이 지연됩니다. 실시간성이 떨어질 수 있습니다.
2. 방식 2: 트랜잭션 로그 테일링 (Log Tailing / CDC) - "고성능 실시간"
DB의 **트랜잭션 로그(Binary Log, WAL)**를 실시간으로 감지하여 메시지를 발행하는 방식입니다. 이를 **CDC(Change Data Capture)**라고 부르며, Debezium 같은 도구를 사용합니다.
동작 순서
- 트랜잭션 범위 내:
- 비즈니스 데이터 저장 + outbox 테이블에 이벤트 저장. (애플리케이션 할 일 끝)
- CDC 커넥터 (Debezium):
- DB의 로그 파일(Binlog)을 감시하다가 outbox 테이블에 INSERT가 발생하면 즉시 낚아챕니다.
- 낚아챈 데이터를 Kafka로 전송합니다.
구조 개념 (애플리케이션 코드가 아님)
Plaintext
[Spring Boot App]
↓ (INSERT)
[MySQL Database] -> (Binlog 기록됨)
↓ (감지)
[Debezium Connector]
↓ (Publish)
[Kafka Topic]
장단점
- 장점:
- 리얼타임: 쿼리가 커밋되자마자 거의 실시간으로 이벤트가 발행됩니다.
- DB 부하 없음: SELECT 쿼리를 날리지 않고 로그 파일을 읽기 때문에 DB 성능에 영향을 주지 않습니다.
- 단점:
- 인프라 복잡도: Kafka Connect, Debezium 같은 별도의 미들웨어를 구축하고 운영해야 합니다. 운영 난이도가 급상승합니다.
3. 실무 비교: 언제 무엇을 쓰는가?
| 구분 | Polling Publisher (스케줄러) | Log Tailing (CDC/Debezium) |
| 작동 방식 | 주기적 SELECT 조회 | DB 트랜잭션 로그(Binlog) 감지 |
| 실시간성 | 낮음 (스케줄링 주기에 의존) | 높음 (거의 즉시) |
| DB 부하 | 있음 (지속적인 쿼리 발생) | 없음 (로그 파일 읽기) |
| 구축 비용 | 낮음 (코드 구현) | 높음 (별도 인프라 필요) |
| 추천 상황 | 트래픽이 적거나, 약간의 지연(1~5초)이 허용될 때 | 대용량 트래픽, 실시간 데이터 동기화가 필수일 때 |
결론
대부분의 중소규모 서비스에서는 폴링(Polling) 방식으로도 충분합니다. 1~2초 정도 메일이 늦게 간다고 큰일 나지 않으니까요.
하지만 금융 거래, 실시간 재고 동기화처럼 0.1초의 지연도 허용하지 않는 대규모 시스템이라면, 인프라 비용을 감수하고서라도 CDC(Log Tailing) 방식을 도입하여 아키텍처를 고도화해야 합니다.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| 헬스 체크 패턴(Health Check Pattern) 완벽 정리 (0) | 2025.12.07 |
|---|---|
| 처리율 제한 패턴(Rate Limiter Pattern) 완벽 정리 (0) | 2025.12.07 |
| CQRS 패턴(CQRS Pattern) 완벽 정리 (0) | 2025.12.07 |
| API 게이트웨이 패턴(API Gateway) 완벽 정리 (0) | 2025.12.07 |
| 캐싱 패턴(Caching Pattern) 완벽 정리 (0) | 2025.12.07 |