Event Driven Architecture와 Transactional Outbox Pattern의 연관성
서론
여행 기록 관리 플랫폼 ‘여기가’ 프로젝트를 진행하며 이메일 인증·비밀번호 초기화 이메일 전송 기능 개발을 담당하게 되었다. 이메일 전송은 외부 메일 서버를 거쳐 실행되는 작업으로 Network I/O 등 시간이 오래 걸리는 작업이다. 따라서, 핵심 비즈니스 로직과 이메일 발송 로직을 분리하여 결합도를 낮추고, 향후 메일 서버 분리의 확장성을 염두해 이메일 발송 로직을 이벤트 기반 아키텍처(EDA)로 분리하였다.
‘이메일 인증’, ‘비밀번호 초기화’와 같은 핵심 기능(Usecase)을 도메인 이벤트로 정의하였다. 그리고, 인증 번호 발급·저장과 같은 핵심 비즈니스 로직 이후 처리해야 할 부가 로직은 각 도메인 이벤트를 수신해 처리하도록 설계하였다. 앞서 이벤트를 통한 시스템 결합도 낮추기 포스팅에서 다루었듯이 이벤트는 로직 간 결합도를 낮출 수 있는 구조이다.
그러나, 부가 로직(이메일 발송)을 EDA 구조로 처리하던 중 메시지 브로커로 도메인 이벤트가 발행되지 않아 이벤트가 유실되는 문제를 겪었다. 유실된 이벤트는 다시 처리할 수 있는 방법이 없기에 인증 번호 유효 시간동안 사용자는 계속해서 인증 번호를 기다려야만 하는 문제가 발생한 것이다.
이 문제에 대한 해결 방법을 찾던 중 ‘Transactional Outbox Pattern’을 알게 되었다. 해당 포스팅에서는 Transactional Outbox Pattern에 관한 개념과 EDA에서 Transactional Outbox Pattern의 연관성을 서술하고자 한다.
본론
# Event Driven Architecture에서 Transactional Outbox Pattern이 사용되는 이유
이전 프로젝트에서는 이벤트 기반 구조를 적용한 경험이 없었기에, 이메일 발송 부가 로직을 이벤트로 분리하는 작업을 진행하지 얼마 지나지 않은 상태에서 이벤트 기반 구조와 트랜잭션 아웃박스 패턴의 개념의 연관성에 대한 의문이 들었다.
처음에는 “트랜잭션 아웃박스 패턴을 사용하면 왜 EDA를 같이 사용하지?”라는 의문이 들정도로 해당 개념을 제대로 파악하지 못하였다. 그러나, 다양한 자료를 수없이 읽어보고 프로젝트에서 트랜잭션 아웃박스 패턴의 이벤트 영속화 아이디어를 참고하여 이벤트 재발행 구조를 도입하면서 EDA를 사용할 때 Transactional Outbox Pattern이 필요한 경우가 있다는 것을 알게되었다.
이벤트 기반 구조에서는 다양한 이점이 존재하지만, “메시지 전송 전 유실 문제”라는 심각한 문제도 존재한다. 또한, 타 서비스에서 “이벤트 재발행”을 요구하는 경우도 존재한다.
## 메시지 유실 문제

서버는 핵심 로직 실행 후 해당 도메인 이벤트를 MessageBroker로 발행하게 된다.
이때, MessageBroker의 일시적 장애나 다른 요인으로 인하여 메시지(이벤트)가 브로커로 전달되지 않는다면 해당 이벤트는 유실되어 더이상 처리할 수 없게 된다.
현재 ‘여기가’ 서비스에서는 이메일 인증, 비밀번호 초기화 등 유효 시간이 존재하는 도메인 이벤트를 처리하지만, 그기간 동안 사용자는 동작이 제한되어 사용자 경험에 안 좋은 영향을 미친다.
하물며, MSA 구조에서 다른 서비스로 CUD 작업에 대한 이벤트 전파 시 이벤트가 유실되어버린다면 엔티티의 정합성이 붕괴되는 심각한 문제를 초래할 수 있다. 즉, EDA에서 이벤트 발행자는 이벤트 발행의 책임을 가진다.
메시지(이벤트) 유실 문제의 해결 방법은 ‘이벤트 영속화’이다. 메시지 브로커로 이벤트 발행이 실패하더라도 영속화된 이벤트가 있다면 언제든지 재발행이 가능할 것이다.
이 문제를 해결하는 방법이 트랜잭션 아웃박스 패턴(Transactional Outbox Pattern)이다.
# Transactional Outbox Pattern
이벤트 기반 아키텍처에서 외부 메시지 브로커를 사용하여 이벤트를 서버 외부로 발행하는 경우 이벤트(메시지)가 유실될 수 있다는 문제점을 인식하였다. 이를 해결하기 위한 방법이 트랜잭션 아웃박스 패턴(Transactional Outbox Pattern)이다.

