- Home >
- web >
- Transactional Outbox Pattern 도입기 1 - Event Driven Architecture - Transactional Outbox Pattern의 연관성
Transactional Outbox Pattern 도입기 1 - Event Driven Architecture - Transactional Outbox Pattern의 연관성
Event Driven Architecture에서 Transactional Outbox Pattern이 사용되는 이유
여행 기록 관리 플랫폼 ‘여기가’ 프로젝트를 진행하며, 비밀번호 초기화 이메일 전송 기능을 담당하게 되었다.
기능 구현을 완료한 뒤 이메일 전송 보장 기능을 도입해야하는 추가 이슈가 발생하였다. 여러 방법들을 모색하던 중 트랜잭션 아웃박스 패턴(Transactional Outbox Pattern)을 알게되었다.
트랜잭션 아웃박스 패턴에 대한 기술 블로그 및 예제 코드를 찾아보면 EDA(Event Driven Architecture)와 함께 사용되는 경우를 많이 보았다.
학부생 규모의 프로젝트를 많이 진행하면서 모놀리식 단일 모듈 구조를 많이 사용하였다. 따라서, 이벤트를 활용해본 경험이 거의 전무하였기에 “트랜잭션 아웃박스 패턴을 사용할 때, 왜 EDA도 함께 사용하는가?” 라는 의문이 들었다.
결론부터 말하자면, 트랜잭션 아웃박스 패턴을 사용할 때 EDA를 같이 사용하는 것이 아니라 EDA를 사용할 때 트랜잭션 아웃박스 패턴을 사용하는 것이다.
앞서 이벤트를 통한 시스템 결합도 낮추기 포스팅에서 이벤트 기반 아키텍처가 왜 필요한지에 대해서 다루었다.
이벤트는 시스템의 결합도를 낮추고, 장애를 격리시킨다는 장점으로 인하여 MSA 환경에서 많이 사용된다.
이메일 전송과 같이 외부 API를 사용하거나 입출력에 많은 시간이 소요되는 작업은 이벤트 기반 아키텍처를 일부 도입하여 해당 기능을 수행하는 주체를 분리하는 것이 성능 및 처리 효율성 향상에 큰 도움이 된다.
그러나, 이벤트를 통한 처리 시 발생하는 문제가 존재한다.
- 메시지 전송 전 유실 문제
- 핵심 로직과 메시지 발송이 같은 트랜잭션으로 묶여야하는 문제
위 2가지 문제가 EDA에서 Transactional Outbox Pattern을 사용하게 되는 이유이다.
메시지 유실 문제

