0. 들어가기 전
취준생 시절에 간간히 컨퍼런스나 여러 블로그를 보면서 'Transactional Outbox Pattern'을 접했었습니다.
Transactional Outbox Pattern을 사용하면 순차적인 메시지 발행을 보장할 수 있다.
처음 Transactional Outbox Pattern을 공부할 당시에는 메시지를 발행해보지도 않았고 크게 와닿지 않아서 넘어갔었습니다.
하지만 이번에 서비스에 이벤트를 발행하는 Task를 맡아서 수행하면서 관련 이슈를 해결해야 했고,
이때 Transactional Outbox Pattern을 적용하고 해결하여 관련 글을 작성하고자 합니다.
(기본적인 Event Driven Architecture에 대한 개념은 생략하도록 하겠습니다!)
이론적인 내용과 함께 직접 구현했던 경험도 추가하여 작성하도록 하겠습니다.
(경험적인 내용보다는 이론적인 내용이 주가 될 것 같습니다!)
1. Transactional Outbox Pattern이란?
Transactional Outbox Event란 무엇일까요? 관련 내용은 아래 문서에 간략하게 설명되어 있습니다.
아래 문서에 소개된 내용을 좀 더 자세하게 다뤄보도록 하겠습니다.
https://microservices.io/patterns/data/transactional-outbox.html
Microservices Pattern: Pattern: Transactional outbox
First, write the message/event to a database OUTBOX table as part of the transaction that updates business objects, and then publish it to a message broker.
microservices.io
1-1. Transactional Outbox Pattern의 등장 배경 (Context)
A service command typically needs to create/update/delete aggregates in the database and send messages/events to a message broker.
For example, a service that participates in a saga needs to update business entities and send messages/events.
Similarly, a service that publishes a domain event must update an aggregate and publish an event.
문서에서는 간략하게 다음과 같은 상황을 가정하고 있습니다.
- 일반적으로 서비스에서는 DB에 CUD하는 작업과 Message Broker로 Message/Event를 Send하는 작업을 수행한다.
- Example)
- Saga 패턴(분산 트랜잭션을 다루는 패턴, 각 분산 환경별로 작업 내용을 Event로 전달)에서 Entity Update와 Message/Event를 Send한다.
- 도메인 Event Publish 시 Aggregate를 Update하고 Event를 Publish 한다.
이러한 상황은 Message System(Event)를 사용하는 아키텍쳐에서 아주 흔한 상황입니다.
도메인 이벤트 Publish와 관련해서 이해하기 쉬운 예시를 들어보면 다음과 같습니다.
- 요구 사항 : 결제 시 결제 알림을 전송한다.
- '결제'와 '알림'을 Message System을 통해 구현
- 결제 시 '결제 상태를 완료로 변경하는 DB Update' & '결제 완료 Event' Publish
이때, 해당 문서에서는 이러한 상황에 대해 다음과 같이 언급합니다.
The command must atomically update the database and send messages in order to avoid data inconsistencies and bugs.
- '반드시' 데이터 불일치와 버그를 피하기 위해 DB Update와 Message Send를 '원자적으로' 수행해야 한다.
👻 문제 상황 (일반적인 메시징 시스템 구성)
결론부터 말하면, 일반적인 메시징 시스템을 구성하면 위와 같이 DB Update와 Message Send의 원자성을 보장할 수 없습니다.
왜냐하면, 아래 그림과 같이 Message Send 작업은 DB 작업이 아니기 때문에 DB 트랜잭션으로 묶을 수 없기 때문입니다.

