트랜잭션 아웃박스 패턴으로 외부 서비스 이슈에도 이벤트 유실 없는 안정적인 서비스 만들기

김재연·2025년 12월 14일
post-thumbnail

배경

올해 3월, 회사에서 하나의 프로젝트를 맡게 되었다.

물류 관련 이벤트가 발생했을 때, 해당 이벤트 내용을 고객에게 이메일로 자동 발송하는 시스템을 만드는 프로젝트였다.

스타트업이라는 환경 덕분에 하나의 프로젝트를 처음부터 끝까지 혼자 책임질 수 있었고, 그만큼 설계부터 구현까지 온전히 주도할 수 있다는 점이 굉장히 설레었다. 그렇게 곧바로 설계에 들어가게 되었다.

설계 개요

이번 프로젝트는 생각보다 신경 쓸 요소가 많았다.

먼저, 알림과 관련된 데이터가 여러 DB에 흩어져 있어 Multi DB Source를 활용해야 했고,
이메일 콘텐츠 또한 단순한 텍스트가 아니라 꽤 화려한 형태를 요구했다.

과거 React를 사용했던 경험을 살려, HTML Email Content를 동적으로 생성할 수 있는 컴포넌트를 직접 설계·구현했다.

또한 도메인 로직 역시 단순하지 않았다.
이벤트의 종류, 상태, 사용자 설정에 따라 발송 여부와 내용이 달라졌기 때문에,
그동안 쌓아왔던 도메인 설계 역량을 최대한 활용할 수 있었던 프로젝트였다고 생각한다.

다만, 이번 글에서 중점적으로 다루고 싶은 내용은 위와 같은 세부 구현보다는
외부 서비스 이슈가 발생하더라도 이벤트 유실 없이 안정적으로 동작하는 구조를 어떻게 만들었는지이다.

따라서 불필요한 설계 설명은 과감히 생략하고, 핵심적인 부분만 다뤄보려고 한다.

기술 스택 및 기본 플로우

  • 프로젝트 프레임워크: Spring Batch Framework
  • 이메일 발송 서비스: Google SMTP

전체적인 흐름은 다음과 같다.

  1. 물류 관련 이벤트 발생
  2. 이메일 발송 이벤트 생성
  3. 이벤트를 비동기로 처리하는 Event Listener에서 처리
  4. 사용자 설정에 따라 이메일 발송 여부 결정
  5. 발송 대상인 경우 SMTP 서버로 요청 전송

이 과정에서 이메일 서버의 상태, 네트워크 문제, 혹은 발송 제한 정책 등으로 인해
외부 서비스가 요청을 거절하거나 실패하는 상황이 발생할 수 있다.

이를 대비해, 이메일 발송 실패 시 최대 5회까지 재시도하도록 구현했다.
(현재는 지수적 백오프를 적용하지 않았지만, 추후 더 안정적인 발송을 위해 적용할 계획이다.)

전체 플로우를 그림으로 표현하면 다음과 같다.

외부 서비스 장애가 가져오는 문제

그렇다면 다음과 같은 상황을 가정해보자.

  • 이메일 발송 재시도를 5번 모두 실패
  • 외부 SMTP 서비스 장애
  • 네트워크 불안정 상태

이 경우, 해당 이벤트는 완전히 유실될 수 있다.

사용자는 원래 받아야 했던 이메일을 받지 못하게 되고,
만약 이 서비스가 유료로 운영되고 있다면 이는 심각한 컴플레인으로 이어질 가능성이 크다.

즉, 외부 서비스로 요청을 보내는 구조에서는
“지금 당장 처리할 수 없는 상황에서도 이벤트 자체는 반드시 보존해야 한다” 는 요구사항이 생긴다.

이 문제를 해결하기 위한 대표적인 패턴이 바로 트랜잭션 아웃박스 패턴(Transaction Outbox Pattern)이다.

트랜잭션 아웃박스 패턴이란?

트랜잭션 아웃박스 패턴은 이벤트(메시지) 자체의 유실을 방지하기 위한 설계 패턴이다.

핵심 개념은 다음과 같다.

  • 이벤트를 바로 외부 시스템으로 전송하지 않는다
  • 먼저 DB에 안전하게 저장한다
  • 이후 별도의 프로세스에서 해당 이벤트를 읽어 처리한다

즉, 이벤트 발행을 트랜잭션 내부에서 즉시 처리하는 것이 아니라,
트랜잭션 바깥에서 안정적으로 처리하도록 분리하는 방식이다.

현재 겪고 있는 문제에 매우 적합한 해결책이었다.

프로젝트에 적용한 트랜잭션 아웃박스 패턴

이번 프로젝트에서는 트랜잭션 아웃박스 패턴을 다음과 같이 적용했다.

1. 이벤트 저장과 발행을 하나의 트랜잭션으로 처리

  • 이메일 이벤트를 DB에 먼저 저장
  • 동일 트랜잭션 내에서 이벤트 발행
  • 이를 통해 이벤트 기록과 발행의 원자성 보장

2. 이벤트 처리 결과를 DB에 기록

  • Event Listener에서 이메일 발송 시도
  • 성공 / 실패 여부를 DB에 업데이트

이를 통해 다음과 같은 구조를 만들 수 있었다.

실패한 이벤트는 어떻게 처리할 것인가?

여기서 한 가지 문제가 남는다.

실패로 기록된 이벤트는 이후 어떻게 처리할 것인가?

이 문제는 별도의 Batch Pipeline을 통해 해결했다.

  • 실패 상태의 이벤트를 주기적으로 조회
  • 이벤트 재발행 모듈을 통해 다시 처리
  • 성공 시 상태 업데이트

구조는 다음과 같다.

최종 이벤트 처리 플로우 정리

전체 흐름을 정리하면 다음과 같다.

  1. 이벤트를 DB에 저장하고 동일 트랜잭션 내에서 발행
  2. Event Listener에서 이메일 발송 시도 후 성공/실패 기록
  3. 별도의 Batch Pipeline에서 실패한 이벤트 조회
  4. 실패 이벤트 재발행 및 재처리

마무리

위와 같은 구조로 설계하고 구현한 이후,
현재까지 이메일 이벤트는 단 한 건도 유실되지 않고 사용자에게 전달되고 있다.

또한 다음과 같은 부가적인 효과도 얻을 수 있었다.

  • 언제 이벤트가 발생했는지
  • 누구에게 발송되었는지
  • 어떤 내용의 이메일이 전달되었는지

모두 추적할 수 있게 되었고,
이는 곧 모니터링과 운영 관점에서의 가시성 확보로 이어졌다.

기술적으로도, 개인적으로도 아주 재미있는 도전이었다.

다음 글에서는
이 구조를 실제 코드 레벨에서 어떻게 구현했는지를 좀 더 상세하게 풀어보려고 한다.

설명해야 할 내용이 많아 꽤 긴 글이 될 것 같지만,
다음 블로깅을 통해 차근차근 정리해보려 한다.

profile
끊임없이 '성장'하는 개발자 김재연입니다.

4개의 댓글

comment-user-thumbnail
2025년 12월 14일

와 샌즈

1개의 답글
comment-user-thumbnail
2025년 12월 14일

재연님 좋은 글 감사합니다 ~ 덕분에 많은 도움이 되었어요 !

1개의 답글