도메인 이벤트와 Transactaion Outbox Pattern

라빈·2022년 8월 24일
0

도메인 이벤트

결합도를 측정할 때 고려해야할 점:

  • 별도 스레드에서 다른 서비스를 호출한다면 서비스 관점에서 결합도를 제거했다고 할 수 있다.
  • 하지만 시스템 관점에서 별도 스레드에서 대상 도메인을 호출하는 의도가 남아있기 때문에 결합도가 제거되지 않는다.

→ 물리적 결합도와 개념적 결합도를 판단할 수 있어야 한다.

물리적, 개념적 결합을 느슨하게 하기 위해 도메인 이벤트를 정의한다. 도메인 이벤트를 정의할 때는 특정 소비처에 목적을 두고 설계하면 안된다. 이벤트를 발행하는 도메인에서 의미있는 행위와 상태를 정의하여 발행하고 소비처는 처리할 비즈니스 목적에 맞는 이벤트를 구독해야한다.

하나의 시스템 안에서도 도메인의 주 행위를 정의해야 한다. 주요 기능의 비관심사를 이벤트 기반으로 분리하여 도메인 행위의 응집을 높이고 비관심사에 대한 결합을 느슨하게 할 수 있다.(도메인의 주요 행위는 정책 기반으로 정의한다.)

내부 이벤트와 외부 이벤트를 나눠서 정의했을때:

  • 내부 이벤트를 정의할 때 소비처에 영향 파악이 쉽고 관리할 수 있다.
  • 내부 이벤트에는 외부에 알릴 필요가 없는 도메인 개념을 녹일 수 있다.
  • 외부 이벤트는 시스템간의 결합도를 줄이는 목적으로 설계한다.
  • 외부 이벤트를 정의할 때 구독자의 행위를 고려하지 않는다.

(고민)

  • 내부 이벤트를 충분히 일반화 할 수 있다면 내부와 외부를 가리지 않고 사용할 수 있다.
    • 이벤트를 설계할 때 행위(event) / 상태(state)를 잘 구분지어서 설계해야 한다.
      • 행위는 상태를 변경시킬 수도 있고 변경시키지 않을 수 있다.
    • 경험상 내부 이벤트와 외부 이벤트를 동일한 이벤트로 사용하면 이벤트의 행위, 상태를 변경할 때 이벤트를 구독하는 다른 시스템에서 마이그레이션에 어려움을 겪는 경우가 많다.
    • 식별자만 페이로드에 포함하는 zero payload 방식을 선택하지 않은 이유는 API 호출로 인한 물리적 결합도가 생기고 이에 대한 다양한 부가 기능이 추가되어야 한다.(fallback 전략 등)

2022. 08. 25 논의 내용 - zero payload vs full payload

  • zero payload 사용 시 보안의 장점이 있다. 이벤트로 내보내면 안되는 데이터의 경우 인증된 사용자만 호출할 수 있는 API를 통해 데이터를 전달해줄 수 있다.
  • 이벤트의 페이로드를 일반화하는 과정은 매우 어렵기 때문에 식별자를 통해 엔티티를 직접 가져갈 수 있다.
  • 이벤트가 누락되거나 밀렸을 경우 zero payload의 식별자를 통해 엔티티의 최신 상태를 조회했을 때 현재 다루는 이벤트의 목적과 다른 데이터를 소비할 수 있다.
  • full payload의 경우 이벤트 자체의 기록만으로 리플레이를 할 수 있다.
  • zero payload 방식에서 API를 통해 엔티티를 제공할 때 어차피 엔티티 자체에 불필요한 데이터가 담기거나 외부에 공개하고 싶지 않은 값들이 노출될 수 있다.
  • full payload의 경우 특정 목적을 가지고 사용하면 좋다. 예를 들어 사내 서비스팀들간의 소통에 full payload 방식을 사용한다면 이벤트 데이터의 컨버팅 과정을 생략할 수 있다. 전사적으로는 규약 없이 데이터를 흘릴 수 있기 때문이다. 하지만 사내망을 벗어나게 이런 이벤트를 흘려서는 안된다.
  • 우리 시스템의 경우 도메인 이벤트를 이용해 Kafka Streams를 구성하였다. 만약 도메인 이벤트를 zero payload로 정의했을 때 스트리밍 개념을 사용할 수 없다.