따라서, 다음과 같은 문제점이 발생합니다.
- DB 업데이트는 정상적으로 수행됐으나, Message 플랫폼의 이슈로 메시지 발행이 실패했을 때
- 메시지가 유실되어 데이터 정합성을 지킬 수 없다.
- 메시지 Publish 처리 중 예외는 Dead-Letter Queue와 같은 방식으로 재발행을 할 수도 있겠지만, 커넥션이 끊어지는 등 예상치 못한 오류 시에는 메시지가 유실된다.
- 분산 환경일 때 여러 대의 요청 중 일부 요청의 메시지만 발행이 실패했을 때 데이터 정합성이 깨진다.
- DB의 데이터를 롤백하기도 어렵다.
- 메시지가 유실되어 데이터 정합성을 지킬 수 없다.
위의 예시로 설명하면, 결제 상태가 완료로 변경되었지만 완료 이벤트가 발송되지 않아 알림이 생성되지 않을 것입니다.
(예시가 알림 도메인이라 비즈니스 영향이 크진 않을 수 있지만, 중요한 도메인 간의 메시지 Pub/Sub이라면 상당히 큰 오류일 것입니다.)
이러한 문제 상황을 해결하기 위해서
DB Update와 Message Send를 원자적으로 수행하는 Transactional Outbox Pattern이 등장합니다.
1-2. Transactional Outbox Pattern
그렇다면 Transactional Outbox Pattern은 어떻게 DB Update와 Message Send를 원자적으로 수행할까요?
먼저, 가장 중요한 핵심은 'Outbox' 테이블을 별도로 생성한다는 점입니다.
- DB에 'Outbox' 테이블 생성
- 해당 Outbox 테이블에 발행할 이벤트 정보 row로 저장
- DB Update와 Outbox Insert를 1개의 DB 트랜잭션으로 묶어서 수행
이렇게 DB Update와 메시지 발행 정보를 1개의 DB 트랜잭션으로 묶음으로써 원자성을 보장합니다.
Transactional Outbox Pattern을 도입한 메시지 발행 Flow는 다음과 같습니다.
- Application에서 DB Update와 메시지 발행 Outbox Table Insert 1개의 트랜잭션으로 수행
- Outbox 테이블에 쌓이는 메시지 row를 Read
- 읽은 메시지를 Message Broker에 Publish
이 과정을 도식화하면 아래 그림과 같습니다.

이렇게 Transactional Outbox Pattern을 사용하면 다음과 같은 이점이 있습니다.
- Outbox Table 자체가 Dead-Letter를 저장하는 Table이 되어 별도의 Dead-Letter Queue를 사용할 필요가 없다.
- DB Update와 메시지 발행(Outbox Insert)이 하나의 트랜잭션으로 묶이기 때문에 데이터 정합성 문제가 없다.
2. Transactional Outbox Pattern 고려할 점
위에서 간단하게 Transactional Outbox Pattern을 살펴봤을 때 간단하게 보일 수 있지만, 몇 가지 고려해야 할 점이 존재합니다.
해당 고려할 점들을 간단하게 살펴보겠습니다.
- Read한 Outbox Table Message(Row) 처리 방안
- Outbox Table의 Message(Row)를 Read하는 방식
2-1. Read한 Outbox Table Message(Row) 처리 방안
Transactional Outbox Pattern Flow에서 Outbox Table에 쌓이는 메시지를 Read하여 발행하는 것을 알 수 있습니다.
이때, 메시지 발행 후 쌓이는 메시지들을 어떻게 처리할지 다음과 같이 크게 2가지 방법이 있습니다.
- 쌓이는 메시지를 History로 남겨놓기
- 이벤트 소싱이나 이벤트를 추적하기 위한 목적
- 발행 후 메시지를 지우기
- 별도의 이벤트 추적이 필요 없을 때
저의 경우에는 발행한 이벤트의 기록을 남겨놓을 필요가 없었기 때문에 메시지를 지우는 방식을 선택했습니다.
이때, 실시간으로 Read한 데이터를 지우는 것은 위험할 수 있다고 생각합니다.
- 결국 DB Delete(Outbox Message Delete)와 Message Send가 동시에 이루어져야 함
- 이는 결국, 또 다시 두 작업의 원자성을 보장하지 못할 수 있음
- DB Delete는 되었지만 Message Send가 되지 않았다면 똑같이 메시지 유실이 될 수 있음
따라서, 저는 하루 정도의 간격을 두고 쌓인 메시지를 DB에서 삭제하는 낙관적인 방법을 선택하여 메시지를 제거했습니다.
2-2. Outbox Table의 Message(Row)를 Read하는 방식
Outbox Table에 쌓인 메시지를 Read하는 방식도 크게 다음과 같이 2가지가 존재합니다.
- Polling Publisher
- Transaction Log Tailing
🎯 Polling Publisher
Polling Publisher 방식은 간단하게 Outbox Table를 주기적으로 Polling하여 메시지를 Read하는 방식입니다.
Polling할 주기를 설정하여 Outbox Table의 발행되지 않은 메시지를 읽어서 발행합니다.
이때 Polling 시 발행되지 않은 메시지를 필터링하기 위해 메시지 발행 시 발행한 메시지 row를 삭제하거나 flag를 업데이트 합니다.
해당 방식은 구현이 간단하지만 주기적으로 DB에 부하를 줄 수 있다는 점 때문에 사용하지 않게 되었습니다.
(해당 방식으로 구현한 예제도 많은 것 같으니, 찾아보시면 좋을 것 같습니다!)
🎯 Transaction Log Tailing
Transaction Log Tailing은 Polling Publisher보다는 러닝 커브가 있고 복잡한 방법입니다.
해당 방식은 Application 단에서 DB를 조회한 결과로 메시지를 Read하는 것이 아닌,
DB단에서 생성되는 'Transaction Log'를 추적하고 데이터 변경을 감지하여 Read하는 CDC(Change Data Capture) 방식입니다.

