C4 모델을 활용한 Event-Driven 아키텍처 문서화
Event-driven 아키텍처는 확장 가능하고 느슨하게 결합된 시스템을 구축하기 위한 기본이 되었습니다. 서비스가 무언가 발생하면 이벤트를 발행합니다. 다른 서비스가 그 이벤트를 구독하고 반응합니다. 발행자는 누가 듣고 있는지 모릅니다. 구독자는 누가 발행했는지 모릅니다. 시스템이 분리되고, 탄력적이며, 유연합니다.
그러나 거의 보이지 않습니다.
Event-driven 시스템의 전통적인 C4 다이어그램을 보면, 메시지 브로커를 가리키는 화살표가 있는 서비스들이 보입니다. 서비스 A가 Kafka에 발행합니다. 서비스 B가 Kafka에서 소비합니다. 하지만 어떤 이벤트가 그들 사이를 흐르나요? 스키마는 무엇인가요? 다른 누가 듣고 있나요? 소비자가 실패하면 어떻게 되나요? 다이어그램은 배관을 보여주지만 행동을 숨깁니다.
Event-driven 아키텍처를 문서화하려면 보이지 않는 것을 가시화하는 기술이 필요합니다 -- 인프라(브로커, 큐, 토픽)뿐만 아니라 이벤트 자체, 그 흐름, 스키마, 보장을 보여주는 것. 이 가이드는 C4 모델을 사용하여 이를 수행하는 방법과 Archyl의 이벤트 채널 기능이 event-driven 시스템을 아키텍처 문서의 일급 시민으로 만드는 방법을 다룹니다.
Event-Driven 시스템이 문서화하기 어려운 이유
Event-driven 아키텍처는 동기적 Request-Response 시스템에 존재하지 않는 여러 문서화 과제를 도입합니다.
보이지 않는 제어 흐름
동기 시스템에서는 호출 체인을 따라 요청을 추적할 수 있습니다. 서비스 A가 서비스 B를 호출하고, 서비스 B가 서비스 C를 호출합니다. 제어 흐름이 명시적이고 코드에서 가시적입니다.
Event-driven 시스템에서 제어 흐름은 암묵적입니다. 서비스 A가 OrderCreated 이벤트를 발행합니다. 어딘가에서 서비스 B가 그 이벤트에 반응하여 재고를 예약합니다. 다른 곳에서 서비스 C가 반응하여 확인 이메일을 보냅니다. 서비스 A는 B와 C에 대해 모릅니다. 제어 흐름은 발행자의 코드가 아닌 이벤트 구독에 의해 정의됩니다.
이 간접성은 단일 서비스의 코드를 읽어서는 시스템의 행동을 이해할 수 없다는 것을 의미합니다. 서비스 간의 이벤트 흐름을 보여주는 더 높은 수준의 뷰가 필요합니다 -- 그리고 그 뷰가 바로 아키텍처 문서가 제공해야 하는 것입니다.
다대다 관계
동기 아키텍처에서 관계는 일반적으로 일대일 또는 일대소수입니다. 서비스 A가 서비스 B를 호출합니다. 관계가 직접적이고 API 호출로 문서화됩니다.
Event-driven 아키텍처에서 관계는 다대다입니다. 단일 이벤트 유형에 하나의 생산자와 다섯 명의 소비자가 있을 수 있습니다. 단일 서비스가 열 개의 다른 생산자로부터 이벤트를 소비할 수 있습니다. 관계 그래프가 동기 시스템보다 더 밀도 있고 복잡합니다.
전통적인 아키텍처 다이어그램은 이 밀도에 어려움을 겪습니다. 모든 생산자에서 모든 소비자로 모든 토픽을 통해 화살표를 그리면 스파게티 접시처럼 보이는 다이어그램이 됩니다. 적절한 추상화 수준에서 이벤트 흐름을 보여주는 문서화 접근이 필요합니다.
스키마 진화
이벤트 스키마는 시간에 따라 진화합니다. OrderCreated 이벤트가 5개 필드로 시작하여 2년에 걸쳐 15개로 성장할 수 있습니다. 소비자가 특정 필드에 의존할 수 있습니다. 스키마 변경은 이전 버전과 호환되지 않으면 소비자를 깨뜨릴 수 있습니다.
현재 스키마를 문서화하는 것은 필요하지만 충분하지 않습니다. 스키마 버전 관리 전략, 호환성 보장, 호환되지 않는 변경의 이력도 문서화해야 합니다.
Eventual Consistency
Event-driven 시스템은 본질적으로 eventually consistent합니다. 서비스 A가 이벤트를 발행하면, 서비스 B가 밀리초 후에 처리하거나 몇 분 후에 처리할 수 있습니다(소비자가 뒤처지거나 재시도가 필요한 경우). 시스템은 그 시간 동안 불일치 상태에 있습니다.
문서는 이러한 일관성 경계를 포착해야 합니다. 시스템의 어떤 부분이 강한 일관성인가? 어떤 부분이 eventually consistent인가? 예상 전파 지연은 얼마인가? 불일치 시간 동안 무엇이 발생하는가?
Dead Letter Queue와 오류 처리
이벤트 소비자가 메시지 처리에 실패하면, 이벤트는 일반적으로 Dead Letter Queue (DLQ)로 갑니다. 하지만 그 후 어떻게 되나요? 누가 DLQ를 모니터링하나요? 재시도 전략은 무엇인가요? 포이즌 메시지는 어떻게 처리되나요?
이러한 오류 처리 패턴은 시스템 행동에 핵심적이지만 거의 문서화되지 않습니다. 아키텍처의 일부이며, 문서에서 가시적이어야 합니다.
C4로 Event-Driven 시스템 모델링
C4 모델은 각 레벨을 사용하는 방식에 몇 가지 적응을 가하면 event-driven 아키텍처를 효과적으로 표현할 수 있습니다.
System Context: 호출이 아닌 데이터 흐름에 집중
System Context 레벨에서 event-driven과 동기 아키텍처는 비슷해 보입니다. 시스템이 사용자 및 외부 시스템과 상호작용합니다. 핵심 차이는 관계의 레이블링 방식입니다.
"호출한다"나 "쿼리한다" 대신, 데이터 흐름을 설명하는 레이블을 사용합니다:
- "주문 이벤트를 전송한다"
- "결제 확인을 수신한다"
- "분석 이벤트를 발행한다"
이 레이블은 높은 수준의 뷰를 구현 세부사항으로 어지럽히지 않으면서 통신의 비동기적 특성을 암시합니다.
Container 다이어그램: 브로커를 가시화하기
Container 다이어그램은 event-driven 아키텍처가 차별화되는 곳입니다. 메시지 브로커(Kafka, RabbitMQ, Amazon SQS/SNS, Google Pub/Sub)는 보이지 않는 구현 세부사항이 아니라 다이어그램의 일급 컨테이너여야 합니다.
Event-driven 전자상거래 시스템의 Container 다이어그램 예시:
systems:
- name: E-Commerce Platform
type: software_system
containers:
- name: Order Service
type: service
technologies: [Go, PostgreSQL]
- name: Inventory Service
type: service
technologies: [Java, PostgreSQL]
- name: Notification Service
type: service
technologies: [Python, Redis]
- name: Analytics Service
type: service
technologies: [Python, ClickHouse]
- name: Event Bus
type: queue
technologies: [Apache Kafka]
relationships:
- from: Order Service
to: Event Bus
label: "OrderCreated, OrderCancelled 발행"
- from: Event Bus
to: Inventory Service
label: "주문 이벤트 전달"
- from: Event Bus
to: Notification Service
label: "주문 및 결제 이벤트 전달"
- from: Event Bus
to: Analytics Service
label: "모든 도메인 이벤트 전달"
- from: Inventory Service
to: Event Bus
label: "InventoryReserved, InventoryReleased 발행"
Event Bus가 다이어그램 중심에 있고, 관계가 통과하는 이벤트 유형을 명시적으로 이름으로 지정합니다. 이렇게 하면 모든 생산자와 모든 소비자 사이에 직접 화살표를 만들지 않고도 이벤트 흐름이 가시화됩니다.
이벤트 채널: 일급 개념
메시지 브로커 내의 개별 토픽, 큐, 스트림도 자체 문서가 필요합니다. 각 이벤트 채널에는 시스템 이해에 중요한 속성이 있습니다:
- 채널 이름: Kafka 토픽, RabbitMQ 큐, SQS 큐 이름
- 이벤트 유형: 이 채널을 통해 흐르는 이벤트
- 생산자: 이 채널에 발행하는 서비스
- 소비자: 이 채널에서 소비하는 서비스
- 직렬화: 이벤트 인코딩 방식 (JSON, Avro, Protobuf)
- 파티셔닝 전략: 파티션 간 이벤트 분배 방식
- 보존 정책: 이벤트 보관 기간
- 순서 보장: 순서 보존 여부와 그 세분성
Archyl에서 이벤트 채널은 전용 엔티티 유형입니다. 이벤트 채널을 생성하고, 속성을 지정하고, 생산 및 소비하는 서비스에 연결합니다. 이것은 이벤트 흐름의 구조화되고 쿼리 가능한 모델을 만듭니다.
예를 들어, "orders" 이벤트 채널은 다음과 같이 문서화될 수 있습니다:
- 이름: orders
- 브로커: Kafka
- 생산자: Order Service
- 소비자: Inventory Service, Notification Service, Analytics Service, Billing Service
- 이벤트 유형: OrderCreated, OrderUpdated, OrderCancelled, OrderCompleted
- 직렬화: Schema Registry와 함께 Avro
- 파티셔닝: 주문 ID 기준
- 보존: 7일
이 수준의 세부사항은 보이지 않는 인프라를 가시적이고 쿼리 가능하게 만듭니다. 개발자가 누가 주문 이벤트를 소비하는지 알아야 할 때, 답이 문서화되어 있고 찾을 수 있습니다.
Component 다이어그램: 이벤트 핸들러와 발행자
Component 레벨에서 event-driven 서비스는 복잡한 서비스에 대해 문서화할 가치가 있는 독특한 내부 구조를 가집니다:
- 이벤트 핸들러: 특정 이벤트 유형을 소비하고 처리하는 컴포넌트
- 이벤트 발행자: 이벤트를 생산하는 컴포넌트
- 사가 / 프로세스 매니저: 이벤트를 통해 다단계 워크플로를 조율하는 컴포넌트
- 프로젝션: 이벤트 스트림에서 읽기 모델을 구축하는 컴포넌트
Order Service의 Component 다이어그램은 다음을 포함할 수 있습니다:
- Order Controller -- 주문 관리를 위한 HTTP 요청 처리
- Order Processor -- 주문 생성 및 검증을 위한 핵심 비즈니스 로직
- Event Publisher -- Kafka에 OrderCreated, OrderUpdated, OrderCancelled 발행
- Payment Event Handler -- PaymentProcessed 및 PaymentFailed 이벤트 소비
- Order Saga -- 서비스 간 주문 이행 워크플로 관리
서비스가 안무(choreography) 또는 오케스트레이션(orchestration) 패턴에 참여할 만큼 복잡할 때 이러한 컴포넌트를 문서화합니다.
일반적인 Event-Driven 패턴 문서화
Event-driven 시스템에서 특정 패턴이 반복적으로 나타납니다. 이를 명시적으로 문서화하면 팀이 코드에서 패턴을 리버스 엔지니어링하지 않아도 됩니다.
Event Sourcing
Event-sourced 시스템에서 엔티티의 상태는 스냅샷이 아닌 이벤트 시퀀스에서 도출됩니다. 이벤트 스트림이 소스 오브 트루스이고, 현재 상태는 프로젝션입니다.
Event Sourcing 문서화 방법:
- 어떤 엔티티가 event-sourced인지 식별
- 각 엔티티의 이벤트 스트림에 있는 이벤트 유형 나열
- 스트림에서 읽기 모델을 도출하는 프로젝션 문서화
- 스냅샷 전략(있는 경우) 기록
CQRS (Command Query Responsibility Segregation)
CQRS는 쓰기 작업(명령)과 읽기 작업(쿼리)을 분리하며, 종종 이벤트를 사용하여 읽기 모델을 쓰기 모델과 동기화합니다.
CQRS 문서화 방법:
- C4 모델에서 명령 측 컨테이너와 쿼리 측 컨테이너를 명확히 분리
- 쓰기 측에서 읽기 측으로의 이벤트 흐름 문서화
- 일관성 모델 기록 (읽기 모델이 얼마나 뒤처질 수 있는지)
안무 vs. 오케스트레이션
안무에서 서비스는 이벤트에 독립적으로 반응합니다. 중앙 코디네이터가 없습니다. 오케스트레이션에서 중앙 서비스(오케스트레이터 또는 사가)가 명령을 보내고 응답을 듣어 워크플로를 조율합니다.
각 워크플로에 어떤 패턴을 사용하는지 문서화합니다. 안무를 사용하면, 예상되는 이벤트 시퀀스와 참여 서비스를 문서화합니다. 오케스트레이션을 사용하면, 사가의 상태 머신과 발행하는 명령을 문서화합니다.
Dead Letter Queue와 재시도 패턴
이벤트 처리를 위한 오류 처리 전략을 문서화합니다:
- 어떤 이벤트에 Dead Letter Queue가 있는가?
- 재시도 정책은 무엇인가 (횟수, 백오프 전략)?
- DLQ 이벤트 모니터링 및 재처리 담당자는 누구인가?
- DLQ 누적에 대한 알림은 어떻게 설정되어 있는가?
Archyl에서 DLQ를 기본 채널에 연결된 추가 이벤트 채널로 모델링할 수 있습니다. 이렇게 하면 오류 처리 인프라가 아키텍처 문서에서 가시화됩니다.
이벤트 스키마 문서화
이벤트 스키마는 생산자와 소비자 사이의 계약입니다. 동기 시스템의 API 계약과 같은 수준의 문서화가 필요합니다.
스키마 문서
각 이벤트 유형에 대해 문서화합니다:
- 이벤트 이름: 명확한 도메인별 이름 (OrderCreated, GenericEvent가 아님)
- 스키마 버전: 스키마의 현재 버전
- 필드: 유형, 설명, 필수/선택 여부를 포함한 모든 필드
- 예시 페이로드: 대표적인 JSON/Avro/Protobuf 예시
- 호환성: 스키마가 전방 호환, 후방 호환, 완전 호환인지
Archyl의 API Contract 기능을 사용하여 REST 및 gRPC 명세서와 함께 이벤트 스키마를 문서화할 수 있습니다. 계약을 이벤트 채널에 연결하여 스키마와 인프라 사이의 직접 연결을 만듭니다.
스키마 진화 전략
팀의 스키마 진화 접근 방식을 문서화합니다:
- 스키마 레지스트리(Confluent Schema Registry, AWS Glue)를 사용하나?
- 어떤 호환성 모드가 시행되나 (backward, forward, full)?
- 소비자에게 호환되지 않는 변경은 어떻게 전달되나?
- 이전 스키마 버전의 폐기 프로세스는 무엇인가?
이 정보는 이벤트 인프라에 연결된 ADR에 속합니다. 전체 시스템에 영향을 미치는 결정이며, 한 번, 명확하게 문서화되고, 모든 팀이 참조해야 합니다.
이벤트 흐름 시각화
정적 다이어그램은 이벤트 인프라를 보여줄 수 있지만, 이벤트 흐름 -- 비즈니스 프로세스를 구현하는 이벤트의 시퀀스 -- 을 보여주는 데는 어려움을 겪습니다.
비즈니스 프로세스에 플로우 사용
Archyl의 Flow 기능으로 비즈니스 프로세스를 구현하는 이벤트 시퀀스를 문서화할 수 있습니다. 예를 들어, "주문 제출" 플로우는 다음을 보여줄 수 있습니다:
- 고객이 Web App을 통해 주문 제출
- API Gateway가 요청을 Order Service에 전달
- Order Service가 주문을 검증하고 영속화
- Order Service가 Kafka에 OrderCreated 이벤트 발행
- Inventory Service가 OrderCreated를 소비하고 재고 예약
- Inventory Service가 InventoryReserved 이벤트 발행
- Payment Service가 InventoryReserved를 소비하고 결제 처리
- Payment Service가 PaymentProcessed 이벤트 발행
- Notification Service가 PaymentProcessed를 소비하고 확인 이메일 발송
이 플로우는 event-driven 아키텍처에서 나타나는 엔드투엔드 행동을 보여줍니다. 어떤 단일 서비스의 코드도 이 플로우를 드러내지 않습니다 -- 아키텍처 레벨에서만 존재합니다.
다른 뷰를 위한 오버레이 사용
Event-driven 아키텍처의 다른 측면을 보여주는 오버레이를 만듭니다:
- 이벤트 흐름 오버레이: 동기 통신을 숨기고 이벤트 관련 관계만 강조
- 생산자/소비자 오버레이: 서비스가 생산자인지, 소비자인지, 둘 다인지에 따라 색상 구분
- 오류 처리 오버레이: DLQ, 재시도 경로, 모니터링 표시
오버레이를 사용하면 단일 아키텍처 모델에서 집중된 뷰를 만들어, 여러 중복 다이어그램이 필요 없습니다.
모범 사례
기술적 작업이 아닌 도메인 행동으로 이벤트 명명
DataUpdated, MessageSent, RecordInserted가 아니라 OrderCreated, PaymentFailed, InventoryReserved와 같은 이름을 사용합니다. 도메인별 이름은 아키텍처 레벨에서 이벤트 흐름을 읽기 좋게 만듭니다.
"왜 동기가 아닌가" 결정 문서화
각 event-driven 상호작용에는 동기 대신 비동기를 사용하기로 한 결정이 있었습니다. 그 결정을 문서화합니다. Order Service가 Inventory Service를 직접 호출하는 대신 이벤트를 발행하는 이유는 무엇인가? 답(분리, 탄력성, 확장성)은 ADR에 기록되어야 합니다.
이벤트 채널 문서를 코드 가까이 유지
이벤트 스키마가 코드에 정의되어 있으면(Protobuf 파일, Avro 스키마, JSON Schema), 아키텍처 문서를 그 파일에 연결합니다. 이렇게 하면 추상적 문서와 구체적 구현 사이의 연결이 만들어집니다.
아키텍처 리뷰에서 이벤트 흐름 검토
분기별 아키텍처 리뷰에서 문서화된 이벤트 흐름을 살펴봅니다. 물어봅니다:
- 문서화되지 않은 새 이벤트 유형이 있는가?
- 더 이상 사용되지 않는 문서화된 이벤트 유형이 있는가?
- 문서 업데이트 없이 새 소비자가 추가되었는가?
- 문서화된 스키마가 여전히 정확한가?
Archyl의 드리프트 감지가 이러한 질문에 자동으로 답하는 데 도움이 되지만, 정기적인 인간 검토가 자동화된 검사가 놓치는 것을 잡아냅니다.
결론
Event-driven 아키텍처 문서화는 보이지 않는 것을 가시화하기 위한 의도적인 노력이 필요합니다. 이벤트는 설계상 생산자와 소비자를 분리합니다. 이것은 아키텍처적 강점이지만 문서화 과제입니다.
C4 모델이 프레임워크를 제공합니다: 큰 그림을 위한 System Context, 메시지 브로커를 일급 요소로 포함하는 Container 다이어그램, 상세한 토픽 및 큐 문서를 위한 이벤트 채널, 복잡한 이벤트 핸들러와 사가를 위한 Component 다이어그램.
Archyl이 도구를 제공합니다: 일급 엔티티로서의 이벤트 채널, event-driven 비즈니스 프로세스를 문서화하는 플로우, 이벤트 스키마를 위한 API 계약, 집중된 뷰를 위한 오버레이, 문서화되지 않은 변경을 잡는 드리프트 감지.
Event-driven 아키텍처를 설계하는 것과 같은 방식으로 문서화하세요: 의도를 가지고, 구조를 갖추고, 시스템의 행동이 개별 서비스가 아닌 서비스 간의 상호작용에서 나타난다는 이해와 함께.
Archyl로 시작하기로 event-driven 아키텍처를 가시적이고, 문서화되고, 이해 가능하게 만드세요.