1. 개요
MSA에서는 서비스 간 결합도가 낮아야 하기 때문에 데이터 송수신 방법으로 Messaging System을 사용합니다.
마이크로서비스 간 비동기 처리 시에 보통 Kafka나 RabbitMQ 같은 Message Broker를 사용하여 Messaging을 구현하는데, Messaging에서 관건은 DB를 원자적으로 업데이트 하고 Message를 발생하는 일이죠.
만약 DB에 데이터 변경이 성공했지만 이벤트 발행에 실패하거나 혹은 이벤트 발행은 성공했지만 DB 반영은 실패한다면..?
서비스 간 데이터 일관성이 깨지고 말겠죠. 분산 트랜잭션 처리에서 가장 골치 아픈 건 데이터의 일관성을 보장하는 문제가 아닐까 싶습니다.
그럼 일관성을 유지하기 위해서는 어떻게 구현하는 것이 좋을까요?
2. Saga Pattern
3. Transactional Outbox Pattern
간단하게는 Message Queue로 DB Table을 사용하는 것을 말합니다.
RDBMS 기반의 애플리케이션이라면 Transactional Outbox Pattern이 가장 접근하기 쉬운 방법입니다.
1) Message를 보내는 서비스에 OUTBOX라는 DB Table을 만들고, 비즈니스 객체를 생성, 수정, 삭제하는 DB 트랜잭션의 일부로 OUTBOX Table 에 Message를 삽입합니다.
로컬 ACID 트랜잭션이기 때문에 원자성은 자동 보장되게 됩니다. (* 여기서 OUTBOX Table은 임시 MQ 역할을 합니다.)
2) 서비스는 Message를 DB 업데이트 트랜잭션에 태워 OUTBOX Table에 INSERT하는 식으로 Message를 확실하게 발행합니다.
Message Relay는 OUTBOX Table에서 Message를 읽어 Message Broker에 발행합니다.
이렇게 이벤트나 Message를 DB에 있는 OUTBOX에 저장하여 DB 트랜잭션의 일부로 발행하는 것입니다.
OUTBOX Table은 DB에 위치하고 있기 때문에 DB 트랜잭션으로 다룰 수 있다. 따라서 Message 발행에 시차가 좀 생기겠지만 결과적으로 일관성을 유지할 수 있게 됩니다.
* 참고 : Outbox Table 은 다음과 같은 Structure를 가집니다.
Column | Type | Modifiers
----------------+---------------------------+-----------
id | uuid | not null
aggregatetype | character varying(255) | not null
aggregateid | character varying(255) | not null
type | character varying(255) | not null
payload | jsonb | not null
4. 흐름 (예)
- 서비스에서 Local DB 테이블 및 Outbox 테이블에 저장 (Outbox 용도는 타 서비스에 데이터를 전송하기 위한 별도의 테이블이다)
- Outbox 테이블의 트랜잭션 로그를 CDC가 읽어 해당 데이터를 메시지 브로커에 전달
- 타 서비스 DB 동기화
5. 분산 트랜잭션 처리를 위한 방안?
데이터 정합성을 맞추기 위해서는,
- 대기 또는 실패 시에 다시 호출
- 배치 작업 실패 시 일괄 취소
- 보상 트랜잭션 처리 (Saga 패턴과 관련됨)