들어가며
마이크로서비스 아키텍처에서 서비스 간 통신은 필수적인 요소입니다. 하지만 분산 시스템에서 데이터베이스 트랜잭션과 메시지 발행을 동시에 보장하는 것은 쉽지 않은 일입니다. 만약 데이터베이스 변경은 성공했지만 메시지 발행이 실패한다면 어떻게 될까요? 이런 문제를 해결하기 위해 아웃박스 패턴을 도입하게 되었습니다.
문제 상황
기존에는 회원탈퇴 처리 시 다음과 같은 방식으로 구현되어 있었습니다.
@Transactional
public void withdrawUser(WithdrawRequestDto request, Long userId) {
User user = userRepository.findByIdAndIsDeletedFalse(userId)
.orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND));
user.delete(); // DB 변경
// 메시지 발행
rabbitTemplate.convertAndSend("user.exchange", "user.withdrawn", message);
}
이 방식의 문제점은 다음과 같습니다.
1. 트랜잭션 경계 문제
데이터베이스 트랜잭션이 커밋된 후 메시지 발행이 실패하면, 데이터베이스는 변경되었지만 다른 서비스에는 이 사실이 전달되지 않습니다.
2. 일관성 보장 불가
분산 시스템에서 여러 리소스(데이터베이스, 메시지 브로커)에 대한 원자적 처리를 보장할 수 없습니다.
3. 장애 복구의 어려움
메시지 발행 실패 시 어떤 이벤트가 누락되었는지 추적하고 재처리하기 어렵습니다.
아웃박스 패턴 선택 이유
이런 문제들을 해결하기 위해 아웃박스 패턴을 선택했습니다. 아웃박스 패턴을 선택한 주요 이유는 다음과 같습니다.
1. 트랜잭션 일관성 보장
비즈니스 데이터 변경과 이벤트 저장을 하나의 트랜잭션으로 처리할 수 있습니다.
2. 메시지 발행 보장
메시지 발행 실패 시 재시도 메커니즘을 통해 최종적으로는 메시지가 발행되도록 보장할 수 있습니다.
3. 장애 복구 가능성
아웃박스 테이블을 통해 미발행된 이벤트를 추적하고 재처리할 수 있습니다.
구현 아키텍처
전체적인 아키텍처는 다음과 같습니다.
비즈니스 로직 실행
↓
아웃박스 테이블에 이벤트 저장 (같은 트랜잭션)
↓
스프링 이벤트 발행 (TransactionPhase.AFTER_COMMIT)
↓
RabbitMQ 메시지 전송
↓
성공 시 아웃박스 테이블 published = true로 업데이트
핵심 구현 내용
1. 아웃박스 이벤트 엔티티
먼저 이벤트를 저장할 아웃박스 테이블을 정의했습니다.

2. 비즈니스 로직에서 아웃박스 이벤트 저장
회원탈퇴 처리 시 비즈니스 데이터 변경과 아웃박스 이벤트 저장을 같은 트랜잭션으로 처리합니다.

3. 트랜잭션 커밋 후 메시지 발행
스프링의 @TransactionalEventListener를 사용하여 트랜잭션 커밋 후에 메시지를 발행합니다.

스프링 이벤트를 선택한 이유
스프링 이벤트를 사용한 이유는 다음과 같습니다.
1. 트랜잭션 경계 제어
TransactionPhase.AFTER_COMMIT을 사용하여 트랜잭션이 성공적으로 커밋된 후에만 메시지를 발행할 수 있습니다.
2. 비동기 처리
메시지 발행 과정에서 발생하는 지연이 비즈니스 로직의 응답 시간에 영향을 주지 않습니다.
3. 관심사 분리
비즈니스 로직과 메시지 발행 로직을 분리하여 코드의 가독성과 유지보수성을 향상시킬 수 있습니다.
재시도 메커니즘 구현
메시지 발행이 실패할 경우를 대비하여 스케줄러 기반의 재시도 메커니즘을 구현했습니다.

Repository 계층 설계
복잡한 쿼리를 효율적으로 처리하기 위해 Repository를 JpaRepository와 QueryRepository로 분리했습니다.

QueryDSL을 사용하여 타입 세이프한 쿼리를 작성했습니다.

예외 처리 전략
예외 상황을 명확히 구분하기 위해 구체적인 예외 처리를 구현했습니다.

각 상황에 맞는 예외를 던져 호출하는 쪽에서 적절한 처리를 할 수 있도록 했습니다.
레이어드 아키텍처 준수
스케줄러에서도 Repository를 직접 참조하지 않고 Service 계층을 통해 접근하도록 구현했습니다.

구현 결과 및 장점
1. 데이터 일관성 보장
비즈니스 데이터 변경과 이벤트 발행이 최종적으로 일치하도록 보장할 수 있게 되었습니다.
2. 장애 복구 능력
메시지 발행 실패 시 자동으로 재시도하며, 수동 개입이 필요한 케이스도 추적 가능합니다.
3. 시스템 안정성
무한 재시도를 방지하여 시스템 리소스를 보호하고, 장애 상황에서도 시스템이 마비되지 않습니다.
4. 운영 가시성
아웃박스 테이블을 통해 이벤트 발행 상태를 추적하고 모니터링할 수 있습니다.
마무리
아웃박스 패턴을 도입함으로써 분산 시스템에서 데이터 일관성과 메시지 발행을 안전하게 보장할 수 있게 되었습니다. 특히 스프링 이벤트와 RabbitMQ를 조합하여 비동기적이면서도 신뢰성 있는 이벤트 처리 시스템을 구축할 수 있었습니다.
이 패턴은 초기 구현 비용이 있지만, 장기적으로는 시스템의 안정성과 신뢰성을 크게 향상시킬 수 있는 투자라고 생각합니다. 특히 마이크로서비스 아키텍처에서 서비스 간 통신의 신뢰성이 중요한 환경에서는 반드시 고려해볼 만한 패턴입니다.
'Spring 7기 프로젝트 > 모임 플렛폼 프로젝트' 카테고리의 다른 글
| Spring Boot에서 RabbitMQ Publisher Confirm 도메인별 설정하기 (1) | 2025.08.06 |
|---|---|
| Spring의 @TransactionalEventListener와 새로운 트랜잭션 전파 (0) | 2025.08.05 |
| Spring Event에서 RabbitMQ로: MSA 전환을 위한 메시징 아키텍처 마이그레이션 (2) | 2025.08.01 |
| Docker Compose 아키텍처 선택 가이드: 단일 vs 다중 인스턴스 구성 (1) | 2025.08.01 |
| Spring Boot 모놀리스 구조에서 ALB는 왜 필요할까요? (0) | 2025.07.31 |