일반적으로 CDC를 구현하기가 힘들기 때문에 CDC를 구현한 Debezium과 같은 오픈 소스를 사용합니다.
이러한 과정에서 CDC의 개념과 Debezium 등 여러 관련 개념을 학습해야 하기 때문에 러닝 커브가 있는 편입니다.
그렇지만 CDC를 한번 구성하고 나면 관리 및 모니터링이 쉽고 DB 단에서 이루어지기 때문에
직접 무거운 SQL 쿼리를 주기적으로 발생시키는 Polling 방식 보다는 DB 부하가 적을 것이라고 기대했습니다.
또, 사내에서 이미 CDC의 환경 구성이 되어 있었기 때문에 리소스가 적어서 Transaction Log Tailing 방식을 사용하게 되었습니다.
적용한 CDC(Debezium)에 대해서는 이정도로만 간략히 소개하고, 시간이 나면 자세히 포스팅으로 다뤄보도록 하겠습니다.
3. Transactional Outbox Pattern 구현
실제로 Transactional Outbox Pattern을 구현한 내용을 마지막으로 글을 마무리하도록 하겠습니다.
위에서 설명했듯이 저는 다음과 같이 Transactional Outbox Pattern을 구현했습니다.
- 기존 서비스 DB에 'Outbox' Table 구성
- Transaction Log Tailing을 위해 CDC 오픈소스인 Debezium 적용
사실 구현이라고 거창하게 말했지만, 어떻게 Outbox 테이블을 구성했는지에 대해서만 언급하게 될 것 같습니다.
Debezium을 적용한 설정들은 다루기에는 너무 딥하고 많기 때문에 시간이 나면 따로 포스팅으로 다뤄보도록 하겠습니다.
3-1. Outbox Table Schema
Outbox Table에는 Debezium(Message Relay)이 읽고 발행할 메시지의 정보를 담아야 합니다.
이는 Event의 Spec과도 관련이 있는데, 공통된 Spec을 사용하기 위해 'Cloud Events'라는 Event Spec을 기반으로 구성했습니다.
CloudEvents
cloudevents.io
Kafka Message의 Format은 크게 다음과 같이 3가지가 존재합니다.
- Key : Kafka Message를 식별하기 위한 키
- Value (Payload) : Message Payload
- Header : Message의 메타 데이터를 담는 헤더
CREATE TABLE outbox
(
event_id uuid PRIMARY KEY,
created_at timestamptz NOT NULL,
event_type varchar(126) NOT NULL,
aggregate_type varchar(126) NOT NULL,
aggregate_id jsonb NOT NULL,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
payload jsonb NOT NULL DEFAULT '{}'::jsonb
);
- event_id : 발행할 이벤트를 식별하기 위한 UUID, PK
- created_at : 이벤트 발생 일시
- aggregate 관련 : 발생한 도메인 type과 id 저장, 어느 Aggregate인지, 어떤 리소스인지 식별하기 위해 사용
- metadata : Kafka Message의 헤더에 담을 추가 정보(메타데이터)
- payload : Kafka Message의 Payload(Value)에 담을 정보
해당 Outbox 테이블의 정보를 기반으로 Debezium에서 Message를 생성하여 발행하도록 했습니다.
최종적으로 구성한 Transactional Outbox Pattern의 구성도는 다음과 같습니다.