트랜잭션 아웃박스 패턴의 핵심은 이벤트를 아웃박스(Outbox)로 저장(영속화)하는 것이다. 또한, 이 아웃박스의 저장을 핵심 로직과 동일 트랜잭션으로 묶어 원자적으로 실행하는 것이다.
이벤트의 영속화는 이벤트 유실에 대한 근본적인 해결책이다. 이벤트가 몇 번이고 유실되어도 DB에 저장된 이벤트가 있다면 추가적으로 발행할 수 있으며, 타 서비스의 특정 재발행 요구를 언제나 대응할 수 있는 수단이다.
Outbox
아웃박스(Outbox)는 사전적인 의미로 “전송 중이거나 전송에 실패한 메시지가 점시 머무는 보관함”을 의미한다.
해당 의미 그대로, 외부로 발행해야할 이벤트들이 아웃박스 형태로 변환되어 데이터베이스에 잠시 머무르게 되는 것이다.
이벤트의 저장
트랜잭션 아웃박스 패턴은 이벤트를 저장한다. 정확하게는 아웃박스의 형태로 저장하는 것이다.
아웃박스를 저장하기 위한 이벤트 저장소(데이터베이스)를 고민해야한다.
아웃박스에는 이벤트의 정보와 아웃박스 자체에 대한 메타 데이터 등에 대한 내용만 가지고 있기 때문에 작은 단위로 저장되며, 이벤트는 고속으로 처리되어야 하기 때문에 RDBMS가 아닌 다른 데이터베이스를 사용해야한다고 생각할 수 있다.
그러나, 우아한 기술 블로그 - 회원 시스템 이벤트기반 아키텍처 구축하기 포스팅에서 같은 RDBMS를 사용해도 괜찮다는 내용을 확인할 수 있었다. 이벤트 저장소와 도메인 저장소를 다른 종류의 데이터베이스로 사용할 경우, 두 저장소에 대한 분산 트랜잭션을 구현해야 하나 이는 굉장히 어려운 일이고 트레이드오프를 고려해야하는 작업이라고 설명한다.
따라서, 도메인 저장소와 이벤트 저장소를 동일한 DBMS로 사용한다면 트랜잭션 처리는 DBMS에 믿고 맡길 수 있다. 쓰기량 및 읽기량에 대한 성능적 리스크는 스케일업/아웃 혹은 샤딩을 하는 방식으로 확장하여 대응 가능하다고 한다.
필자 또한 분산 트랜잭션에 관한 경험 부족을 고려하여 단일 저장소(RDBMS, MySQL)을 사용하여 이벤트(아웃박스)를 저장하기로 하였다.
이벤트의 상태
도메인 이벤트의 발생 서비스에서는 2가지 책임을 가진다.
- 이벤트의 저장
- 이벤트의 발행
또한, 이벤트는 아래와 같은 상태로 분류할 수 있다.
- 막 저장되어 발행을 대기하고 있는 이벤트
- 메시지 브로커로 발행에 성공한 이벤트
- 메시지 브로커로 발행에 실패한 이벤트
위 분류 외에도 다양한 상태를 정의하여 세밀하게 이벤트를 제어할 수 있다.
가장 중요한 것은 이벤트의 메시지 브로커까지의 발행 여부이다. 발행되지 않은 이벤트는 재발행을 해야한다. 그것이 도메인 이벤트 발생 서비스에서 가지는 책임이다.
트랜잭션 시점(Before/After Commit)에 따른 이벤트 처리 분리
트랜잭션 아웃박스 패턴의 핵심 개념 중 하나는 비즈니스 로직과 아웃박스 기록을 하나의 트랜잭션으로 묶어 원자적 수행 하는 것이다.
Spring Event를 사용할 경우 commit, rollback 등 트랜잭션 전/후 다양한 시점에서 Event를 처리할 수 있는 어노테이션 @TransactionalEventListener를 제공한다.
따라서, 커밋 이전 시점(Before Commit)과 커밋 이후 시점(After Commit)에서 이벤트를 처리할 수 있다.
- 이벤트 기록:
TransactionPhase.BEFORE_COMMIT - 이벤트 발행:
TransactionPhase.AFTER_COMMIT
트랜잭션 아웃박스 패턴의 공식 문서를 참고하면, 이벤트 시점을 기준으로 이벤트를 발행하는 흐름은 보이지 않는다. 이는 MessageRelay를 통해서 주기적으로 이벤트를 폴링해 재발행할 수 있는 구조이기 때문에 굳이 시점을 기준으로 이벤트를 꼭 발행하지 않아도 되기 때문이다.
그러나, ‘여기가’ 서비스에서는 유효시간이 존재하는 이벤트를 다루기 때문에 빠른 이벤트 발행이 필수라고 생각하여 커밋 이후 시점에 바로 이벤트 발행을 시도하도록 구현하였다.
TransactionPhase.BEFORE_COMMIT
TransactionPhase.BEFORE_COMMIT 시점에서는 이벤트를 이벤트 아웃박스 저장소에 기록(저장)한다. 만약 이벤트 저장소에 기록이 실패하게 되면 트랜잭션 전체가 실패하게 되며 롤백되게 된다.
TransactionPhase.AFTER_COMMIT
TransactionPhase.AFTER_COMMIT 시점에서는 이벤트를 메시지 브로커로 발행한다. 이 시점에서는 이미 이벤트 아웃박스의 기록이 완료되었기 때문에 발행에 실패하더라도 상관없다.
앞서 설명하였듯이 굳이 커밋 이후 즉시 이벤트를 발행해야하는 것은 아니며, 트랜잭션 시점에 따른 이벤트 처리 기능을 제공하지 않는 프레임워크를 사용하는 경우라면 굳이 해당 단계를 생략하더라도 MessageRelay를 통해 재발행 가능하기 때문에 생략 가능하다.
Event Polling (MessageRelay)
트랜잭션 아웃박스 패턴은 DB에 저장된 아웃박스를 주기적으로 조회(Polling)하여 메시지 브로커로 발행한다.
메시지 릴레이(Message Relay)는 아웃박스의 주기적인 조회를 담당한다. 본 프로젝트에서는 이벤트를 아웃박스 형태로 저장하기에 이벤트 폴러(Event Poller)라는 이름을 사용한다. 또한, EventPoller는 조회한 이이벤트를 외부로 발행해야하는 외부 이벤트 발행기(External Event Publisher)의 역할도 수행해야한다.
필자는 실제 구현 상에 있어서 이벤트 폴러와 외부 이벤트 발행기를 분리하여 구현하였다.
이벤트 폴러의 경우에는 상태에 따라 조회해야할 쿼리가 다양하고, 이벤트 아웃박스를 조회하는 것이 주된 역할이다. 또한, 외부 이벤트 발행 로직은 TransactionPhase.AFTER_COMMIT 시점에서도 동시에 사용될 수 있다. 따라서, 각 클래스간 응집도와 재사용성을 고려하여 분리하여 구현하였다.
이벤트 발행 여부
이벤트의 상태를 기록하며 ‘메시지 브로커로 이벤트 발행이 성공한 상태’를 기록한다고 하였다.
그렇다면 서버에서는 이벤트를 발행하면 해당 이벤트가 외부 메시지 브로커로 잘 도착했는지 여부를 알 수 있어야한다.
이는 다양한 메시지 브로커에서 제공하는 Publish Comfirm 기술을 통해 메시지 브로커로 메시지를 수신했는지 여부를 확인할 수 있다.
이에 관한 자세한 내용은 차후 포스팅에서 다룰 예정이다.
# 결론
EDA는 이벤트를 통해서 다른 모듈·서비스로 이벤트를 전달하기에 한 도메인에서 핵심 비즈니스 로직과 부가 로직의 결합도를 낮추고, 높은 확장성을 제공한다. ‘여기가’ 서비스처럼 메일 전송과 같은 특정 작업 전용 서버의 분리 가능성을 고려한다면 EDA를 도입하는 것도 좋다고 생각한다. 또한, MSA 환경에서는 다른 서비스로 CUD 이벤트를 전파하여 데이터 정합성을 유지하기도 한다.
도메인 이벤트의 발생 서비스에서는 부가 로직에 대한 관심을 가질 필요는 없지만, 해당 이벤트에 대한 부가 로직이 실행되기 위해서 메시지 발행의 책임을 가진다.
이때, 외부 메시지 브로커의 장애 등으로 메시지가 유실되거나 타 서비스에서 이벤트 재발행을 요구하는 경우에 일회성으로 발행한 이벤트를 재발행할 수 있는 방법이 없다.
트랜잭션 아웃박스 패턴의 핵심은 위와 같은 문제를 해결하고자 이벤트를 저장소에 저장하는 것이다.
이벤트를 아웃박스 저장소에 저장하기 때문에 실패한 이벤트·재발행을 요구 등에 대해 언제든지 이벤트를 재발행 할 수 있는 기반을 마련할 수 있다.
이벤트는 아웃박스(Outbox)의 형태로 저장된다.
아웃박스는 ‘보낼 편지함’이라는 사전적 의미처럼 발행을 위해 임시로 저장된 이벤트들을 나타내는 개념이다. 아웃박스는 발행/재발행 등을 처리하기 위하여 상태를 기록해야 한다.
Spring Event는 @TransactionalEventListener를 통해 트랜잭션 시점에 따라 이벤트를 처리할 수 있다.
BEFORE_COMMIT 시점에서는 이벤트를 아웃박스의 형태로 저장하며, 이를 핵심 로직과 동일 트랜잭션에서 원자적으로 처리한다.
AFTER_COMMIT 시점에서는 이벤트를 외부 메시지 브로커로 발행한다. EventPoller가 존재하기 때문에 해당 과정에서의 발행 실패는 크게 신경을 안 써도 된다.
이벤트 폴러(Event Poller, MessageRelay)는 주기적으로 저장소에 저장된 이벤트 아웃박스를 조회하여 외부 메시지 브로커로 발행한다.
이를 통하여 이벤트 기반 아키텍처를 도입해 관심사의 분리, 결합도 감소라는 장점과 이벤트 재발행 가능이라는 안정성도 확보할 수 있게 된다.
향후 포스팅에서 트랜잭션 아웃박스 패턴을 적용시킨 과정을 단계적으로 나누어 서술할 예정이다.