3.12 이벤트 기반 아키텍처와 메시징
개요와 동기
이벤트 기반 아키텍처(EDA)는 구성 요소가 서로를 직접 호출하는 대신 이벤트를 생산하고 그에 반응하여 소통하는 스타일입니다. 이벤트는 사실입니다. “OrderPlaced”나 “PaymentCaptured”처럼 이미 일어난 일입니다. 생산자는 사실을 알리고 넘어가며, 얼마든지 많은 소비자가 생산자가 누가 듣는지 모른 채 각자의 일정에 따라 반응합니다. 이는 호출자가 특정 서비스에 무언가를 하라고 요청하고 답을 기다리는 2.3장의 요청-응답 호출과는 다른 자세입니다.
대규모 조직에서 매력은 규모에서의 분리입니다. 수십 개 팀과 수백 개 서비스가 있을 때 모든 것을 직접 점대점 호출로 엮으면, 한 팀의 변경이 다른 팀을 깨고 아무도 이유를 추적할 수 없는 취약한 그물이 생깁니다. 이벤트는 팀들이 서로의 내부가 아니라 사실의 공유 스트림을 통해 통합하게 해 주며, 새 소비자는 생산자가 코드 한 줄 바꾸지 않고 구독으로 합류합니다. 순수한 처리량보다 이 속성이 이벤트 기반 접근이 얽힌 통합을 대체하는 기업과 각자 자기 시스템을 소유한 기관들을 잇는 정부 전반으로 계속 퍼지는 이유입니다.
공공 부문은 과소평가하기 쉬운 두 번째 이점을 얻습니다. 무슨 일이 있었는지에 대한 내구성 있고 순서가 있는 기록은 감사와 투명성 자산입니다. 시민이 급여 결정이 왜 그렇게 나왔는지 물으면, 거기에 이른 이벤트의 불변 로그가 직접 답합니다. 그러나 이벤트 기반 설계는 공짜가 아니며 항상 옳지도 않습니다. 비동기 흐름은 추적하기 더 어렵고, 추론하기 더 어려우며, 과도하게 적용하기 쉽습니다. 이 장은 분리와 규모가 추가된 복잡성만큼 값을 하는 때와 단순한 동기 호출이 더 잘 섬겼을 때에 대해 단호한 입장을 취합니다. 3.3장의 분산 시스템 현실 위에 서 있으니, 아직 읽지 않았다면 먼저 읽으십시오.
핵심 원칙
- 이벤트는 지시가 아니라 사실입니다. 이벤트는 무슨 일이 있었는지 말하고, 명령은 무언가가 일어나기를 요청합니다. 둘을 구별하고 이벤트 이름은 과거 시제로 지으십시오.
- 분리가 요점입니다. 생산자는 누가 이벤트를 소비하는지 알거나 신경 쓰지 않아야 합니다. 그렇다면 메시징 의상을 입은 결합입니다.
- 최소 한 번 전달로 설계하십시오. 정확히 한 번 전달은 신화입니다. 모든 소비자를 멱등하게 만들어 중복이 무해하게 하십시오.
- 순서는 값을 치르는 보장입니다. 순서는 토픽 전체가 아니라 파티션 안에서 얻습니다. 파티션 키를 의도적으로 고르십시오.
- 스키마는 계약입니다. 이벤트의 모양은 공개 인터페이스입니다. 발행된 API를 다루듯 신중하게 진화시키십시오.
- 비동기가 관측 불가능을 뜻하지 않습니다. 메시지를 처음부터 끝까지 따라갈 수 없다면 시스템을 운영할 수 없습니다.
- 복잡성은 얻어 내야 합니다. 이벤트 소싱, CQRS, 사가는 강력하고 비쌉니다. 기본으로가 아니라 문제가 요구할 때 손을 뻗으십시오.
권장 사항
무언가를 만들기 전에 이벤트, 명령, 메시지를 구분한다
이 세 단어는 서로 바꿔 쓰이고 그 혼동이 실제 설계 실수를 낳습니다. 명령은 무언가를 하라는 요청(“CapturePayment”)으로, 하나의 핸들러를 향하며 거부될 수 있습니다. 이벤트는 무언가가 이미 일어났다는 알림(“PaymentCaptured”)으로, 관심 있는 모두에게 방송되며 사실이 이미 참이므로 거부될 수 없습니다. 메시지는 둘 중 어느 것이든 전선 너머로 실어 나르는 중립적 봉투입니다. 이 구분이 결합을 형성합니다. 명령은 발신자를 특정 수신자와 결과에 결합시키고, 이벤트는 다음에 일어날 일에 대한 통제를 내려놓습니다. 이벤트 이름은 과거 시제로 짓고, 사실은 “이 특정한 일을 하러 가 주세요”를 뜻하는 “이벤트”를 발행하고 있음을 알아챘다면, 명령을 위장해 쓴 것입니다.
큐, 로그, 발행/구독을 의도적으로 고른다
모든 메시징이 같은 모양은 아니며, 틀린 것을 고르는 것은 흔한 초기 실수입니다. 메시지 큐는 각 메시지를 한 소비자에게 전달하고 보통 처리되면 제거하며, 많은 워커가 각각 한 번씩 처리할 작업을 가져가는 작업 분배에 맞습니다. 내구성 있는 이벤트 로그(스트림)는 이벤트를 순서대로 보관하고 여러 독립 소비자가 각자 속도로 읽으며 어느 시점에서든 이력을 재생하게 해 주어, 이벤트 분배와 감사에 맞습니다. 발행/구독은 생산자가 토픽에 발행하고 여러 구독자가 각자 사본을 받습니다. 실용적 규칙은 이렇습니다. 메시지가 한 워커가 완료해야 하는 작업이라면 큐에, 지금이나 나중에 많은 당사자가 관심 가질 수 있는 사실이라면 내구성 있는 로그에 손을 뻗으십시오. 로그는 복구와 새 소비자 온보딩을 위한 재생도 줍니다. 이 선택이 데이터 저장 전략과 어떻게 상호작용하는지는 3.4장을 보십시오.
자율에는 코레오그래피를, 통제에는 오케스트레이션을 선호한다
비즈니스 프로세스가 여러 서비스에 걸칠 때 두 방식 중 하나로 조율합니다. 코레오그래피에서는 각 서비스가 이벤트에 반응해 자기 것을 내보내며 중앙의 두뇌가 없습니다. 최대한 분리되어 팀 자율에 좋지만, 전체 프로세스는 어느 단일 장소도 기술하지 않는 창발적 동작으로만 존재합니다. 오케스트레이션에서는 중앙 조정자가 단계를 이끌고 전체 흐름을 압니다. 모니터링하고 수정하기가 더 쉽지만, 모든 단계가 의존하는 구성 요소가 생기는 대가를 치릅니다. 좋은 기본값은 느슨하게 관련된 반응(“주문이 배송되면 로열티 서비스가 포인트를 부여”)에는 코레오그래피, 분명한 성공 조건과 상태를 보고할 필요가 있는 정의된 트랜잭션에는 오케스트레이션입니다. 중요한 프로세스가 열 개의 이벤트 핸들러에 흩어진 민간 전승으로만 존재하게 두지 마십시오.
이벤트 소싱과 CQRS는 값을 할 때만 쓴다
이벤트 소싱은 덮어쓰는 현재 스냅샷이 아니라 추가 전용 이벤트 시퀀스로 상태를 저장하고, 재생해서 현재 상태를 재구축합니다. 이점은 완벽한 감사 추적, 과거 어느 상태든 재구성하는 능력, 시간 질의입니다. 비용은 이벤트 스키마를 영원히 버전 관리하고, 재생과 스냅샷을 다루고, 대부분의 개발자가 써 본 적 없는 멘탈 모델을 지는 것입니다. CQRS(Command Query Responsibility Segregation)는 쓰기 모델을 하나 이상의 읽기 모델과 분리해 읽기와 쓰기가 독립적으로 확장되고 진화하게 합니다. 이벤트 소싱과 자연스럽게 짝을 이루지만 필수는 아닙니다. 둘 다 진짜 감사, 컴플라이언스, 복잡한 질의 필요가 있는 도메인에서 빛나며, 그래서 규제되는 금융과 정부가 수고할 가치를 찾습니다. 단순한 생성-읽기-갱신-삭제 서비스에는 후회할 우발적 복잡성이므로, 반사적으로 시스템 전체가 아니라 필요한 도메인의 조각에 적용하십시오.
분산 트랜잭션은 2단계 커밋이 아니라 사가로 관리한다
보통 여러 서비스와 데이터베이스에 하나의 원자적 트랜잭션을 두를 수 없습니다. 분산 2단계 커밋은 네트워크를 가로질러 락을 쥐고, 가용성을 깎고, 확장이 나빠서 이벤트 기반 시스템에 거의 맞지 않습니다. 사가 패턴이 그것을 대체합니다. 트랜잭션을 로컬 트랜잭션의 시퀀스로 모델링하고, 각각이 다음을 촉발하는 이벤트를 내보내며, 뒤 단계가 실패하면 그것을 취소하는 보상 행동을 각 단계에 줍니다. “재고 예약”이 성공했는데 “카드 청구”가 실패하면, 보상이 재고를 풉니다. 사가는 코레오그래피나 오케스트레이션으로 구성할 수 있고, 모니터링해야 하는 것에는 대개 오케스트레이션이 이깁니다. 사가는 최종 일관성을 받아들이므로 시스템이 수렴하기 전에 중간 상태(“예약됐지만 결제 안 됨”)를 거치니, 사용자 경험과 감사 추적이 “진행 중”과 “보상됨” 상태를 정직하게 보이게 설계하십시오. 3.3장이 분산 시스템의 각도에서 같은 영역을 다룹니다.
최소 한 번 전달로 설계하고 소비자를 멱등하게 만든다
메시지 시스템은 실패를 가로질러 정확히 한 번 전달할 수 없습니다. “처리했다”고 말하는 확인 응답 자체가 유실되어 재전달을 강제할 수 있기 때문입니다. 달성할 수 있는 것은 멱등한 처리를 곁들인 최소 한 번 전달이며, 정확히 한 번의 효과를 냅니다. 멱등성은 같은 이벤트를 두 번 처리해도 한 번 처리한 것과 같은 결과를 남긴다는 뜻입니다. 각 이벤트의 멱등성 키와 이미 처리한 것의 기록으로 거기에 이르러, 중복이 인식되어 버려지게 하십시오. 최대 한 번 전달(보내고 잊기, 재전달 없음)은 더 단순하지만 메시지를 조용히 잃으므로, 잃어도 되는 데이터에 남겨 두십시오. 일부 벤더가 광고하는 “정확히 한 번” 라벨은 의심하며 다루십시오. 흔히 문구가 암시하는 종단 간 보장이 아니라 특정 조건 아래 한 시스템 경계 안에서의 정확히 한 번을 뜻합니다.
파티션으로 순서를 통제하고 컨슈머 그룹을 안다
순서는 전역적이고 공짜가 아니라 지역적이고 값을 치르는 것입니다. 스트림은 파티션으로 나뉘며, 토픽 전체가 아니라 파티션 안에서 순서를 얻습니다. 이벤트는 파티션 키로 파티션에 라우팅되므로, 그 키를 고르는 것이 무엇이 순서를 유지할지 통제하는 방법입니다. 고객 ID로 키를 잡으면 한 고객의 이벤트는 서로에 대해 순서를 유지하고 다른 고객은 병렬로 처리됩니다. 컨슈머 그룹은 워커 집합이 토픽의 파티션을 나누어 갖고 각 파티션을 한 워커가 처리하게 하며, 파티션별 순서를 보존하면서 처리량을 확장하는 방법입니다. 이곳에서 확장성이 정확성을 만나며 3.5장으로 이어집니다. 실제 순서 요구를 반영하고 부하를 고르게 퍼뜨리는 파티션 키를 고르십시오. 트래픽 대부분을 한 파티션으로 몰아가는 키는 어떤 수의 워커로도 풀 수 없는 핫스팟을 만들기 때문입니다.
스키마를 레지스트리와 진화 규칙이 있는 계약으로 다룬다
이벤트의 구조는 만난 적 없는 팀이 소비하는 발행된 인터페이스이므로, 부주의하게 바꾸면 멀리서 그들을 깨뜨립니다. 이벤트 스키마를 스키마 레지스트리, 곧 각 스키마를 저장하고 생산자가 하나를 바꾸려 할 때 호환성 규칙을 시행하는 공유 카탈로그에 두십시오. 명시적 정책을 채택하십시오. 하위 호환 변경(선택적 필드 추가)은 허용되고, 호환 파괴 변경(필드 제거, 타입 변경, 이름 바꾸기)은 새 스키마 버전과 이전 계획이 필요합니다. 이로써 생산자가 모든 소비자에 걸친 동기화된 배포 없이 진화할 수 있으며, 이것이 이벤트를 고른 이유 전체입니다. 2.3장의 같은 인터페이스 버전 관리 규율이 적용됩니다. 이벤트 스키마는 다른 이름의 API이기 때문입니다.
트랜잭셔널 아웃박스로 전달을 보장하고 실패를 명시적으로 처리한다
고전적 버그가 있습니다. 서비스가 데이터베이스에 쓰고 이벤트를 발행하는데 둘 사이에서 충돌해, 데이터베이스는 바뀌었지만 이벤트는 나가지 않았습니다. 트랜잭셔널 아웃박스는 이벤트를 상태 변경과 같은 데이터베이스 트랜잭션 안에서 아웃박스 테이블에 써서 함께 커밋되거나 실패하게 함으로써 이를 고칩니다. 그다음 별도의 릴레이가 아웃박스를 읽어 브로커에 발행하며, 흔히 변경 데이터 캡처로 데이터베이스 로그를 따라갑니다. 소비 실패에는 데드 레터 큐가 반복해서 실패하는 메시지를 보관해, 독약 메시지(잘못된 형식 등으로 결코 성공하지 못할 메시지)가 뒤의 큐를 영원히 막지 못하게 합니다. 빠른 생산자가 느린 소비자를 압도하지 못하도록 백프레셔를 더하십시오. 큐에 한도를 두고, 가득 차면 메모리를 소진하는 대신 부하를 늦추거나 떨어내십시오. 이 네 메커니즘이 시연과 새벽 3시에 운영할 수 있는 시스템을 가릅니다.
비동기 흐름을 처음부터 끝까지 관측 가능하게 만든다
이벤트 기반으로 가는 가장 어려운 비용은 단일 비즈니스 행위가 이제 생산자, 브로커, 소비자에 흩어지고 그것들을 묶는 호출 스택이 없다는 것입니다. 모든 이벤트를 통해 상관 ID를 전파해 하나의 논리적 흐름을 모든 홉에 걸쳐 따라갈 수 있게 하십시오. 3.3장이 동기 호출에 처방하는 것과 같은 규율입니다. 컨슈머 랙(각 소비자가 실시간에서 얼마나 뒤처져 읽는지)을 일급 지표로 추적하십시오. 상승하는 랙이 문제의 가장 이른 경고이기 때문이며, 데드 레터 큐 깊이, 처리 지연, 재전달 비율도 모니터링하십시오. 이것이 없으면 조용히 소비되지 못한 이벤트가 며칠 뒤 누락된 데이터로 드러나는 보이지 않는 버그가 됩니다.
장단점
| 접근 방식 | 장점 | 단점 / 비용 |
|---|---|---|
| 동기 요청/응답 | 추론이 단순. 즉각적인 결과. 쉬운 추적 | 강한 시간적 결합. 연쇄 실패. 제한된 규모 |
| 이벤트 기반 (로그 위의 발행/구독) | 분리. 독립적 확장. 재생. 감사 추적 | 최종 일관성. 더 어려운 추적. 더 많은 움직이는 부분 |
| 메시지 큐 (작업 분배) | 부하 평준화. 버퍼링. 백프레셔에 우호적 | 메시지당 한 소비자. 방송에는 덜 적합 |
| 이벤트 소싱 + CQRS | 전체 이력. 시간 질의. 읽기/쓰기가 독립적으로 확장 | 영원한 스키마 버전 관리. 재생의 복잡성. 가파른 학습 곡선 |
| 사가 (2단계 커밋 대비) | 확장 가능. 가용. 분산 락 없음 | 최종 일관성. 보상 로직. 추론이 더 어려움 |
핵심 긴장은 분리와 이해 가능성 사이에 있습니다. 추가하는 모든 이벤트는 생산자와 소비자 사이의 결합을 느슨하게 해 팀 자율과 독립적 확장을 사는 동시에, 동기 호출이 평이하게 말해 주었을 이야기의 한 줄을 제거합니다. 흐름은 창발적이 되어 어느 한 파일이 아니라 상호작용 속에 삽니다. 선택적으로 하여 해결하십시오. 팀 경계를 가로지르는 통합, 많은 소비자로의 팬아웃, 부하 급증의 버퍼링, 감사처럼 분리가 진정으로 값을 하는 곳에 이벤트를 쓰십시오. 페이지를 렌더링하려고 데이터를 읽는 것처럼 즉각적인 답과 단순한 멘탈 모델이 필요한 곳에는 동기 호출을 유지하십시오. 피해야 할 실패 방식은 모든 내부 함수 호출을 이벤트로 바꾸고 그것을 아키텍처라고 부르는 것입니다. 3.2장이 채택하는 모든 패턴에 요구하는 것과 같은 아키텍처적 판단입니다.
팀과 논의할 질문
이 특정한 상호작용에 정말 이벤트가 필요합니까, 아니면 동기 호출이 더 명확하고 안전하겠습니까? 이 질문을 건너뛰는 것이 시스템이 우발적 복잡성을 쌓는 방식입니다. 정직한 시험은 생산자가 결과를 지금 돌려받아야 하는지(호출), 다른 이들이 자기 시간에 반응할 수 있는 사실을 알리는 것인지(이벤트)입니다. 일반적 선호가 아니라 구체적 상호작용을 가져와, 분리로 무엇을 얻고 추적의 명확성으로 무엇을 내주는지 물으십시오. 호출자가 “이벤트”가 처리되기를 기다리며 막힌다면, 느리고 디버깅하기 어려운 동기 호출을 만들고 그 특권에 추가 값을 치른 것입니다. 내부, 같은 팀, 지금 답이 필요한 상호작용의 기본은 직접 호출이어야 합니다. 이벤트는 느슨한 결합이 비용만큼 값을 하는 곳에 남겨 두십시오.
소비자가 같은 이벤트를 두 번 받으면 어떻게 되며, 실제로 테스트해 보았습니까? 최소 한 번 전달은 중복이 일어날 것을 보장하므로 모든 소비자가 멱등해야 하지만, 멱등성은 주장하기 쉽고 틀리기 쉽습니다. 실제 소비자를 짚어 두 번째 전달이 어떻게 인식되고 무력화되는지 정확히 따라가 보십시오. 멱등성 키, 처리된 이벤트 로그, 자연스럽게 멱등한 연산 중 무엇이든. 배치를 재전달하고 이중 청구, 중복 레코드, 반복된 알림이 없음을 확인한 실제 테스트의 결과를 가져오십시오. 이메일, 결제, 제3자 호출처럼 데이터베이스를 벗어나는 부작용에 특히 주의하십시오. 멱등하지 않은 버그가 고객에게 직접 해가 되는 곳이기 때문입니다. 팀이 중복 안전을 입증하는 테스트를 가리킬 수 없다면, 중복 안전하지 않다고 가정하십시오.
이벤트 흐름이 프로덕션에서 깨질 때 알아채기까지 얼마나 걸리며, 하나의 메시지를 처음부터 끝까지 추적할 수 있습니까? 비동기 실패는 조용하므로, 조용히 처리를 멈춘 소비자는 누락된 데이터가 고객 불만이나 감사 간극이 될 때까지 눈에 띄지 않을 수 있습니다. 가장 이른 신호가 무엇인지, 컨슈머 랙과 데드 레터 큐 깊이를 아무도 보지 않는 대시보드가 아니라 경보 지표로 모니터링하는지 물으십시오. 실제 인시던트나 게임 데이 연습을 가져와 하나의 상관 ID를 생산자, 브로커, 모든 소비자에 걸쳐 따라가는 데 얼마나 걸리는지 재 보십시오. 답이 “여러 서비스를 grep하고 짐작한다”라면 관측 가능성은 떠안은 복잡성에 준비되어 있지 않습니다. 규제 부문에서는 메시지가 정확히 어떻게 이동했는지 재구성할 수 있는 것이 흔히 호의가 아니라 컴플라이언스 요건입니다.
이미 많은 팀이 소비하는 이벤트 스키마를 어느 것도 깨뜨리지 않고 어떻게 진화시키겠습니까? 이벤트의 모양은 공개 계약이며, 수십 소비자가 일단 의존하면 순해 보이는 변경이 컴파일러의 경고도 없이 들어 본 적 없는 시스템을 멀리서 깨뜨릴 수 있습니다. 긴장은 실제입니다. 생산자는 빠르게 움직여 이벤트를 정리하고 싶고, 모든 소비자는 모양이 영원히 동결되기를 원하므로, 어떤 변경이 안전한지(선택적 필드 추가)와 어떤 것이 새 버전과 이전 기간을 요구하는지(필드 제거, 타입 변경, 이름 바꾸기) 미리 합의하십시오. 토픽별 실제 소비자 목록, 오늘 스키마 레지스트리가 호환성 규칙을 시행하는지 아니면 모양이 비공식 합의로 바뀌는지, 이전 중 두 버전이 얼마나 병렬로 돌 수 있는지를 가져오십시오. 대기업이나 기관 간 정부 데이터 공유 협정에서 조용히 깨진 스키마는 소유하지 않은 시스템의 레코드를 손상시키고 나중에 감사 실패로 드러날 수 있으므로, 호환성 시행을 예의가 아니라 거버넌스로 다루십시오.
가장 중요한 다중 서비스 트랜잭션에서 각 보상 행동은 실제로 무엇을 취소하며, 사용자와 감사자는 어떤 중간 상태를 보겠습니까? 사가는 하나의 원자적 트랜잭션이라는 편안한 환상을 각각 실패할 수 있는 로컬 단계의 시퀀스와 맞바꾸므로, 시스템은 수렴하기 전에 “예약됐지만 결제 안 됨”과 “청구됐지만 배송 안 됨” 같은 상태를 실제로 거칩니다. 그렇지 않은 척하는 것이 돈을 새게 하거나 레코드를 고아로 만드는 사가를 출시하는 방법입니다. 실제 프로세스를 처음부터 끝까지 따라가며 모든 단계의 보상(무엇이 재고를 풀고, 무엇이 카드를 환불하는가)을 지명하고, 모니터링할 수 있는 오케스트레이션 사가가 어느 단일 장소도 기술하지 않는 창발적 코레오그래피를 이기는지 결정하십시오. 행복한 경로가 아니라 실제로 테스트한 실패 사례를 가져와, 사용자 경험과 감사 추적이 “진행 중”과 “보상됨” 상태를 숨기지 않고 정직하게 보여 주는지 확인하십시오. 금융, 급여, 세무 시스템에서 규제 기관은 각 중간 순간에 기록이 어떠했는지와 보상에 누가 책임졌는지 물을 것이므로, 사가의 상태는 그 자체가 컴플라이언스 산출물입니다.
브로커나 이벤트 로그는 누가 운영하며, 그것을 운영하는 진짜 비용을 관리형 대안과 견주어 보았습니까? 메시징 백본은 다이어그램에 그리면 나타나는 공짜 인프라가 아닙니다. 누군가 패치하고, 파티션을 확장하고, 보존을 조정하고, 새벽 3시에 호출되면 대응하고, 용량과 장애 양상을 소유합니다. 오픈 소스 브로커를 자체 호스팅할지 관리형 서비스를 살지, 통제와 데이터 거주를 운영 부담과 라이선스 비용에 견주어 의도적으로 결정하고, 팀이 분산 로그를 잘 운영할 깊이가 있는지 정직하십시오. 총소유비용을 가져오십시오. 호출 대기 부하, 필요한 전문 기술, 보존과 스토리지 청구서, 브로커 장애가 모든 종속 흐름에 하는 일. 기업에서 그 답은 플랫폼 팀의 임무를 형성하고, 정부 기관에서는 조달 규칙, 데이터 주권 요건, 벤더 종속을 피해야 하는 절실한 필요가 가장 싼 선택지를 뒤집을 수 있으므로, 10년 운영할 기술을 확정하기 전에 그런 제약을 드러내십시오.
분야별 관점
스타트업. 가장 희소한 자원이 엔지니어링 주의이므로 팬아웃이 실제로 아플 때까지 동기를 유지하십시오. 세 가지가 하나의 행위에 반응해야 할 때는 자체 브로커 클러스터를 세우는 대신 관리형 큐나 로그에 단일 이벤트(“OrderPlaced” 같은)를 발행하고, 결제와 즉각적인 답이 필요한 것은 직접 호출로 유지하십시오. 세련돼 보이려고 이벤트 소싱, CQRS, 사가를 채택하지 마십시오. 그 복잡성은 작은 팀을 앞지르고 경쟁하는 바로 그 반복을 늦춥니다.
소기업. 메시징 전문가도 Kafka를 운영할 의욕도 없으니 비동기 통합을 운영하는 것이 아니라 사는 것으로 다루십시오. 기존 도구가 이미 내보내는 이벤트(웹훅, 클라우드 제공자나 SaaS 플랫폼에 내장된 큐)에 기대고, 관리형 서비스가 전달, 보존, 멱등성 배관을 소유하게 하십시오. 선택을 정직하게 사느냐 만드느냐로 구성하십시오. 믿을 만한 웹훅 핸들러 몇 개가 새벽 3시에 인력을 대거나 디버깅할 수 없는 맞춤 브로커보다 낫습니다.
대기업. 문제는 많은 팀에 걸친 통합 비용이므로, 수익은 다스려지는 공유 플랫폼입니다. 내구성 있는 이벤트 로그, 시행되는 호환성 정책이 있는 스키마 레지스트리, 분명한 토픽 소유권, 각 팀이 다시 발명하지 않도록 표준 멱등성 및 아웃박스 패턴입니다. 이것이 취약한 엔터프라이즈 서비스 버스나 점대점 링크의 그물을 대체하는 것이 실제 절감으로 누적되는 환경이며, 가시적 상태를 갖춘 오케스트레이션 사가가 운영 직원이 다단계 프로세스를 관리하게 해 주는 곳입니다. 이 규모에서는 조용한 소비자 실패가 수십 개 시스템에 걸친 누락된 데이터가 되므로, 관측 가능성과 스키마 거버넌스 투자를 명시적으로 예산에 넣으십시오.
정부. 내구성 있고 순서가 있는 이벤트 로그는 감사와 투명성 자산입니다. “이 결정이 왜 이렇게 나왔는가”에 불변의 사실 시퀀스로 답하므로, 책임성이 요구하는 곳에서는 이벤트 소싱에 기대십시오. 기관 간 데이터 공유는 안정적인 이벤트 ID에 대한 중복 제거를 갖춘 최소 한 번 전달이 필요해, 재전달된 레코드가 중복 사례를 만들지 않게 해야 하며, 조달은 종속에 대비해 데이터 주권, 브로커 한계의 공개, 이식성을 저울질해야 합니다. 적절한 곳에서는 흐름이 어떻게 동작하고 시민이 자동화된 결과를 어떻게 다툴 수 있는지 공개하고, 이벤트 로그를 감독 기관이 조사하려 할 방어 가능한 기록으로 유지하십시오.
사례
스타트업. 작은 전자상거래 스타트업은 하나의 동기 흐름으로 시작합니다. 결제가 결제 서비스를 호출하고 기다립니다. 성장하면서 주문 확인 이메일, 재고 갱신, 로열티 프로그램이 구매에 반응하기를 원하는데, 각각을 결제 안의 또 다른 동기 호출로 엮으면 결제가 느리고 취약해집니다. 팀은 단일 “OrderPlaced” 이벤트를 내구성 있는 로그에 발행하고 세 독립 소비자가 반응하게 하여 결제가 다시 빨라지고, 나중에 네 번째 반응을 더해도 결제를 바꿀 필요가 없습니다. 주문을 확인하기 전에 예/아니오 답이 필요하므로 결제 승인은 동기로 유지하며, 이는 그들 규모에서 정확히 맞는 선입니다.
대기업. 한 글로벌 보험사는 점대점 통합과 낡아 가는 엔터프라이즈 서비스 버스(ESB), 곧 모든 시스템이 거치며 병목이자 단일 장애점이 된 중앙 허브에 허우적댑니다. 각 도메인이 자기 사실(계약 발행, 청구 접수, 지급 완료)을 발행하고 소비 팀이 필요한 것을 구독하는 내구성 있는 이벤트 로그로 이전합니다. 운영 직원이 각 청구의 상태를 볼 수 있게 오케스트레이션된 청구 사가가 실패하는 단계의 보상을 갖춘 다단계 정산을 조율합니다. 스키마 레지스트리 덕에 계약 팀은 마흔 개 소비 시스템에 걸친 동기화된 배포 없이 자기 이벤트를 진화시킬 수 있으며, 이것이 옛 ESB가 강요한 바로 그 취약성입니다.
정부. 한 국가 세무 기관은 시민과 감사자에게 “내 평가가 왜 이렇게 나왔는가”에 방어 가능한 답을 줘야 합니다. 평가 도메인을 이벤트 소싱으로 모델링하여, 모든 변경이 순서가 있는 로그의 불변 이벤트이고 현재 평가는 그 이벤트의 재생입니다. 시민이 수치에 이의를 제기하면 담당자가 과거 어느 날짜의 정확한 상태든 재구성해 그것을 낳은 사실의 순서를 보여 줍니다. 기관 간 데이터 공유는 최소 한 번 전달의 내구성 있는 토픽 위에서 돌고, 각 기관의 소비자가 이벤트 ID로 중복 제거하므로 재전달된 레코드가 중복 사례를 만들지 않습니다. 이벤트 로그는 감독 기관이 요구하는 감사 추적을 겸해, 컴플라이언스 의무를 설계의 부산물로 바꿉니다.
비즈니스 사례: 동기, ROI, TCO
이벤트 기반 아키텍처의 수익은 한 가지, 곧 시간에 따른 통합 비용이 지배합니다. 점대점 통합은 그 비용이 연결 수에 따라 커지게 하고, 연결 수는 시스템 수보다 빠르게 늘어서, 통합이 전달 역량을 먹어 치우는 세금이 됩니다. 이벤트는 이를 평평하게 합니다. 팀들이 사실의 공유 스트림을 통해 통합하고, 새 소비자는 구독으로 합류하며, 생산자는 버전 관리되는 스키마 뒤에서 진화하므로, 다음 통합의 한계 비용이 급격히 떨어집니다. 이것이 리더십에게 말할 핵심 ROI 이야기입니다. 날것의 성능이 아니라 많은 팀에 걸친 변경 비용의 누적되는 감소입니다.
믿어지도록 총소유비용을 정직하게 밝히십시오. 운영할 브로커 인프라, 유지할 스키마 거버넌스, 비동기 시스템이 정말 디버깅하기 더 어렵기 때문에 더 가파른 운영 학습 곡선을 떠안으므로, 관측 가능성 투자를 처음부터 예산에 넣으십시오. 이를 맞는 곳에 이벤트를 채택하지 않는 비용과 저울질하십시오. 모든 변경이 여러 팀의 조율 프로젝트인 굳어진 통합 계층, 아무도 건드리지 못하는 병목이 된 레거시 ESB, 옛 것을 방해하지 않고는 기능을 추가할 수 없는 상태입니다. 취약한 점대점 배선을 대체하는 기업과 내구성 있는 감사 추적을 만드는 정부에서는 감사와 분리 필요가 실재하는 곳에서 수익이 가장 강합니다. 그런 필요가 없는 곳에서는 더 단순한 동기 설계의 TCO가 더 낮다는 것이 정직한 답이며, 그렇게 말해야 합니다.
안티패턴과 함정
- 위장한 분산된 모놀리스. 모두 함께 배포해야 하고 서로의 내부 이벤트에 의존하는 서비스. 브로커를 더했지만 결합은 유지해 두 스타일의 단점을 모두 지닙니다.
- 명령으로서의 이벤트. “나를 위해 이 특정한 일을 해 주세요”를 뜻하는 “이벤트”를 발행해 추가 지연과 더 나쁜 추적성으로 강한 결합을 다시 만드는 것.
- 정확히 한 번 전달 가정. 브로커가 불가피하게 메시지를 재전달할 때 깨지거나, 이중 청구하거나, 레코드를 중복시키는 소비자.
- 모든 것에 이벤트 소싱. 이력이 필요 없던 단순한 생성-읽기-갱신-삭제 도메인에 이벤트 소싱과 CQRS를 적용해 이득 없이 가파른 복잡성을 사는 것.
- 스키마 거버넌스 없음. 생산자가 이벤트 모양을 마음대로 바꿔, 호환성 검사 없이 멀리서 다운스트림 소비자를 깨뜨리는 것.
- 아웃박스 무시. 데이터베이스 쓰기와 이벤트 발행을 두 별개 단계로 해서, 둘 사이의 충돌이 이벤트를 조용히 잃거나 유령 이벤트를 내보내는 것.
- 데드 레터 처리 없음. 독약 메시지 하나가 파티션을 막거나, 실패한 메시지가 잡아서 검사할 큐 없이 사라지는 것.
- 보이지 않는 흐름. 상관 ID도, 컨슈머 랙 경보도, 추적도 없는 비동기 처리로, 실패가 누락된 데이터가 될 때까지 조용히 남는 것.
성숙도 모델
- 1단계, 시작: 통합이 즉흥적인 점대점 호출이거나, 브로커가 있지만 동기 요청/응답처럼 쓰입니다. 중복이 소비자를 깨뜨리고, 스키마 규율이나 홉을 가로지르는 추적이 없으며, 실패한 메시지는 조용히 사라집니다.
- 2단계, 발전: 일부 흐름에서 브로커나 로그가 실제로 쓰이고, 몇몇 팀이 기본 실천을 갖습니다. 소비자가 멱등해지고 있고 데드 레터 큐가 일부 실패를 잡습니다. 그러나 접근은 팀마다 일관되지 않고, 이벤트와 명령이 여전히 뒤섞이고, 스키마는 비공식 합의로 바뀌며, 관측 가능성은 얇습니다.
- 3단계, 표준화: 이벤트, 명령, 메시지가 조직 전체에서 의도적으로 구별됩니다. 소비자는 표준으로 멱등하고, 스키마는 문서화되고 시행되는 호환성 정책이 있는 레지스트리에 있으며, 트랜잭셔널 아웃박스가 전달을 보장합니다. 보상이 있는 사가가 다중 서비스 트랜잭션을 처리하고, 상관 ID가 모든 홉을 통해 전파되며, 이런 패턴은 각 팀에 맡겨지지 않고 적혀서 일관되게 적용됩니다.
- 4단계, 관리: 이벤트 플랫폼이 기준선에 대한 데이터로 측정되고 통제됩니다. 컨슈머 랙, 데드 레터 큐 깊이, 재전달 비율, 처리 지연이 합의된 임계값을 가진 경보 지표로 추적되며 아무도 보지 않는 대시보드가 아닙니다. 스키마 호환성은 생산자가 변경을 출시하기 전에 자동으로 검증되고, 재생, 실패 처리, 멱등성은 바라는 것이 아니라 고정된 주기로 테스트됩니다. 주어진 상호작용이 이벤트여야 하는지 동기 호출이어야 하는지는 증거로 결정되고, 각 브로커의 용량, 보존 비용, 신뢰성은 목표에 대해 검토됩니다.
- 5단계, 오케스트레이션: 이벤트 기반 스타일과 동기 스타일이 상호작용마다 몸에 밴 것처럼 선택되고, 이벤트 소싱과 CQRS는 감사와 질의 필요가 정당화하는 곳에 정확히, 그 외에는 적용되지 않습니다. 비동기 흐름이 동기 흐름만큼 관측 가능하고, 플랫폼은 필요가 이동함에 따라(죽은 토픽 퇴역, 스키마 거버넌스 진화, 파티션 재균형) 지속적으로 개선되며, 메시징은 더 넓은 아키텍처 및 감사 전략과 통합되어 조직이 비즈니스와 의무가 변함에 따라 이벤트 설계를 적응시킵니다.
논의를 위한 아이디어
- 현재의 “이벤트” 중 어느 것이 몰래 명령이며, 정직하게 모델링하면 어떤 결합을 제거하겠습니까?
- 내일 하루치 이벤트 전체를 소비자에게 재생한다면 무엇이 깨지겠으며, 그것이 멱등성과 재생 안전에 대해 무엇을 알려 줍니까?
- 도메인의 어느 부분이 이벤트 소싱의 감사 추적을 진정으로 필요로 하고, 어느 부분이 소싱하면 복잡해지기만 할 단순한 상태입니까?
- 위험한 빅뱅 전환 없이 레거시 엔터프라이즈 서비스 버스나 점대점 통합의 그물에서 어떻게 이전하겠습니까?
- 가장 중요한 다중 서비스 프로세스는 보상이 있는 진짜 오케스트레이션 사가입니까, 아니면 어느 단일 장소도 기술하지 않는 창발적 코레오그래피입니까?
핵심 요점
- 이벤트 기반 아키텍처는 분리, 독립적 확장, 재생, 감사 추적을 사고 이해 가능성과 운영 복잡성을 비용으로 치릅니다. 이점이 실재하는 상호작용마다 선택하십시오.
- 이벤트(일어난 사실), 명령(행동 요청), 메시지(봉투)를 구별하십시오. 혼동이 실제 결합 실수를 낳습니다.
- 최소 한 번 전달로 설계하고 모든 소비자를 멱등하게 만드십시오. 정확히 한 번 전달은 신화이고, 정확히 한 번의 효과는 엔지니어링의 성취입니다.
- 순서는 파티션별이고, 스키마는 레지스트리에 속하는 계약이며, 아웃박스, 데드 레터 큐, 백프레셔는 메시징을 프로덕션 수준으로 만드는 배관입니다.
- 2단계 커밋 대신 보상이 있는 사가를 쓰고, 이벤트 소싱과 CQRS는 감사와 질의 필요가 가파른 비용을 정당화하는 곳에만 손을 뻗으십시오.
- 비동기 흐름은 실패할 때 조용하므로, 상관 ID, 컨슈머 랙 모니터링, 종단 간 추적이 운영 가능한 시스템과 보이지 않는 시스템의 차이입니다.
참고 문헌과 더 읽을거리
- Martin Kleppmann, Designing Data-Intensive Applications
- Gregor Hohpe and Bobby Woolf, Enterprise Integration Patterns
- Chris Richardson, Microservices Patterns (sagas, transactional outbox, CQRS)
- Sam Newman, Building Microservices
- Ben Stopford, Designing Event-Driven Systems
- Adam Bellemare, Building Event-Driven Microservices
- Vaughn Vernon, Implementing Domain-Driven Design (event sourcing and CQRS)
- Martin Fowler, “Event Sourcing” and “CQRS” (martinfowler.com articles)
- Hector Garcia-Molina and Kenneth Salem, “Sagas” (1987)