4. 결론
Transactional Outbox Pattern을 사용하여 다음과 같은 문제를 해결할 수 있었습니다.
- DB Update와 Message Send를 원자적으로 수행
- DB Update 이후 Message가 발행되지 않아도 Outbox 테이블에서 재발행이 가능하므로 데이터 정합성 보장(최종적 일관성)
- 분산 환경에서도 공통 메시지 Table인 Outbox Table로 메시지 순서를 보장할 수 있음
결론적으로는 Transactional Outbox Pattern을 사용해서 위와 같이 간단한 이점을 얻을 수 있었습니다.
다소 간단해보이지만 이벤트로 비즈니스를 처리하는 EDA 관점에서 아주 중요한 이점이라고 생각이 드는 것 같습니다.
해당 구현 과정에서 추가로 공부해야 할 부분은 다음과 같습니다.
- CloudEvents : Event Spec 구성 시 사용한 well-known Event Specification (공통 Event Spec 구성을 위해)
- Debezium : DB의 Transaction Log를 기반으로 변경된 데이터를 감지하는 CDC 라이브러리 (Kafka Connector 기반)
이와 관련해서는 시간이 된다면 다음에 포스팅해보도록 하겠습니다.
Reference
https://microservices.io/patterns/data/transactional-outbox.html
Microservices Pattern: Pattern: Transactional outbox
First, write the message/event to a database OUTBOX table as part of the transaction that updates business objects, and then publish it to a message broker.
microservices.io
https://ridicorp.com/story/transactional-outbox-pattern-ridi/
Transactional Outbox 패턴으로 메시지 발행 보장하기 - 리디주식회사
Event-Driven Architecture에서 메시지 발행의 신뢰성을 보장하는 방법은 무엇일까요? 리디 서비스에 Transactional Outbox 패턴을 도입한 배경과 그 과정에서 얻은 배움을 공유합니다.
ridicorp.com
'Kafka' 카테고리의 다른 글
[Kafka] Apache Kafka 공식문서 살펴보기 (Design, 심화 이론) (2) | 2024.12.21 |
---|---|
[Kafka] Apache Kafka 공식 문서 살펴보기 (기본 이론) (1) | 2024.11.30 |
0. 들어가기 전
취준생 시절에 간간히 컨퍼런스나 여러 블로그를 보면서 'Transactional Outbox Pattern'을 접했었습니다.
Transactional Outbox Pattern을 사용하면 순차적인 메시지 발행을 보장할 수 있다.
처음 Transactional Outbox Pattern을 공부할 당시에는 메시지를 발행해보지도 않았고 크게 와닿지 않아서 넘어갔었습니다.
하지만 이번에 서비스에 이벤트를 발행하는 Task를 맡아서 수행하면서 관련 이슈를 해결해야 했고,
이때 Transactional Outbox Pattern을 적용하고 해결하여 관련 글을 작성하고자 합니다.
(기본적인 Event Driven Architecture에 대한 개념은 생략하도록 하겠습니다!)
이론적인 내용과 함께 직접 구현했던 경험도 추가하여 작성하도록 하겠습니다.
(경험적인 내용보다는 이론적인 내용이 주가 될 것 같습니다!)
1. Transactional Outbox Pattern이란?
Transactional Outbox Event란 무엇일까요? 관련 내용은 아래 문서에 간략하게 설명되어 있습니다.
아래 문서에 소개된 내용을 좀 더 자세하게 다뤄보도록 하겠습니다.
https://microservices.io/patterns/data/transactional-outbox.html
Microservices Pattern: Pattern: Transactional outbox
First, write the message/event to a database OUTBOX table as part of the transaction that updates business objects, and then publish it to a message broker.
microservices.io
1-1. Transactional Outbox Pattern의 등장 배경 (Context)
A service command typically needs to create/update/delete aggregates in the database and send messages/events to a message broker.
For example, a service that participates in a saga needs to update business entities and send messages/events.
Similarly, a service that publishes a domain event must update an aggregate and publish an event.
문서에서는 간략하게 다음과 같은 상황을 가정하고 있습니다.
- 일반적으로 서비스에서는 DB에 CUD하는 작업과 Message Broker로 Message/Event를 Send하는 작업을 수행한다.
- Example)
- Saga 패턴(분산 트랜잭션을 다루는 패턴, 각 분산 환경별로 작업 내용을 Event로 전달)에서 Entity Update와 Message/Event를 Send한다.
- 도메인 Event Publish 시 Aggregate를 Update하고 Event를 Publish 한다.
이러한 상황은 Message System(Event)를 사용하는 아키텍쳐에서 아주 흔한 상황입니다.
도메인 이벤트 Publish와 관련해서 이해하기 쉬운 예시를 들어보면 다음과 같습니다.
- 요구 사항 : 결제 시 결제 알림을 전송한다.
- '결제'와 '알림'을 Message System을 통해 구현
- 결제 시 '결제 상태를 완료로 변경하는 DB Update' & '결제 완료 Event' Publish
이때, 해당 문서에서는 이러한 상황에 대해 다음과 같이 언급합니다.
The command must atomically update the database and send messages in order to avoid data inconsistencies and bugs.
- '반드시' 데이터 불일치와 버그를 피하기 위해 DB Update와 Message Send를 '원자적으로' 수행해야 한다.
👻 문제 상황 (일반적인 메시징 시스템 구성)
결론부터 말하면, 일반적인 메시징 시스템을 구성하면 위와 같이 DB Update와 Message Send의 원자성을 보장할 수 없습니다.
왜냐하면, 아래 그림과 같이 Message Send 작업은 DB 작업이 아니기 때문에 DB 트랜잭션으로 묶을 수 없기 때문입니다.

