개발자는 기록이 답이다

이벤트 메시지의 Payload, 어디까지 포함해야 할까? 본문

카테고리 없음

이벤트 메시지의 Payload, 어디까지 포함해야 할까?

slow-walker 2025. 3. 30. 23:55

[작성하고 수정중입니다..]

 

제가 담당한 "문의" 도메인은 레거시 시스템인 "상담" 도메인으로부터 이벤트를 수신하고, 이를 기반으로 후속 로직을 처리한다. 이번 글에서는 2024년 말에 개발한 "상태 카드 발송" 기능과 관련하여, 이벤트 메시지의 Payload 설계에 대해 고민했던 내용을 적어보고자 합니다.

 

📌 문제 상황

기존 레거시 "상담" 시스템에서는 ConsultationCreatedEvent (상담 생성 이벤트)ConsultationChangedEvent (상담 변경 이벤트) 만을 메시지로 발행하고 있습니다. 이 "상담" 도메인은 SU(selected user) 를 생성하는 도메인으로, 다른 여러 도메인에서 참조되는 Upstream 역할을 하며, 다양한 Downstream 시스템들이 해당 이벤트를 구독하고 있습니다.

 

Downstream 시스템 간의 결합도를 낮추기 위해, 이벤트에는 구체적인 데이터를 포함하지 않고 이벤트 이름과 상담 식별자와 관련된 ID만 담은 Zero Payload 방식을 채택하고 있었습니다. 이를 통해 이벤트 소비자는 이벤트 발생 여부만 감지하고, 별도로 API 를 통해 이벤트 발행자를 질의하여 필요한 데이터를 얻는 방식을 사용하고 있었습니다.

 

 

{
  "consultationId": 5621989,
  "hospitalId": 4848,
  "txDateTime": "2025-04-05T18:17:35",
  "raisedAt": "2025-03-29T15:38:32"
}

 

Zero Payload는 우아한 형제들 기술블로그에도 적혀있는 방법으로 서비스간 결합을 느슨하게 만들 수 있는 방법입니다.

하지만 현재 구조에서 Zero Payload를 유지하면서 제가 구현해야 하는 기능을 만들 수 없었습니다. 아래 3가지가 바로 그 이유 였습니다.

 

1️⃣ 첫 번째 문제: 상태 변경만 식별 불가

Consultation은 "100개 이상의 컬럼"을 가진 복잡한 모델입니다. 이 때문에 서비스마다 추적하고자 하는 상담의 필드가 달라졌고, 변경이 발생할 때마다 모든 이벤트를 ConsultationChangedEvent로만 발행하는 구조가 되어버렸습니다.


그 결과, "상태만 변경되었는지"를 구분할 수 없게 되는 문제가 발생했습니다. 아래와 같은 서로 다른 변경사항이 모두 동일한 SQS로 전달되면서, "문의"도메인에서는 이 이벤트가 어떤 상태가 변경되었는지 의도를 정확히 파악할 수 없었습니다

  • 상담의 방문자 이름이 변경된 경우
  • 상담의 상태가 변경된 경우
  • 상담의 메모가 변경된 경우
  • 상담 포인트가 상환된 경우
  • 기타 등등

 

2️⃣ 두 번째 문제: Zero Payload로 인한 상태 추적 한계

 

Zero Payload는 서비스 간의 결합도를 낮춘다는 장점이 있지만, 오직 이벤트 발생 이후의 최종 상태(Snapshot)만 조회할 수 있다는 한계가 있습니다. 상담에서는 상태가 변경될때마다 기록하는 히스토리가 없었기 때문입니다. 즉, 변경된 상태의 최종 결과만 담고 있는 반정규화 칼럼만 있고, 이력을 담당하는 테이블이 없습니다. (왜 이력이 필요한가는 아래 적혀있는 3번 문제에서 나옵니다)

 

그렇다면 왜 처음부터 상태 이력을 저장하지 않았을까요?

추측해보면, 상담 테이블이 100개가 넘는 칼럼을 가지고 있기 때문에, 모든 필드의 변경 내역을 추적하는 히스토리 테이블을 설계하는 데 현실적인 어려웠을 것 같니다. 또한, 이력 테이블을 하나로 통합할지, 아니면 유즈케이스별로 분리할지에 대한 고민도 당시에는 해결되지 않았던 것으로 보입니다.

 

상담 변경 이벤트를 발행할 때는 Outbox Pattern을 사용하고 있긴 하지만, 이때 저장되는 메시지는 변경된 전체 데이터를 담지 않은 Zero Payload 메시지이기 때문에, 이 자체를 히스토리 데이터로 활용할 수는 없습니다.

 

결과적으로, 수신 시스템인 문의 도메인에서는 이벤트 수신 후 consultation을 조회해도 변경 전 상태를 알 수 없기 때문에, 아래와 같은 정보가 누락되며, 상태 변경 카드 발송 기능을 구현하는 데 있어 큰 제약이 있었습니다.

  • 누가
  • 어떤 상태에서 (from)
  • 어떤 상태로 (to)
  • 변경했는지

 

