- Published on
EventStorming 1일 워크숍으로 DDD 바운디드 컨텍스트 나누기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 다른 팀이 같은 단어를 다르게 쓰고, 같은 기능이 여러 서비스에 중복 구현되며, 장애가 나면 "어디가 주인인지"부터 싸우는 상황은 대부분 경계가 불명확한 도메인에서 시작합니다. DDD의 바운디드 컨텍스트는 이 문제를 해결하는 강력한 도구지만, 문서만으로는 합의가 잘 안 됩니다.
EventStorming은 도메인 이벤트를 중심으로 "실제 일"을 빠르게 가시화하고, 그 결과물로 바운디드 컨텍스트 경계를 합리적으로 도출하는 데 매우 효과적입니다. 이 글은 1일(약 6~7시간) 워크숍으로 바운디드 컨텍스트를 나누는 실전 운영 방법, 산출물, 그리고 흔한 함정을 정리합니다.
워크숍 목표와 산출물 정의
목표(1일에 현실적으로 가능한 범위)
- 핵심 시나리오 2~4개를 End-to-End로 이벤트 플로우로 정리
- 용어 충돌과 책임 경계 후보를 명확히 드러내기
- 바운디드 컨텍스트 후보 2~6개와 각 컨텍스트의 책임(Ownership) 합의
- 컨텍스트 간 통합 방식(이벤트, API, ACL 등) 초안 도출
산출물(회의 끝나고 남아야 하는 것)
- 이벤트 타임라인(도메인 이벤트, 커맨드, 정책, 외부 시스템)
- 핫스팟(충돌/불명확/병목/규칙 복잡) 목록
- 바운디드 컨텍스트 맵 초안(컨텍스트 이름, 책임, 인터페이스)
- 후속 액션(추가 탐색이 필요한 질문, 실험 티켓)
준비물과 역할: 1일 워크숍을 성립시키는 조건
참가자 구성(필수)
- 도메인 전문가 2명 이상(운영/CS/정책 담당 포함 권장)
- 개발자 3~6명(서비스별 대표)
- 제품/기획 1~2명
- 퍼실리테이터 1명(중립적 진행자)
준비물
- 오프라인: 포스트잇(색상 4~6종), 마커, 큰 보드
- 온라인: Miro/Mural/FigJam 템플릿
- 사전 자료: 주요 유스케이스, 현재 시스템 구성도(있으면), 장애/클레임 사례 3개
역할 분리 팁
- 퍼실리테이터는 "정답"을 말하지 않고, 질문으로 합의를 끌어냅니다.
- 도메인 전문가는 "실제로 일어나는 일"을 말하고, 개발자는 "현재 구현"을 말합니다. 둘이 섞이면 워크숍이 설계 리뷰로 흐르기 쉽습니다.
1일 아젠다(6~7시간) 예시
0) 오프닝 20분
- 규칙: 비난 금지, 사실 기반, 용어는 보드에 적고 합의
- 범위: "오늘은 결제까지" 같은 명확한 스코프 선언
1) Big Picture EventStorming 90분
- 도메인 이벤트(과거형, 완료형)를 시간순으로 배치
- 예:
주문이 생성됨,결제가 승인됨,배송이 시작됨
핵심은 "시스템이 하는 일"이 아니라 "비즈니스에서 일어난 사실"로 쓰는 것입니다.
2) 핫스팟 표시 30분
- 규칙이 복잡한 지점, 책임이 모호한 지점, 장애가 잦은 지점에 마커
- 예: "환불 정책은 누가 결정하나", "재고 차감 시점이 서비스마다 다름"
3) Process/Design Level로 내려가기 120분
- 이벤트 앞에 커맨드(의도), 그 사이에 정책(자동화 규칙), 외부 시스템을 추가
- 이 단계에서 "경계" 후보가 보이기 시작합니다.
4) 바운디드 컨텍스트 후보 도출 60분
- 이벤트/커맨드를 덩어리로 묶고 이름을 부여
- 각 덩어리의 "유비쿼터스 언어"(동일 단어의 의미)를 정의
5) 컨텍스트 맵과 통합 방식 60분
- 컨텍스트 간 관계를 정리: 이벤트 발행/구독, 동기 API, ACL 필요 여부
- 팀 소유권과 배포 단위(모놀리식 모듈 vs 마이크로서비스)는 분리해서 논의
6) 마무리 20분
- 남은 질문과 후속 실험 티켓화
- "오늘의 결정"과 "보류"를 명확히 구분
이벤트에서 경계로: 바운디드 컨텍스트를 나누는 7가지 판단 기준
1) 동일 용어의 의미가 달라지는 지점
예를 들어 "주문"이 어떤 팀에게는 결제 전 장바구니 확정이고, 다른 팀에게는 결제 승인 후 상태라면 경계가 필요합니다. 같은 단어를 억지로 통일하기보다, 컨텍스트별로 명확히 정의하고 번역(ACL)을 두는 편이 안전합니다.
2) 불변 규칙이 다른 지점(정책의 소유자가 다른 곳)
- 쿠폰 적용 규칙은 마케팅이, 환불 규칙은 CS/정산이 주로 소유
- 규칙 변경 빈도와 승인 프로세스가 다르면 경계 후보입니다.
3) 트랜잭션 일관성 요구가 끊기는 지점
"반드시 한 트랜잭션이어야 한다"는 요구가 실제로 어디까지인지 확인합니다. 많은 경우 진짜 요구는 "최종 일관성"으로 충분합니다. 이 지점을 잘못 잡으면 분산 트랜잭션 지옥이 열립니다.
사가/보상 트랜잭션이 필요한 구간은 컨텍스트 경계 논의에서 자주 등장합니다. 실무에서 보상 누락이 어떻게 장애로 이어지는지 감을 잡고 싶다면 MSA 사가 패턴 - 보상 트랜잭션 누락 디버깅도 함께 참고하면 좋습니다.
4) 데이터 모델이 같은 테이블을 공유하려는 유혹이 큰 지점
컨텍스트를 나누면 "조인"이 불편해집니다. 그래서 초기에는 테이블 공유로 타협하고 싶어지는데, 이 순간 경계가 무너집니다. 공유 DB는 장기적으로 변경 비용을 폭발시킵니다.
5) 팀 경계(커뮤니케이션 비용)가 높은 지점
팀이 다르면 배포 주기, 우선순위, 장애 대응 방식이 다릅니다. 팀 간 조율이 잦은 기능은 컨텍스트를 나누거나, 최소한 인터페이스 계약을 명확히 해야 합니다.
6) 장애가 전파되는 지점(결합이 강한 지점)
특정 기능 장애가 다른 영역 전체를 멈추게 한다면, 경계가 잘못되었거나 통합 방식이 부적절할 가능성이 큽니다.
7) 읽기 모델과 쓰기 모델이 갈라지는 지점
조회는 다양한 화면/리포트 요구로 확장되고, 쓰기는 규칙 중심으로 보수적으로 변합니다. 이 분리는 컨텍스트 또는 최소한 모듈 경계로 이어지기 쉽습니다.
예시: 커머스 도메인에서 1일 워크숍으로 나오는 결과물
아래는 "주문부터 환불까지"를 다뤘을 때 자주 나오는 이벤트 흐름 예시입니다.
이벤트(예시)
주문이 생성됨결제가 요청됨결제가 승인됨재고가 차감됨배송이 시작됨구매확정됨환불이 요청됨환불이 승인됨환불이 완료됨
컨텍스트 후보(예시)
Ordering: 주문 생성, 주문 상태 전이의 규칙Payment: 결제 승인/취소, PG 연동, 결제 수단 정책Inventory: 재고 예약/차감/복원 규칙Fulfillment: 배송/출고 프로세스Refund: 환불 정책, 승인 프로세스, 정산 연계
여기서 중요한 포인트는 "서비스 개수"를 결정하는 게 아니라, 언어와 규칙의 경계를 확정하는 것입니다. 하나의 배포 단위 안에서도 모듈로 바운디드 컨텍스트를 유지할 수 있습니다.
코드 예제: 이벤트 중심 경계를 코드에 반영하기
워크숍 결과를 코드로 옮길 때는 "컨텍스트 단위로 패키지/모듈을 분리"하고, 통합은 이벤트 또는 명시적 API로만 하게 만드는 것이 핵심입니다.
1) 컨텍스트별 이벤트 정의(예: Kotlin)
Ordering 컨텍스트에서 발행하는 이벤트는 Ordering의 언어로 정의합니다.
package ordering.domain.event
import java.time.Instant
import java.util.UUID
sealed interface OrderingEvent {
val occurredAt: Instant
}
data class OrderCreated(
val orderId: UUID,
val customerId: UUID,
val totalAmount: Long,
override val occurredAt: Instant = Instant.now()
) : OrderingEvent
data class OrderCancelled(
val orderId: UUID,
val reason: String,
override val occurredAt: Instant = Instant.now()
) : OrderingEvent
Payment 컨텍스트는 OrderCreated를 그대로 "공유"하기보다, 구독 시점에 자신의 언어로 번역하는 편이 안전합니다.
2) ACL(번역 계층)로 이벤트를 컨텍스트 언어로 변환
package payment.acl
import ordering.domain.event.OrderCreated
import payment.domain.command.RequestPayment
class OrderingToPaymentTranslator {
fun toRequestPayment(e: OrderCreated): RequestPayment {
return RequestPayment(
orderId = e.orderId,
amount = e.totalAmount,
reason = "ORDER_CREATED"
)
}
}
이렇게 하면 Ordering의 이벤트 스키마 변경이 Payment 도메인 모델을 직접 오염시키는 것을 줄일 수 있습니다.
3) 사가 오케스트레이션의 뼈대(예: TypeScript)
하루 워크숍에서 "완벽한 사가"까지 설계하려 하면 과합니다. 대신 어떤 이벤트가 다음 커맨드를 트리거하는지 뼈대를 잡아두면 좋습니다.
type DomainEvent =
| { type: 'OrderCreated'; orderId: string; amount: number }
| { type: 'PaymentApproved'; orderId: string; paymentId: string }
| { type: 'PaymentFailed'; orderId: string; reason: string };
type Command =
| { type: 'RequestPayment'; orderId: string; amount: number }
| { type: 'CancelOrder'; orderId: string; reason: string };
export function handle(event: DomainEvent): Command[] {
switch (event.type) {
case 'OrderCreated':
return [{ type: 'RequestPayment', orderId: event.orderId, amount: event.amount }];
case 'PaymentFailed':
return [{ type: 'CancelOrder', orderId: event.orderId, reason: event.reason }];
default:
return [];
}
}
여기서 핵심은 "어떤 이벤트가 경계를 넘어 어떤 의도를 만든다"를 명시하는 것입니다.
1일 워크숍에서 자주 터지는 함정과 대응
함정 1) 현재 DB 테이블을 기준으로 컨텍스트를 자른다
테이블은 과거의 설계 결정이 누적된 결과물입니다. 이벤트와 규칙을 먼저 보고, 데이터는 나중에 매핑하세요.
함정 2) 서비스 분리 논쟁으로 흐른다
바운디드 컨텍스트는 논리적 경계이고, 마이크로서비스는 배포 경계입니다. 1일 워크숍에서는 논리 경계에 집중하고, 배포는 후속으로 미루는 편이 성공 확률이 높습니다.
함정 3) 동기 호출로 모든 걸 해결하려 한다
동기 API는 간단하지만 결합이 강합니다. 이벤트 기반 통합이 필요한 구간을 최소 1개라도 식별해두면, 이후 아키텍처 선택이 쉬워집니다.
함정 4) "정의"만 하고 "검증"을 안 한다
워크숍 결과는 가설입니다. 반드시 얇은 프로토타입이나 슬라이스(예: 주문 생성부터 결제 요청까지)로 검증해야 합니다.
워크숍 이후 1주일 액션 플랜
- 컨텍스트별 용어집(Glossary) 1페이지로 정리
- 컨텍스트 간 계약(이벤트 스키마 또는 API) 초안 작성
- 가장 충돌이 큰 핫스팟 1개를 선택해 "작은 리팩터링" 티켓으로 실행
- 운영 관점에서 장애 전파를 줄이는 관측 포인트를 추가
MSA 환경이라면 네트워크/정책 문제로 "서비스는 떴는데 트래픽이 0" 같은 일이 실제로 자주 발생합니다. 컨텍스트가 분리될수록 이런 이슈의 진단 역량이 중요해지니, 운영 관점 참고로 EKS Pod는 뜨는데 트래픽 0 - NetPol·SG·CNI 10분 진단도 같이 읽어두면 좋습니다.
체크리스트: 오늘 바로 실행하기
- 스코프를 "핵심 시나리오 2~4개"로 제한했는가
- 이벤트를 과거형으로 썼는가(예:
결제가 승인됨) - 핫스팟을 표시하고, 논쟁 지점을 보드 위로 올렸는가
- 컨텍스트 이름과 책임을 한 문장으로 말할 수 있는가
- 컨텍스트 간 통합을 "이벤트/명시적 API/ACL"로 표현했는가
마무리
EventStorming의 힘은 "빠른 합의"가 아니라 "빠른 불일치 발견"에 있습니다. 하루 동안 이벤트 플로우를 눈으로 맞춰보면, 바운디드 컨텍스트는 추상 개념이 아니라 충돌을 줄이는 실무 도구로 바뀝니다. 1일 워크숍의 목표는 완벽한 설계가 아니라, 경계에 대한 공통 언어와 다음 실험의 출발선을 만드는 것입니다.