← Posts

Event-Driven · DLQ · Schema

실무의 이벤트 - 언제 꺼내고 무엇을 조심하나

패턴을 안다고 다 써야 하는 건 아니다. 이벤트 스키마라는 계약, 순서와 독약 메시지, 배포 후 안 보이는 흐름을 트레이싱으로 잇기, 그리고 애초에 이벤트가 답이 아닌 자리까지 짚으며 시리즈를 닫는다.

목차
  1. 패턴은 문제가 부를 때만
  2. 이벤트 스키마는 계약이다
  3. 순서가 필요하면 따로 챙긴다
  4. 독약 메시지와 DLQ
  5. 배포 후는 안 보인다: 트레이싱
  6. 테스트가 어렵다
  7. 애초에 이벤트가 답이 아닌 자리
  8. 정리

패턴의 이름을 다 배웠다. 이제 반대 방향이 중요하다 - 언제 안 쓰고, 쓸 때 무엇을 조심하나. 이벤트로 이은 시스템의 실무를 짚으며 시리즈를 닫는다.

패턴은 문제가 부를 때만

가장 흔한 실수는 이름을 배웠으니 다 써보는 것이다. 아웃박스·사가·CQRS·이벤트 소싱을 처음부터 다 얹으면, 단순한 일에 거대한 기계를 돌리게 된다.

개요에서 말한 원칙이 실무의 전부다 - 문제가 실제로 나타났을 때 해당 패턴을 꺼낸다. 아직 한 서비스면 사가는 없어도 되고, 조회가 단순하면 CQRS는 과하다. 패턴을 아는 값은 “그 문제엔 이미 답이 있다”이지, “다 써야 한다”가 아니다.

이벤트 스키마는 계약이다

이벤트는 pub-sub이 못박았듯 계약이다. 한번 내보낸 이벤트의 모양은 그걸 듣는 모든 서비스가 의존한다. 함부로 바꾸면 조용히 깨진다.

  • 필드를 빼거나 이름을 바꾸는 건 계약 위반 - 그 필드를 읽던 소비자가 깨진다. 빼지 말고 더하는 방향으로만 바꾼다(옛 소비자는 새 필드를 무시하면 그만).
  • 정말 바꿔야 하면 버전을 나눈다 - 주문됨.v2를 새로 내보내고, 소비자들이 옮겨갈 때까지 둘 다 발행한다.
  • 이벤트에 무엇을 담을지 처음에 신중히 - 너무 적게 담으면 소비자가 매번 되물어야 하고, 너무 많이 담으면 계약이 비대해져 못 바꾼다.

순서가 필요하면 따로 챙긴다

멱등 소비자 글에서 짚었듯, 멱등성은 중복을 막지 순서를 안 지킨다. “가입” 다음 “탈퇴”가 뒤바뀌면 사고다.

순서가 중요하면 같은 대상의 이벤트를 한 줄로 몬다 - 흔히 대상 id(예: 회원 id)로 파티션을 나눠, 같은 회원의 이벤트는 항상 같은 줄에서 순서대로 처리되게 한다. 단, 이러면 그 줄이 병목이 될 수 있으니 순서가 진짜 필요한 곳에만 건다. 대부분의 이벤트는 순서가 상관없다.

독약 메시지와 DLQ

계속 실패하는 메시지가 하나 있으면 어떻게 될까. 소비자가 처리하다 실패 → 재전송 → 또 실패를 무한 반복한다. 이 독약 메시지(poison message) 하나가 뒤의 멀쩡한 메시지까지 다 막는다.

diagramdiagram

그래서 정해진 횟수만큼 재시도하고도 실패하면, 그 메시지를 데드레터 큐(DLQ, dead-letter queue) 라는 별도 큐로 치운다. 본 흐름은 막히지 않고 계속 흐르고, DLQ에 쌓인 것은 사람이 나중에 살펴 원인을 고치거나 다시 넣는다. DLQ가 쌓이는 걸 알림으로 감시하는 것까지가 한 세트다.

배포 후는 안 보인다: 트레이싱

이벤트로 이으면 하나의 요청이 여러 서비스를 비동기로 거친다. 어디서 멈췄는지, 왜 느린지가 무슨 일이 있었는지 보기 어려운 그 문제로 돌아온다.

그래서 이벤트에 상관 id를 실어, 한 주문이 만든 모든 이벤트를 하나로 꿴다. 이게 분산 트레이싱이다 - 흩어진 이벤트를 한 흐름으로 이어 “이 주문이 지금 사가의 어디까지 갔나”를 보이게 한다. 이벤트 아키텍처와 관측가능성은 사실상 함께 가야 한다.

테스트가 어렵다

비동기로 이은 시스템은 테스트가 까다롭다. “이벤트를 냈다”와 “그 결과가 반영됐다” 사이에 시간이 있어, 결과를 바로 단언하기 어렵다.

  • 단위로는 각 조각을 격리해 본다 - “이 이벤트를 받으면 이 상태가 된다”를 멱등성·보상 포함해 검증한다.
  • 사가 전체는 통합 테스트로 - 진짜를 붙여 이벤트가 실제로 흐르는지, 실패 시 보상이 도는지 본다.
  • 결과를 기다리는 테스트가 필요하다 - “곧 이렇게 될 것”을 일정 시간 안에 확인하는 식으로, 결과적 정합성을 테스트에 반영한다.

애초에 이벤트가 답이 아닌 자리

시리즈를 이벤트로 채웠지만, 동기 호출이 맞는 자리가 분명히 있다.

  • 즉시 답이 필요하면 동기로 - “이 쿠폰 쓸 수 있나”처럼 지금 당장 예/아니오가 필요한 조회는, 이벤트로 돌리면 오히려 복잡하고 느리다. 그냥 물어보고 기다리는 게 맞다.
  • 강한 일관성이 필수면 이벤트를 피한다 - 이벤트 패턴은 대부분 결과적 정합성을 전제한다. “절대 잠깐도 어긋나면 안 되는” 곳(계좌 이체의 차변·대변)은 한 트랜잭션 안에 두는 게 맞다.
  • 간단한 시스템은 이벤트 없이 - 서비스 하나, DB 하나로 되는 일에 이벤트를 얹으면 얻는 것 없이 복잡성만 진다.

느슨하게 잇는 것은 결합을 낮추는 대신 복잡성과 지연을 사는 거래다. 그 거래가 남는 장사일 때만 한다.

정리

  • 패턴은 문제가 실제로 나타났을 때 꺼낸다 - 이름을 배웠다고 다 쓰는 게 아니다.
  • 이벤트 스키마는 계약 - 빼지 말고 더하고, 바꿔야 하면 버전을 나눈다. 순서가 필요하면 파티션으로 따로 챙긴다.
  • 독약 메시지는 DLQ로 치워 본 흐름을 막지 않고, 흩어진 흐름은 트레이싱으로 꿴다.
  • 무엇보다 이벤트가 답이 아닌 자리를 안다 - 즉시성·강한 일관성이 필요하거나 시스템이 단순하면 동기 호출이 맞다.

여기까지가 이 시리즈다. 쪼개고 이으면 생기는 문제들에서 출발해, 중복을 삼키고(멱등 소비자), 발행을 원자적으로(아웃박스), 분산 트랜잭션을 보상으로(사가), 조회를 갈라(CQRS), 상태를 로그로(이벤트 소싱) 다뤘다. 이벤트 패턴의 목적은 화려함이 아니라, 쪼갠 시스템을 잃은 것 없이 이어 붙이는 것이다.