juchanei

Transactional outbox


https://microservices.io/patterns/data/transactional-outbox.html을 번역한 글입니다.

Also known as

Application events

Context

전형적인 서비스 커맨드는 데이터베이스를 업데이트 하는 동시에 메시지/이벤트를 발송할 필요가 있다. 예를 들면, saga에 참여하는 서비스는 데이터베이스 업데이트와 메시지/이벤트 발행을 원자적으로 수행해야 한다. 도메인 이벤트(domain event)를 발행하는 서비스 역시 aggregate을 업데이트하고 이벤트를 발행하는 동작을 원자적으로 수행해야 한다.

서비스 커맨드는 일반적으로 데이터베이스를 업데이트하는 동시에 메시지/이벤트를 발송한다. 그러나 메시지 브로커는 보통 전통적인 분산 트랜잭션(Two phase commit, 2PC)를 지원하지 않기 때문에, 데이터베이스와 메시지 브로커에 걸쳐 업데이트와 메시지/이벤트 발행을 원자적으로 수행할 수 없다. 만약 가능하다 하더라도 서비스가 데이터베이스와 메시지에 모두 결합되는 구조는 바람직 하지 않을 수 있다.

하지만 2PC를 사용하지 않으면, 트랜잭션 중에 메시지가 발송된 것을 확신할 수 없다. 트랜잭션이 커밋 되는 것을 보장할 수 없을 뿐만 아니라, 트랜잭션 커밋 후 메시지 발송 중에 크래시가 발생하지 않는다는 것 또한 보장할 수 없다.

더불어, 메시지는 반드시 서비스가 발송한 순서대로 메시지 브로커에 도착해야 한다. 예를 들면, 서비스에서 한 aggregate이 트랜잭션 T1, T2에 의해 업데이트 된다고 하자. 두 트랜잭션은 같은 서비스 인스턴스에서 수행될 수도 있고 다른 서비스 인스턴스에서 수행될 수도 있다. 각각 트랜잭션은 대응되는 이벤트 E1, E2가 있다. 이 때 만일 트랜잭션 T1 다음에 T2가 수행된다면, 이벤트 E2는 반드시 E1 다음에 발행되어야 한다.

Problem

어떻게 데이터베이스 업데이트와 메시지/이벤트 발행을 안정적/원자적으로 할 수 있을까?

Forces

  • 2PC는 사용하지 않는다.
  • 만약 데이터베이스 트랜잭션이 커밋됐다면, 메시지는 반드시 발송되어야 한다. 반대로 데이터베이스가 롤백되었다면, 메시지는 발송되지 않아야 한다.
  • 메시지는 반드시 서비스가 발송한 순서대로 메시지 브로커에 도착해야 한다. 같은 aggregate을 업데이트하는 다수에 서비스 인스턴스에 걸쳐서 이 순서는 반드시 지켜져야 한다.

Solution

관계형 데이터베이스를 사용하는 서비스는 outbox 테이블(e.g. MESSAGE)에 메시지/이벤트를 로컬 트랜잭션으로 insert 한다. NoSQL 데이터베이스를 사용하는 서비스는 메시지/이벤트를 record(e.g. document 또는 item) attribute를 추가하는 방식으로 업데이트 한다. 별도의 메시지 릴레이 프로세스는 데이터베이스에 추가 된 이벤트를 메시지 브로커로 발행한다.

image

Result context

이 패턴은 아래와 같은 장점이 있다.

  • 2PC를 사용하지 않는다.
  • 데이터베이스 트랜잭션이 커밋된 경우에만 메시지가 발송 되는 것을 보장 한다.
  • 메시지는 어플리캐이션에 의해 발송된 순서대로 메시지 브로커에 도착한다.

이 패턴은 아래와 같은 단점이 있다.

  • 개발자가 데이터베이스 업데이트 후에 메시지/이벤트를 발행하는 것을 잊어버릴 수 있기 때문에 잠재적으로 에러가 발생할 수 있다.

이 패턴은 또한 아래 이슈가 있다.

  • 메시지 릴레이는 같은 메시지를 중복해서 발행할 수 있다. 예를 들면, 메시지를 발행한 후 발행 여부를 기록하기 전에 크래시가 일어나, 이를 재실행 하면서 메시지를 다시 발행할 수 있다. 따라서, 메시지 컨슈머는 이미 처리한 동일한 메시지에 대해서 반드시 멱등하게 동작해야 한다. 다행히도 메시지 컨슈머는 보통 멱등하게 동작하므로 (브로커는 메시지를 여러번 보낼 수 있다) 이 것이 문제되지 않는다.

Learn more