따라서, 다음과 같은 문제점이 발생합니다.
- DB 업데이트는 정상적으로 수행됐으나, Message 플랫폼의 이슈로 메시지 발행이 실패했을 때
- 메시지가 유실되어 데이터 정합성을 지킬 수 없다.
- 메시지 Publish 처리 중 예외는 Dead-Letter Queue와 같은 방식으로 재발행을 할 수도 있겠지만, 커넥션이 끊어지는 등 예상치 못한 오류 시에는 메시지가 유실된다.
- 분산 환경일 때 여러 대의 요청 중 일부 요청의 메시지만 발행이 실패했을 때 데이터 정합성이 깨진다.
- DB의 데이터를 롤백하기도 어렵다.
- 메시지가 유실되어 데이터 정합성을 지킬 수 없다.
위의 예시로 설명하면, 결제 상태가 완료로 변경되었지만 완료 이벤트가 발송되지 않아 알림이 생성되지 않을 것입니다.
(예시가 알림 도메인이라 비즈니스 영향이 크진 않을 수 있지만, 중요한 도메인 간의 메시지 Pub/Sub이라면 상당히 큰 오류일 것입니다.)
이러한 문제 상황을 해결하기 위해서
DB Update와 Message Send를 원자적으로 수행하는 Transactional Outbox Pattern이 등장합니다.
1-2. Transactional Outbox Pattern
그렇다면 Transactional Outbox Pattern은 어떻게 DB Update와 Message Send를 원자적으로 수행할까요?
먼저, 가장 중요한 핵심은 'Outbox' 테이블을 별도로 생성한다는 점입니다.
- DB에 'Outbox' 테이블 생성
- 해당 Outbox 테이블에 발행할 이벤트 정보 row로 저장
- DB Update와 Outbox Insert를 1개의 DB 트랜잭션으로 묶어서 수행
이렇게 DB Update와 메시지 발행 정보를 1개의 DB 트랜잭션으로 묶음으로써 원자성을 보장합니다.
Transactional Outbox Pattern을 도입한 메시지 발행 Flow는 다음과 같습니다.
- Application에서 DB Update와 메시지 발행 Outbox Table Insert 1개의 트랜잭션으로 수행
- Outbox 테이블에 쌓이는 메시지 row를 Read
- 읽은 메시지를 Message Broker에 Publish
이 과정을 도식화하면 아래 그림과 같습니다.