제가 담당한 "상태 카드 발송" 기능에서 필요한 시나리오는 다음과 같습니다,

  • 고객이 예약을 접수한 경우 - 예약 접수 카드 (발화자 : 고객)
  • 병원이 예약을 확정한 경우 - 예약 확정 카드 (발화자 : 병원)
  • 고객이 예약을 변경한 경우 - 예약 변경 카드 (발화자 : 고객)
  • 병원이 예약을 변경한 경우 - 예약 변경 카드 (발화자 : 병원)
  • 병원이 예약을 취소한 경우 - 예약 취소 카드 (발화자 : 고객)
  • 고객이 예약을 취소한 경우 - 예약 취소 카드 (발화자 : 병원)

 

3️⃣ 세 번째 문제: 복잡한 상태 체계

상담 상태는 사용자 유형에 따라 파트너(병원) 입장에서는 13단계, 고객 입장에서는 4단계로 나누어져 있었습니다. 아래는 파트너 어드민에서 병원이 예약 접수한 고객들의 상담 이력을 관리하는 화면 예시입니다.

 


각 상태는 1단계 → 2단계 → 3단계로 단순히 흐르지 않고, 자유롭게 상호 이동이 가능한 구조였기 때문에, 이 상태들의 변화를 일관되게 관리할 수 있는 체계가 필요했습니다.

 

📌 해결 방안

결국 zero payload를 포기하고, 필드를 추가하는 방식으로 개발했습니다.

필드는 "stateTransition(상태 전이)"과 "stateTransitionActor(상태 전이 주체)"를 추가했습니다.

각 필드는 "상태 카드 발송" 기능에서 필요한 이전 이후 상태를 분기로 하여 enum으로 개발하였습니다.

{
  "consultationId": 5621989,
  "hospitalId": 4848,
  "txDateTime": "2025-04-05T18:17:35",
  "raisedAt": "2025-03-29T15:38:32",
  "stateTransition": "CONSULTATION_CANCELED",
  "stateTransitionActor": "MEMBER",
  "prevState": "FIXED"
}

 

개발은 완성했지만, 메세지를 발행할때 언제 zero payload를 사용해야 할지, 필드를 추가한다면 downstream과의 강결합이 생기는건지 의문이 들어 추가적으로 공부해보았습니다.

 

📌 메세지와 메세지 채널의 종류

 마이크로서비스 패턴』『도메인 주도 설계 첫걸음』을 읽고 정리한 내용은 아래와 같습니다.

 

이벤트는 메세지이지만 메세지가 반드시 이벤트는 아닙니다. 메세지에는 2가지 유형이 있습니다.

메시지와 이벤트는 다릅니다.  이벤트(Event)는 메시지의 한 종류이지만, 모든 메시지가 이벤트인 것은 아닙니다.

 

메시지는 크게 두 가지 유형으로 나눌 수 있습니다

1️⃣  커맨드(Command)

"특정 동작을 수행해라"라는 명령

 

 

  • 특정 동작을 수행하라는 지시를 담은 메시지입니다.
  • 수신자는 해당 명령을 반드시 실행해야 하며, 요청자와 수신자 간에 강한 의존 관계가 존재합니다.

 

예시:

  • CreateOrderCommand → "주문을 생성해라"


2️⃣ 이벤트(Event)

"무엇이 일어났다"는 사실

 

  • 이미 발생한 도메인 내 사건을 알리는 메시지입니다.
  • 수신자는 이 이벤트에 반드시 반응하지 않아도 되며, 느슨한 결합(loose coupling) 을 유지할 수 있습니다.

예시:

  • OrderCreatedEvent → "주문이 생성되었다"

커맨드와 이벤트 모두 메시지 기반 비동기 통신에 사용될 수 있지만, 역할과 관계성은 분명히 다릅니다. 커맨드는 동사 형태로 지시를 내리는 구조 (Create, Cancel, Update 등)이고, 이벤트는 과거형 또는 수동태로, 어떤 일이 이미 발생했는지를 기술 (Created, Cancelled, Updated 등)합니다.

 

이벤트는 수신자가 다양한 방식으로 처리할 수 있도록, 충분한 정보를 포함하는 것이 중요합니다. 즉, 발행자는 수신자를 배려해 설계해야 하며, 이벤트를 통해 "무슨 일이, 언제, 누가, 어떤 상태로" 일어났는지를 최대한 명확히 전달해야 합니다.

 

 

메세지 채널은 2가지 종류가 있습니다.

 

1️⃣ point-to-point 채널

하나의 메시지를 여러 수신자 중 오직 한 명에게만 전달합니다. 요청-응답 또는 명령 기반의 일대일 상호작용에 적합하며, 일반적으로 커맨드를 처리할 때 사용됩니다.  

2️⃣ 발행-구독(publish-subscribe) 

하나의 메시지를 해당 채널을 구독하고 있는 모든 수신자에게 전달합니다. 이벤트 기반의 일대다 상호작용에 적합하며, 시스템 간 느슨한 결합을 가능하게 합니다.  


이처럼 메시지의 성격(Command vs Event)에 따라 적절한 메시지 채널(Point-to-Point vs Publish-Subscribe)을 선택하는 것이 중요합니다.