
올해 3월, 회사에서 하나의 프로젝트를 맡게 되었다.
물류 관련 이벤트가 발생했을 때, 해당 이벤트 내용을 고객에게 이메일로 자동 발송하는 시스템을 만드는 프로젝트였다.
스타트업이라는 환경 덕분에 하나의 프로젝트를 처음부터 끝까지 혼자 책임질 수 있었고, 그만큼 설계부터 구현까지 온전히 주도할 수 있다는 점이 굉장히 설레었다. 그렇게 곧바로 설계에 들어가게 되었다.
이번 프로젝트는 생각보다 신경 쓸 요소가 많았다.
먼저, 알림과 관련된 데이터가 여러 DB에 흩어져 있어 Multi DB Source를 활용해야 했고,
이메일 콘텐츠 또한 단순한 텍스트가 아니라 꽤 화려한 형태를 요구했다.
과거 React를 사용했던 경험을 살려, HTML Email Content를 동적으로 생성할 수 있는 컴포넌트를 직접 설계·구현했다.
또한 도메인 로직 역시 단순하지 않았다.
이벤트의 종류, 상태, 사용자 설정에 따라 발송 여부와 내용이 달라졌기 때문에,
그동안 쌓아왔던 도메인 설계 역량을 최대한 활용할 수 있었던 프로젝트였다고 생각한다.
다만, 이번 글에서 중점적으로 다루고 싶은 내용은 위와 같은 세부 구현보다는
외부 서비스 이슈가 발생하더라도 이벤트 유실 없이 안정적으로 동작하는 구조를 어떻게 만들었는지이다.
따라서 불필요한 설계 설명은 과감히 생략하고, 핵심적인 부분만 다뤄보려고 한다.
전체적인 흐름은 다음과 같다.
이 과정에서 이메일 서버의 상태, 네트워크 문제, 혹은 발송 제한 정책 등으로 인해
외부 서비스가 요청을 거절하거나 실패하는 상황이 발생할 수 있다.
이를 대비해, 이메일 발송 실패 시 최대 5회까지 재시도하도록 구현했다.
(현재는 지수적 백오프를 적용하지 않았지만, 추후 더 안정적인 발송을 위해 적용할 계획이다.)
전체 플로우를 그림으로 표현하면 다음과 같다.

그렇다면 다음과 같은 상황을 가정해보자.
이 경우, 해당 이벤트는 완전히 유실될 수 있다.

사용자는 원래 받아야 했던 이메일을 받지 못하게 되고,
만약 이 서비스가 유료로 운영되고 있다면 이는 심각한 컴플레인으로 이어질 가능성이 크다.
즉, 외부 서비스로 요청을 보내는 구조에서는
“지금 당장 처리할 수 없는 상황에서도 이벤트 자체는 반드시 보존해야 한다” 는 요구사항이 생긴다.
이 문제를 해결하기 위한 대표적인 패턴이 바로 트랜잭션 아웃박스 패턴(Transaction Outbox Pattern)이다.
트랜잭션 아웃박스 패턴은 이벤트(메시지) 자체의 유실을 방지하기 위한 설계 패턴이다.
핵심 개념은 다음과 같다.
즉, 이벤트 발행을 트랜잭션 내부에서 즉시 처리하는 것이 아니라,
트랜잭션 바깥에서 안정적으로 처리하도록 분리하는 방식이다.
현재 겪고 있는 문제에 매우 적합한 해결책이었다.
이번 프로젝트에서는 트랜잭션 아웃박스 패턴을 다음과 같이 적용했다.
이를 통해 다음과 같은 구조를 만들 수 있었다.

여기서 한 가지 문제가 남는다.
실패로 기록된 이벤트는 이후 어떻게 처리할 것인가?
이 문제는 별도의 Batch Pipeline을 통해 해결했다.
구조는 다음과 같다.

전체 흐름을 정리하면 다음과 같다.
위와 같은 구조로 설계하고 구현한 이후,
현재까지 이메일 이벤트는 단 한 건도 유실되지 않고 사용자에게 전달되고 있다.
또한 다음과 같은 부가적인 효과도 얻을 수 있었다.
모두 추적할 수 있게 되었고,
이는 곧 모니터링과 운영 관점에서의 가시성 확보로 이어졌다.
기술적으로도, 개인적으로도 아주 재미있는 도전이었다.
다음 글에서는
이 구조를 실제 코드 레벨에서 어떻게 구현했는지를 좀 더 상세하게 풀어보려고 한다.
설명해야 할 내용이 많아 꽤 긴 글이 될 것 같지만,
다음 블로깅을 통해 차근차근 정리해보려 한다.
와 샌즈