이렇게 Transactional Outbox Pattern을 사용하면 다음과 같은 이점이 있습니다.
- Outbox Table 자체가 Dead-Letter를 저장하는 Table이 되어 별도의 Dead-Letter Queue를 사용할 필요가 없다.
- DB Update와 메시지 발행(Outbox Insert)이 하나의 트랜잭션으로 묶이기 때문에 데이터 정합성 문제가 없다.
2. Transactional Outbox Pattern 고려할 점
위에서 간단하게 Transactional Outbox Pattern을 살펴봤을 때 간단하게 보일 수 있지만, 몇 가지 고려해야 할 점이 존재합니다.
해당 고려할 점들을 간단하게 살펴보겠습니다.
- Read한 Outbox Table Message(Row) 처리 방안
- Outbox Table의 Message(Row)를 Read하는 방식
2-1. Read한 Outbox Table Message(Row) 처리 방안
Transactional Outbox Pattern Flow에서 Outbox Table에 쌓이는 메시지를 Read하여 발행하는 것을 알 수 있습니다.
이때, 메시지 발행 후 쌓이는 메시지들을 어떻게 처리할지 다음과 같이 크게 2가지 방법이 있습니다.
- 쌓이는 메시지를 History로 남겨놓기
- 이벤트 소싱이나 이벤트를 추적하기 위한 목적
- 발행 후 메시지를 지우기
- 별도의 이벤트 추적이 필요 없을 때
저의 경우에는 발행한 이벤트의 기록을 남겨놓을 필요가 없었기 때문에 메시지를 지우는 방식을 선택했습니다.
이때, 실시간으로 Read한 데이터를 지우는 것은 위험할 수 있다고 생각합니다.
- 결국 DB Delete(Outbox Message Delete)와 Message Send가 동시에 이루어져야 함
- 이는 결국, 또 다시 두 작업의 원자성을 보장하지 못할 수 있음
- DB Delete는 되었지만 Message Send가 되지 않았다면 똑같이 메시지 유실이 될 수 있음
따라서, 저는 하루 정도의 간격을 두고 쌓인 메시지를 DB에서 삭제하는 낙관적인 방법을 선택하여 메시지를 제거했습니다.
2-2. Outbox Table의 Message(Row)를 Read하는 방식
Outbox Table에 쌓인 메시지를 Read하는 방식도 크게 다음과 같이 2가지가 존재합니다.
- Polling Publisher
- Transaction Log Tailing
🎯 Polling Publisher
Polling Publisher 방식은 간단하게 Outbox Table를 주기적으로 Polling하여 메시지를 Read하는 방식입니다.
Polling할 주기를 설정하여 Outbox Table의 발행되지 않은 메시지를 읽어서 발행합니다.
이때 Polling 시 발행되지 않은 메시지를 필터링하기 위해 메시지 발행 시 발행한 메시지 row를 삭제하거나 flag를 업데이트 합니다.
해당 방식은 구현이 간단하지만 주기적으로 DB에 부하를 줄 수 있다는 점 때문에 사용하지 않게 되었습니다.
(해당 방식으로 구현한 예제도 많은 것 같으니, 찾아보시면 좋을 것 같습니다!)
🎯 Transaction Log Tailing
Transaction Log Tailing은 Polling Publisher보다는 러닝 커브가 있고 복잡한 방법입니다.
해당 방식은 Application 단에서 DB를 조회한 결과로 메시지를 Read하는 것이 아닌,
DB단에서 생성되는 'Transaction Log'를 추적하고 데이터 변경을 감지하여 Read하는 CDC(Change Data Capture) 방식입니다.