메시지 브로커는 내부에서 재시도 로직이나 메시지 영속화 등 다양한 기능을 제공한다. 즉, 메시지 브로커로 메시지(이벤트)가 도착하면 내부에서 제공하는 기능으로 메시지를 기억하거나 재시도 등의 작업을 수행할 수 있다.
그러나, 서버 애플리케이션에서 메시지 브로커로 메시지를 전송하는 도중에 메시지가 유실되어버린다면 더이상 처리할 수 없게 된다.
애플리케이션 내부에서 처리되는 이벤트라면 처리 흐름 안에 이벤트가 여전히 존재하기때문에 유실될 가능성이 존재하지 않는다. 그러나, MSA 환경에서는 애플리케이션이 독립적으로 구성되어 있다. 주로 메시지 브로커를 통해 외부로 이벤트를 전달하는 이유는 해당 이벤트를 처리해야하는 부가 로직이 존재하기 때문이다.
따라서, 각 애플리케이션 간 데이터를 이벤트 기반으로 주고받으며 Kafka, RabbitMQ, AWS SNS/SQS와 같은 외부 메시지 브로커를 이용한다. 외부 메시지 브로커가 일시적으로 장애가 발생하거나 메시지큐(버스)가 가득 차 더이상 메시지를 처리할 수 없을 때 단순히 이벤트를 발송하기만 하면 해당 이벤트는 유실되어 버린다.
핵심 로직과 메시지 발송이 같은 트랜잭션으로 묶여야하는 문제
비밀번호 초기화 기능의 핵심 로직인 ‘임시 비밀번호 발급’과 부가 로직인 ‘비밀번호 초기화 이메일 전송’은 하나의 트랜잭션으로 묶어 실행되어야지만 정상적으로 동작하는 기능이다.
비밀번호 초기화 이메일 전송은 핵심 로직에 포함되기는 하나, 이메일 전송의 측면에서 부가 로직으로 설명하였다.
이렇듯 대부분 DB를 업데이트하는 작업은 트랜잭션과 함께 메시지를 발행해야한다.
만약, DB 업데이트와 메시지 발행을 하나의 트랜잭션으로 묶지 않으면 다음과 같은 문제가 발생할 수 있다.
- 임시 비밀번호 갱신 실패, 비밀번호 초기화 메일 전송 성공
- 임시 비밀번호 갱신 성공, 비밀번호 초기화 메일 전송 실패
비밀번호 초기화 메일 전송 후 에러가 발생하여 임시 비밀번호 갱신에는 실패하여 임시 비밀번호 갱신 쿼리가 롤백된다면, 사용자들은 초기화되었다는 비밀번호로 로그인을 시도하겠지만 로그인은 실패하게 된다.
이와 반대로, 임시 비밀번호는 갱신하였는데, 비밀번호 초기화 메일이 전송되지 않았다면 사용자들은 새로 바뀐 비밀번호를 알지 못해 로그인을 하지 못하는 상황이 발생할 것이다.
이 문제를 해결하는 방법이 트랜잭션 아웃박스 패턴(Transactional Outbox Pattern)이다.
Transactional Outbox Pattern
이벤트 기반 아키텍처에서 외부 메시지 브로커를 사용하여 다른 애플리케이션으로 메시지를 전달할 경우 ‘메시지 전송 전 유실’, ‘핵심 로직과 메시지 발송이 동일 트랜잭션 내 위치’와 같은 문제가 존재한다는 것을 확인하였다.
이를 해결하기 위한 방법이 트랜잭션 아웃박스 패턴(Transactional Outbox Pattern)이다.