이벤트 저장소를 설계하게 되는 계기:

  • 구독자측에서 비정상적 이벤트 처리로 인해 이벤트 재발행이 필요할 수 있다.
  • 구독자가 재발행하길 원하는 이벤트의 형태는 다양할 수 있다.(특정 이벤트, 특정 기간, 특정 타입 등)
    • 데이터의 경우 최종 상태만 보관한다.(행위로 인한 최종 변경 상태) 따라서 과정을 복원하기 어렵다.

→ 이벤트 저장소에 이벤트를 저장하는 행위까지를 도메인의 중요한 행위로 정의해야한다. 이벤트 저장소에 이벤트 저장 실패 시 도메인 행위가 실패했다고 간주된다.

DBMS를 다양하게 사용할 경우 분산 트랜잭션 처리에 어려움이 있을 수 있으므로 동일 저장소에 데이터베이스와 이벤트 발행 기록을 남기는 전략을 선택한다. → Transactional Outboux Pattern

Transactional Outbox Pattern

서비스의 명령(command)은 데이터베이스를 업데이트하고 메시지 / 이벤트를 발행해야 한다. 비슷하게, 서비스가 도메인 이벤트를 발행한다면 반드시 원자적으로 aggregate를 업데이트하고 이벤트를 발행해야 한다.

이것은 데이터 불일치와 버그를 방지할 수 있다. 그러나 기존의 분산 트랜잭션을 사용하여 데이터베이스를 자동으로 업데이트하고 메시지 / 이벤트를 발행하는 것은 불가능하다. 메시지 브로커가 2PC(Two Phase Commit)을 지원하지 않을 수 있다.

2PC를 사용하지 않더라도 트랜잭션 중간에 메시지를 발행하는 것은 신뢰할 수 없다. 왜냐하면 해당 트랜잭션이 커밋된다는 보장이 없기 때문이다. 마찬가지로, 서비스에서 트랜잭션 이후에 메시지를 보내더라도 메시지를 보내는 과정에서 장애가 발생하지 않음을 보장할 수 없다.

문제점

어떻게 하면 신뢰할수 있고 데이터베이스의 단일 데이터만 업데이트하며 메시지 / 이벤트를 발행할 수 있는가?

제약사항

  • 2PC는 사용할 수 없는 방법이다.
  • 데이터베이스의 트랜잭션이 커밋되었다면 메시지는 반드시 발행된다. 반대로, 데이터베이스가 롤백 된다면 메시지는 반드시 발행되지 않는다.
  • 메시지는 반드시 서비스가 발행한 순서대로 메시지 브로커에 전달된다. 같은 aggregate의 메시지라면 여러 서비스 인스턴스 간에도 순서가 지켜져야 한다.

해결방법

서비스는 관계형 데이터베이스에 메시지 / 이벤트를 outbox table에 로컬 트랜잭션 단위에서 저장한다. 서비스는 NoSQL 데이터베이스를 메시지 / 이벤트의 속성(attributes)을 저장하기 위해 사용할 수 있다. 별도의 메시지 전달(Message Relay) 과정이 데이터베이스에 저장된 이벤트를 메시지 브로커로 전송한다.

결과 분석

이점:

  • 2PC를 사용하지 않는다.
  • 데이터베이스 트랜잭션 커밋과 메시지 전송 모두가 보장된다.
  • 애플리케이션이 전송한 순서대로 메시지 브로커로 메시지가 전달된다.

결점:

  • 개발자가 데이터베이스 업데이트 이후 메시지 / 이벤트 발행 과정을 누락해 에러가 발생할 잠재적 가능성이 존재한다.

이슈 사항:

  • 메시지 전달(Message Relay) 시스템에서는 메시지를 한 번 이상 발행할 수 있어야 한다. 예를 들어, 메시지를 발행했으나 기록하기 이전에 장애가 발생해 시스템을 재시작했다면 메시지를 재발행해야 한다. 결과적으로, 메시지 소비자는 멱등성을 보장해야한다.(메시지가 갖는 고유 ID에 따라 처리 여부를 알아야 한다.)

참고

profile
작은 개발지식부터 공유해요 :)

0개의 댓글