일반적으로 CDC를 구현하기가 힘들기 때문에 CDC를 구현한 Debezium과 같은 오픈 소스를 사용합니다.
이러한 과정에서 CDC의 개념과 Debezium 등 여러 관련 개념을 학습해야 하기 때문에 러닝 커브가 있는 편입니다.
그렇지만 CDC를 한번 구성하고 나면 관리 및 모니터링이 쉽고 DB 단에서 이루어지기 때문에
직접 무거운 SQL 쿼리를 주기적으로 발생시키는 Polling 방식 보다는 DB 부하가 적을 것이라고 기대했습니다.
또, 사내에서 이미 CDC의 환경 구성이 되어 있었기 때문에 리소스가 적어서 Transaction Log Tailing 방식을 사용하게 되었습니다.
적용한 CDC(Debezium)에 대해서는 이정도로만 간략히 소개하고, 시간이 나면 자세히 포스팅으로 다뤄보도록 하겠습니다.
3. Transactional Outbox Pattern 구현
실제로 Transactional Outbox Pattern을 구현한 내용을 마지막으로 글을 마무리하도록 하겠습니다.
위에서 설명했듯이 저는 다음과 같이 Transactional Outbox Pattern을 구현했습니다.
- 기존 서비스 DB에 'Outbox' Table 구성
- Transaction Log Tailing을 위해 CDC 오픈소스인 Debezium 적용
사실 구현이라고 거창하게 말했지만, 어떻게 Outbox 테이블을 구성했는지에 대해서만 언급하게 될 것 같습니다.
Debezium을 적용한 설정들은 다루기에는 너무 딥하고 많기 때문에 시간이 나면 따로 포스팅으로 다뤄보도록 하겠습니다.
3-1. Outbox Table Schema
Outbox Table에는 Debezium(Message Relay)이 읽고 발행할 메시지의 정보를 담아야 합니다.
이는 Event의 Spec과도 관련이 있는데, 공통된 Spec을 사용하기 위해 'Cloud Events'라는 Event Spec을 기반으로 구성했습니다.
CloudEvents
cloudevents.io
Kafka Message의 Format은 크게 다음과 같이 3가지가 존재합니다.
- Key : Kafka Message를 식별하기 위한 키
- Value (Payload) : Message Payload
- Header : Message의 메타 데이터를 담는 헤더
CREATE TABLE outbox
(
event_id uuid PRIMARY KEY,
created_at timestamptz NOT NULL,
event_type varchar(126) NOT NULL,
aggregate_type varchar(126) NOT NULL,
aggregate_id jsonb NOT NULL,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
payload jsonb NOT NULL DEFAULT '{}'::jsonb
);
- event_id : 발행할 이벤트를 식별하기 위한 UUID, PK
- created_at : 이벤트 발생 일시
- aggregate 관련 : 발생한 도메인 type과 id 저장, 어느 Aggregate인지, 어떤 리소스인지 식별하기 위해 사용
- metadata : Kafka Message의 헤더에 담을 추가 정보(메타데이터)
- payload : Kafka Message의 Payload(Value)에 담을 정보
해당 Outbox 테이블의 정보를 기반으로 Debezium에서 Message를 생성하여 발행하도록 했습니다.
최종적으로 구성한 Transactional Outbox Pattern의 구성도는 다음과 같습니다.

4. 결론
Transactional Outbox Pattern을 사용하여 다음과 같은 문제를 해결할 수 있었습니다.
- DB Update와 Message Send를 원자적으로 수행
- DB Update 이후 Message가 발행되지 않아도 Outbox 테이블에서 재발행이 가능하므로 데이터 정합성 보장(최종적 일관성)
- 분산 환경에서도 공통 메시지 Table인 Outbox Table로 메시지 순서를 보장할 수 있음
결론적으로는 Transactional Outbox Pattern을 사용해서 위와 같이 간단한 이점을 얻을 수 있었습니다.
다소 간단해보이지만 이벤트로 비즈니스를 처리하는 EDA 관점에서 아주 중요한 이점이라고 생각이 드는 것 같습니다.
해당 구현 과정에서 추가로 공부해야 할 부분은 다음과 같습니다.
- CloudEvents : Event Spec 구성 시 사용한 well-known Event Specification (공통 Event Spec 구성을 위해)
- Debezium : DB의 Transaction Log를 기반으로 변경된 데이터를 감지하는 CDC 라이브러리 (Kafka Connector 기반)
이와 관련해서는 시간이 된다면 다음에 포스팅해보도록 하겠습니다.
Reference
https://microservices.io/patterns/data/transactional-outbox.html
Microservices Pattern: Pattern: Transactional outbox
First, write the message/event to a database OUTBOX table as part of the transaction that updates business objects, and then publish it to a message broker.
microservices.io
https://ridicorp.com/story/transactional-outbox-pattern-ridi/
Transactional Outbox 패턴으로 메시지 발행 보장하기 - 리디주식회사
Event-Driven Architecture에서 메시지 발행의 신뢰성을 보장하는 방법은 무엇일까요? 리디 서비스에 Transactional Outbox 패턴을 도입한 배경과 그 과정에서 얻은 배움을 공유합니다.
ridicorp.com
'Kafka' 카테고리의 다른 글
[Kafka] Apache Kafka 공식문서 살펴보기 (Design, 심화 이론) (2) | 2024.12.21 |
---|---|
[Kafka] Apache Kafka 공식 문서 살펴보기 (기본 이론) (1) | 2024.11.30 |