트랜잭션 아웃박스 패턴의 핵심은 이벤트를 데이터베이스에 아웃박스(Outbox)로 저장하는 것이다. 또한, 이 아웃박스의 저장을 하나의 트랜잭션으로 핵심 로직의 실행과 하나의 트랜잭션으로 묶는다.
이를 통하여 핵심 로직이 완전하게 실행된 경우에만 아웃박스가 존재하게 된다. 따라서, 이후 서버에서는 메시지 릴레이(Message Relay)를 통하여 주기적으로 아웃박스를 조회하여 메시지 브로커로 발행한다.
Outbox
아웃박스(Outbox)는 사전적인 의미로 ‘전송 중이거나 전송에 실패한 메시지가 점시 머무는 보관함’을 의미한다.
해당 의미 그대로, 외부로 발행해야할 이벤트들이 아웃박스 형태로 변환되어 데이터베이스에 잠시 머무르게 되는 것이다.
이벤트의 저장
트랜잭션 아웃박스 패턴의 핵심은 이벤트를 저장하는 것이다. 정확하게는 아웃박스의 형태로 저장하는 것이다.
아웃박스를 저장하기 위한 이벤트 저장소(데이터베이스)를 고민해야한다.
아웃박스에는 이벤트의 정보와 아웃박스 자체에 대한 메타 데이터 등에 대한 내용만 가지고 있기 때문에 작은 단위로 저장되며, 이벤트는 고속으로 처리되어야 하기 때문에 RDBMS가 아닌 다른 데이터베이스를 사용해야한다고 생각할 수 있다.
그러나, 우아한 기술 블로그 - 회원 시스템 이벤트기반 아키텍처 구축하기 포스팅에서 같은 RDBMS를 사용해도 괜찮다는 내용을 확인할 수 있었다. 이벤트 저장소와 도메인 저장소를 다른 종류의 데이터베이스로 사용할 경우, 두 저장소에 대한 트랜잭션을 처리해야하나 다종 데이터베이스의 분산 트랜잭션을 구현하는 것은 굉장히 어려운 일이라고 설명하고 있다.
따라서, 도메인 저장소와 이벤트 저장소를 동일한 DBMS로 사용한다면 트랜잭션 처리는 DBMS에 믿고 맡길 수 있으며, 단일 저장소를 사용하여 쓰기량 및 읽기량에 대한 성느적 리스크는 스케일업/아웃 혹은 샤딩을 하는 방식으로 확장하여 대응 가능하다고 한다.
필자 또한 분산 트랜잭션에 관한 어려움과 현재 지식의 부족함을 고려하여 단일 저장소(RDBMS, MySQL)을 사용하여 이벤트(아웃박스)를 저장하기로 하였다.
이벤트의 상태
핵심 로직을 실행하는 애플리케이션에서는 핵심 로직 발생에 대한 이벤트를 발행해야 한다. 즉, 아래와 같은 2가지 역할은 수행을 해야한다.
- 이벤트의 저장
- 이벤트의 발행
따라서, 이벤트는 막 생성되어 저장된 이벤트 / 메시지 브로커로 발행된 이벤트 / 메시지 브로커로 발행에 실패한 이벤트 등 다양한 상태가 존재하게 된다.
가장 중요한 것은 이벤트의 발행 여부이다.
EDA를 통하여 모듈 간 강결합성을 낮춰 관심사의 분리, 장애 격리의 장점을 얻었다.
비즈니스 도메인의 부가 로직에 대한 관심은 낮아졌지만, 부가 로직 실행이 보장되어야하기에 이벤트 발행 책임이 생긴다.
이벤트의 상태는 이벤트 발행/재발행에 있어 필수적으로 필요한 속성이다.
메시지 릴레이(Message Relay)를 통해서 이벤트 아웃박스를 조회하기 위해서는 아직 발행되지 않은 이벤트들만 조회해야 한다. 또한, 이미 발행된 이벤트는 삭제를 하고, 발행에 실패한 이벤트는 재발행을 시도하는 등의 처리를 위해서 이벤트 아웃박스의 상태가 필요하다.
트랜잭션 시점(Before/After Commit)에 따른 이벤트 처리 분리
트랜잭션 아웃박스 패턴의 핵심 개념 중 하나는 비즈니스 로직과 이벤트 기록을 하나의 트랜잭션으로 묶어 원자적 수행을 하는 것이다.
SpringEvent를 사용할 경우 커밋, 롤백 등 트랜잭션 전/후 등의 시점에서 Event를 처리할 수 있는 어노테이션 @TransactionalEventListener를 제공한다.
이 중 ‘커밋 이전 시점(Before Commit)’과 ‘커밋 이후 시점(After Commit)’에서 이벤트를 처리할 수 있다.
- 이벤트 기록:
TransactionPhase.BEFORE_COMMIT - 이벤트 발행:
TransactionPhase.AFTER_COMMIT
이벤트 발행의 경우에는 메시지 릴레이를 통해서 조회를 하여 발행하지만, SpringEvent에서 제공하는 After Commit 시점의 이벤트 리스너를 통해서 메시지를 바로 발행할 수도 있다.
TransactionPhase.BEFORE_COMMIT 시점에서는 이벤트를 이벤트 아웃박스 저장소에 기록(저장)한다.
만약 이벤트 저장소에 기록이 실패하게 되면 트랜잭션 전체가 실패하게 되며 롤백되게 된다.
TransactionPhase.AFTER_COMMIT 시점에서는 이벤트를 메시지 브로커로 발행한다.
After Commit 시점에서 이벤트를 발행하지 않아도 무방하다. SpringEvent는 @TransactionalEventListener를 통해서 트랜잭션 시점에 따라 제어가 가능하지만, 다른 프레임워크를 사용하거나 SpringEvent를 이용하지 않는다면 트랜잭션 시점에 따른 처리가 불가능할 수도 있기 때문에 Microservice Architecture - Pattern: Transactional outbox에서도 메시지 릴레이를 통한 주기적 폴링 방식으로 이벤트 아웃박스를 조회해 발행하는 방식을 소개한다.
After Commit 시점에서 이벤트 리스너를 통해 이벤트 발행 로직을 바로 수행한다면, 메시지 릴레이에서 조회할 이벤트 아웃박스의 양이 줄어들게 된다. 또한, 이벤트 발행이 실패하더라도 이미 저장소에 이벤트 아웃박스가 저장되어 있기 때문에 향후 폴링 시점에서 재발행이 가능하다. 따라서, After Commit 시점에서 이벤트 발행 로직을 수행하는 방향으로 설계하였다.
이벤트 폴링
트랜잭션 아웃박스 패턴에서 데이터베이스에 저장된 이벤트 아웃박스를 주기적으로 조회(Polling)하여 메시지 브로커로 이벤트를 발행한다.
메시지 릴레이(Message Relay)는 이벤트 아웃박스의 주기적인 조회를 담당하는 이벤트 폴러(Event Poller)와 외부로 이벤트를 발행하는 외부 이벤트 발행기(External Event Publisher)가 합쳐진 개념이다.
필자는 실제 구현 상에 있어서 이벤트 폴러와 외부 이벤트 발행기를 분리하여 구현하였다.
이벤트 폴러의 경우에는 상태에 따라 조회해야할 쿼리가 다양하다. 또한, 외부 이벤트 발행기도 After Commit 시점에서도 동시에 사용될 수 있다. 따라서, 각 클래스간 응집도와 재사용성을 고려하여 분리하여 구현하였다.
이에 관한 자세한 내용은 차후 포스팅에서 다룰 예정이다.
Summary
MSA 환경에서 Event Driven Architecture를 사용한다면 메시지 브로커를 통해 다른 모듈로 이벤트를 전달하게 된다. 이를 통해 핵심 비즈니스 도메인에서 부가 로직과의 결합도를 낮추어 관심사를 분리할 수 있다.
어떤 부가 로직이 실행될지에는 관심을 가질 필요는 없지만, 부가 로직이 실행되기 위해서는 메시지 발행이 보장되어야 한다.
이때, 메시지 유실과 핵심 로직과 메시지 발행의 원자적 실행을 위해 트랜잭션 아웃박스 패턴(Transactional Outbox Pattern)을 사용하게 된다.
트랜잭션 아웃박스 패턴의 핵심은 이벤트를 저장소에 저장하는 것이다.
이벤트를 저장소에 저장하기때문에 메시지 유실 문제를 해결하고, 비즈니스 도메인 수정과 메시지 발행을 하나의 트랜잭션으로 묶어 원자적인 연산을 수행하도록 한다.
이벤트는 아웃박스(Outbox)의 형태로 저장된다. 아웃박스는 ‘보낼 편지함’이라는 사전적 의미처럼 발행을 위해 임시로 저장된 이벤트들을 나타내는 개념이다. 아웃박스는 발행/재발행 등을 처리하기 위하여 상태를 기록해야 한다.
SpringEvent는 @TransactionalEventListener를 통해 트랜잭션 시점에 따라 이벤트를 처리할 수 있다. BEFORE_COMMIT 시점에서는 이벤트를 아웃박스의 형태로 저장하며, AFTER_COMMIT 시점에서는 이벤트를 발행한다.
이벤트 폴러(Event Poller)는 주기적으로 저장소에 저장된 이벤트 아웃박스를 조회하여 외부 이벤트 발행기(External Event Publisher)를 통하여 외부 메시지 브로커로 이벤트를 발행한다.
이를 통하여 이벤트 기반 아키텍처에서 관심사의 분리, 결합도 감소와 전송 보장이라는 장점을 동시에 가질 수 있게 된다.
향후 포스팅에서 트랜잭션 아웃박스 패턴을 적용시킨 과정을 단계적으로 나누어 서술할 예정이다.