이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 이벤트 모델링 리뷰에서 이름보다 먼저 봐야 할 기준은 무엇인가?
- 어떤 event 설계가 재처리, 중복 처리, schema 변경에서 문제를 만드는가?
- Java/Spring 코드 리뷰에서 outbox와 event schema를 어떻게 점검하는가?
- 이벤트 모델링 실수를 운영 장애로 번지기 전에 어떻게 발견하는가?
개요
이벤트 모델링 리뷰는 event 이름이 예쁜지 확인하는 시간이 아니다.
발생 조건, key, version, payload 의미, 취소/삭제 표현, 중복 처리, replay 가능성을 확인하는 시간이다.
좋은 event는 한 번 발행된 뒤 여러 consumer가 오래 읽을 수 있어야 한다.
나쁜 event는 처음에는 잘 동작해 보이다가 schema 변경, backfill, consumer 추가 때 문제를 만든다.
원리
event review는 다음 순서로 진행한다.
- 이 event가 실제 비즈니스 사건인가?
- 언제 한 번만 발생하는가?
- 어떤 key로 partitioning, idempotency, replay를 할 수 있는가?
- consumer가 모르는 필드나 version을 견딜 수 있는가?
- 취소, 삭제, 보정, 늦은 도착을 어떻게 표현하는가?
이 질문에 답하지 못하면 아직 모델링이 끝난 것이 아니다.
흔한 실수
| 실수 | 증상 | 결과 |
|---|---|---|
| 상태명을 event로 사용 | OrderStatusChanged 남발 | consumer가 의미를 추측 |
| key 누락 | payment id, line id 없음 | fact grain 생성 실패 |
| version 없음 | payload 의미 변경 | replay 실패 |
| 삭제 event 없음 | row delete만 수행 | search/mart 잔존 |
| DTO 재사용 | 화면 필드 변경에 event 변경 | consumer 장애 |
| audit/event 혼용 | actor 정보와 business event 섞임 | 개인정보와 계약 충돌 |
실수는 대부분 도구 문제가 아니라 의미와 계약을 명확히 하지 않은 데서 온다.
리뷰 체크리스트
event name:
- 과거형 비즈니스 사건인가?
- 상태명이나 명령형 이름이 아닌가?
emission:
- 어떤 transaction에서 발생하는가?
- 중복 callback이나 retry 때 한 번만 남는가?
schema:
- eventId, aggregateId, occurredAt, version이 있는가?
- required field와 optional field가 구분되어 있는가?
consumer:
- idempotency key가 명확한가?
- replay 시 같은 결과가 나오는가?
change:
- 필드 추가/삭제/의미 변경 정책이 있는가?
- consumer owner가 리뷰했는가?리뷰 체크리스트는 문서에만 있으면 부족하다.
PR template이나 architecture review 항목으로 들어가야 한다.
나쁜 예
{
"type": "OrderUpdated",
"orderId": 10042,
"status": "PAID",
"amount": 49000,
"updatedAt": "2026-06-30T10:15:30+09:00"
}이 event는 어떤 일이 발생했는지 모호하다.
결제 승인인지, 관리자 수정인지, 재동기화인지 알 수 없다.
event id와 version도 없어 중복 처리와 schema 변경에 약하다.
좋은 방향
{
"eventId": "evt-10042-002",
"eventType": "OrderPaid",
"eventVersion": 1,
"aggregateType": "Order",
"aggregateId": "10042",
"occurredAt": "2026-06-30T10:15:30+09:00",
"payload": {
"orderId": 10042,
"paymentId": "pay-555",
"paidAmount": "49000.00",
"currency": "KRW"
}
}이 event는 발생한 사건, 중복 판단 key, version, business payload가 분리되어 있다.
consumer가 검색 색인, warehouse fact, 알림을 만들 때 필요한 최소 계약도 보인다.
Spring 코드 리뷰 지점
@Transactional
public void handlePaymentApproved(PaymentApprovedMessage message) {
Order order = orderRepository.getById(message.orderId());
order.markPaid(message.paymentId(), message.amount());
outboxRepository.save(OutboxEvent.from(order.paidEvent()));
}리뷰에서는 markPaid가 중복 callback에 안전한지 본다.
또한 state 변경과 outbox 저장이 같은 transaction 안에 있는지 확인한다.
event 발행을 transaction 밖에서 직접 Kafka로 보내면 dual write 문제가 생길 수 있다.
판단 축
- event 이름은 past-tense business fact로 둔다.
- command, state, audit action을 event와 섞지 않는다.
- event id와 aggregate id를 모두 둔다.
- payload는 consumer가 필요한 business key를 포함한다.
- schema 변경과 replay 테스트를 같이 본다.
실패 모델
- 같은 결제 callback이 두 번 와서
OrderPaid가 두 번 발행된다. OrderUpdated만 있어 warehouse가 결제 fact와 취소 fact를 구분하지 못한다.- product category key가 없어 mart dimension join이 실패한다.
- 필드 의미가 바뀌었는데 event version이 그대로다.
- 삭제와 개인정보 익명화 event가 없어 downstream data가 남는다.
운영 지표
event_review_defect_count: 리뷰에서 발견한 event 설계 결함 수다.duplicate_event_emission_count: 같은 business event 중복 발행 수다.missing_required_key_count: 필수 key가 비어 있는 event 수다.consumer_deserialization_error_count: schema 변경으로 인한 consumer 오류다.replay_mismatch_count: replay 결과가 기존 projection과 다른 수다.
품질 검증
SELECT event_type, COUNT(*) AS count
FROM outbox_event
WHERE created_at >= now() - interval '1 day'
GROUP BY event_type;SELECT event_type, COUNT(*) AS missing_event_id_count
FROM outbox_event
WHERE event_id IS NULL OR aggregate_id IS NULL
GROUP BY event_type;event 품질도 DB 데이터처럼 검증해야 한다.
누락된 key는 장애가 난 뒤가 아니라 발행 직후 발견해야 한다.
개인 프로젝트 기준
개인 프로젝트에서는 간단한 PR checklist로 충분하다.
event 이름, event id, aggregate id, version, occurredAt, 재처리 테스트만 확인해도 많은 실수를 줄일 수 있다.
Kafka가 없어도 outbox table과 처리 이력으로 리뷰 기준을 연습할 수 있다.
기업 운영 기준
기업에서는 event catalog나 schema registry와 함께 리뷰 기준을 운영한다.
producer 팀이 event를 바꿀 때 consumer owner, 데이터 팀, 보안 요구를 함께 확인한다.
중요 event는 replay test와 compatibility check가 배포 gate가 되어야 한다.
실전 팁
- event 이름이
Updated,Changed,Synced로 끝나면 의미가 충분한지 다시 묻는다. - event를 새로 만들 때 consumer가 없더라도 미래 replay를 고려한다.
- 삭제와 취소는 처음부터 별도 event로 모델링한다.
- schema 변경 PR에는 old event sample과 new event sample을 같이 둔다.
- event 리뷰는 code owner뿐 아니라 downstream owner를 포함한다.
위험 신호!
- event 설계 문서가 payload 예시 하나뿐이다.
- event 발생 조건이 테스트 코드에만 숨어 있다.
- consumer가 payload 필드를 null-safe하게 읽지 못한다.
- replay를 해 본 적이 없는데 event log를 신뢰한다고 말한다.
- audit log와 domain event가 같은 topic에 섞여 있다.
확인 질문
확인 질문
- 이벤트 모델링 리뷰에서 이름보다 먼저 봐야 할 것은 무엇인가?
- 발생 조건, key, version, consumer 계약, 취소/삭제 표현, replay 가능성이다.
OrderUpdated같은 event 이름이 위험한 이유는 무엇인가?
- 어떤 비즈니스 사건인지 모호해 consumer가 결제, 취소, 보정 의미를 추측하게 된다.
- event 설계가 재처리에 안전한지 확인하려면 무엇을 테스트해야 하는가?
- 같은 event를 두 번 처리해도 결과가 안전한지, 과거 version event를 replay해도 projection이 맞는지 테스트한다.
참고 문서
- Martin Fowler, Domain Event
- Apache Kafka Documentation
- Confluent Schema Registry Documentation
- Martin Kleppmann, Designing Data-Intensive Applications