도메인 경계 재설계 TDD
Background
근거 PRD: /Users/biuea/Desktop/dpdpdndn/프로젝트/스포츠앱/도메인 경계 재설계/PRD.md (검수 PASS)
sports-application backend는 com.sportsapp.domain 하위 12개 도메인 + 공유 커널 common + 조회·유틸 컨텍스트 dashboard·image(domain 레이어 없음)로 구성된 Hexagonal + Rich Domain 모놀리스다. PR #193(PKG-2)로 레이어 우선 → 컨텍스트 우선 패키지 구조 이전이 끝났으나 “도메인 간 경계·관계·의존 방향”은 문서화되지 않았다.
이 과제는 후속 7개 과제(②B2B 파트너 연동 ~ ⑧지능형 장애 알림)의 선행 병목이다. ②③은 이 TDD가 확정하는 컨텍스트 맵·관계 유형·제공 인터페이스를 그대로 인용해 설계한다.
Overview
- 무엇을: 12개 도메인 +
common+dashboard·image를 코어(거래)/지원(운영·알림)/서브시스템(AI 에이전트)/공유 커널/조회·유틸 5개 분류로 확정한 컨텍스트 맵을 산출한다. - 왜: 도메인 소유 데이터·의존 방향의 합의된 기준이 없으면 후속 과제 설계가 충돌한다. B2B 파트너가 어느 인터페이스를 경유할지, 마케팅 이벤트 로직이 어느 도메인에 얹힐지가 이 기준 없이는 확정되지 않는다.
- 어떻게: 실제 코드를 읽어 AS-IS 컨텍스트 맵을 그리고, 관계 유형(Shared Kernel/Customer-Supplier/Conformist/ACL)·허용 의존 방향·Aggregate Root·소유 데이터를 표로 확정한다. FR-7 도메인 레이어 교차 참조 금지를 ArchUnit 정적 테스트로 실행 가능하게 만든다(베이스라인 0건 유지). 코드 재배치는 이 과제 범위 밖(PRD Non-Goals·M3) 이며, 산출물은 문서 + 실행 가능한 아키텍처 규칙 테스트다.
Terminology
| 용어 | 정의 |
|---|---|
| 컨텍스트 맵(Context Map) | DDD의 Bounded Context 간 관계·의존 방향을 표기한 지도 |
| 코어 도메인(Core) | 고객 대상 거래 흐름을 소유하는 도메인 (booking, facility, goods, payment, ticketing, user, post, message) |
| 지원 도메인(Supporting) | 코어를 보조하는 운영·알림 도메인 (notification, operator, weather) |
| 서브시스템(Subsystem) | AI 에이전트 운영을 위한 격리 서브시스템 (mcp) |
| 공유 커널(Shared Kernel) | 여러 도메인이 공유하는 최소 공통 계약 (common) |
| 조회·유틸 컨텍스트 | Aggregate Root 없이 조회 조합·파일 처리만 하는 컨텍스트 (dashboard, image) |
| Customer-Supplier | 하위(Customer)가 상위(Supplier)에 의존하되 Supplier가 계약을 안정적으로 제공하는 관계 |
| Conformist | 하위가 상위의 모델을 그대로 수용(변형 없이 순응)하는 관계 |
| ACL(Anti-Corruption Layer) | 외부/타 컨텍스트 모델을 자기 도메인 언어로 번역하는 방어 계층 (여기선 도메인이 정의한 Gateway interface) |
| 제공 인터페이스(Provided Interface) | 한 컨텍스트가 다른 컨텍스트에 노출하는 진입점 (여기선 UseCase) |
Define Problem
AS-IS
실제 코드(backend/src/main/kotlin/com/sportsapp/)를 읽어 확인한 현재 구조.
도메인 레이어 12개 (Aggregate Root 보유):
| 도메인 | 주요 Entity (Aggregate Root 굵게) | Domain Service | 외부 Gateway | 저장소 |
|---|---|---|---|---|
| booking | Booking, Slot / BookingStatus | BookingDomainService, SlotDomainService | PaymentRefundGateway | MySQL |
| facility | Facility | FacilityDomainService, FacilityOwnerDomainService, PublicFacilityImportService | GeocodingGateway, PublicSportsFacilityGateway | MongoDB |
| goods | Product, Cart, GoodsOrder, Stock, CartItem, GoodsOrderItem | GoodsDomainService, CartDomainService | (없음) | MySQL + Redis |
| payment | Payment | PaymentDomainService | PaymentGateway, OrderConfirmationGateway | MySQL |
| ticketing | Event, TicketOrder, Seat, Ticket | TicketingDomainService | SeatLockStore | MySQL |
| user | User, Role, UserRole, RolePermission | UserDomainService, AuthDomainService | JwtIssuer, JwtBlacklistStore | MySQL + Redis |
| post | Post, Comment | PostDomainService | (없음) | MySQL |
| message | Room, Message, RoomParticipant | MessageDomainService | (없음) | MySQL |
| notification | Notification, PushToken | NotificationDomainService, PushTokenDomainService | NotificationChannelGateway, TemplateRenderer | MySQL |
| operator | OperatorInboxNotification | OperatorInboxNotificationDomainService | (없음) | MySQL |
| weather | 없음 (Entity 0개, gateway+service+vo만) | WeatherDomainService | WeatherGateway | 외부 API |
| mcp | McpToken, McpAuditLog, McpAnomalyEvent | McpTokenDomainService, McpAuditLogDomainService, McpAnomalyEventDomainService, McpAnomalyDetector | ConfirmationTokenGateway | MySQL |
조회·유틸 컨텍스트 2개 (domain 레이어 없음):
dashboard—application/dashboard·presentation/dashboard만 존재.GetMyDashboardSummaryUseCase·GetOperationKpiUseCase가booking·facility·goods·ticketing·user의 도메인 서비스를 조합 호출해 조회한다. Aggregate Root·쓰기 없음.image—application/image·presentation/image만 존재.CreatePresignedUploadUrlUseCase가common/storage/ImageDomainService를 호출해 presigned URL을 발급한다. image의 도메인 로직은domain/common/storage(공유 커널)에 위치한다(ImageDomainService,ImageKeyGenerator,ImageStorageGateway,PresignedUpload).
공유 커널 common: AggregateRoot, DomainEvent, DomainEventPublisher, BusinessException(+ exceptions/*), Permission·PermissionRepository, UserRoleName, Currency, PgEventType, DistributedLock, security/OwnershipGuard, storage/Image*.
현재 의존 관계 (실측):
- 동기 — application 레이어 오케스트레이션:
booking→payment,goods→payment,ticketing→payment,facility→booking,dashboard→{booking,facility,goods,ticketing,user}. - 동기 — domain Gateway interface(자기 도메인이 정의, 타 도메인 import 아님): booking의
PaymentRefundGateway, payment의OrderConfirmationGateway(코어로 콜백). - 비동기 — 이벤트:
payment.completed.v1·booking.confirmed.v1→NotificationEventWorker(Kafka), core →McpAnomalyEventWorker(AFTER_COMMIT), booking 환불 →BookingRefundEventWorker(AFTER_COMMIT), notification 발송 →NotificationDispatchEventWorker(AFTER_COMMIT). - domain 레이어 교차 도메인 import: 0건 (
grep -rn "import com.sportsapp.domain\." domain/ | grep -v domain.common결과 공집합 — FR-7 베이스라인).
문제점:
- 코어 거래 도메인과 운영 지원 서브시스템(
operator·mcp)이 같은 레벨 패키지로 평면 나열 — 계층 구분 없음. dashboard·image가 컨텍스트 맵에 분류된 적 없음 (특히image는 이전 AS-IS 파악에서 누락).- 도메인 쌍별 관계 유형·허용 의존 방향이 문서로 존재하지 않음 — 코드에 암묵적.
- FR-7 교차 참조 규칙(베이스라인 0건)을 지키는지 자동 검증하는 수단 없음.
TO-BE
- 5개 분류(코어/지원/서브시스템/공유 커널/조회·유틸)로 명시 분류한 컨텍스트 맵 문서.
- 도메인 쌍별 관계 유형·허용 의존 방향 표.
- 도메인별 Aggregate Root·소유 데이터(쓰기 권한) 표.
goods·ticketing제공 인터페이스(UseCase 경계) 목록.- FR-7 정적 검증을 실행 가능한 ArchUnit 테스트로 확립 (베이스라인 0건 유지, 2분 이내).
- 컨텍스트 맵 갱신 절차(FR-8).
Architecture Benchmarking (의무)
| 제품/사례 | 해결 방식 | 참고할 패턴 | 미참고 사유 |
|---|---|---|---|
| Shopify 모놀리스 + Packwerk (shopify.engineering/shopify-monolith, Enforcing Modularity with Packwerk) | 37개 컴포넌트로 도메인 분할, package.yml로 컴포넌트 간 의존을 정적 강제. 물리 분리 없이 경계만 강제 | ① “물리 분리 없이 경계만 정적 강제”가 본 과제 Non-Goals(MSA 분리 안 함)와 일치 → 정적 검증(ArchUnit)으로 채택. ② 의존 방향을 선언적 config로 두고 위반을 빌드가 잡는 fitness function 접근 채택 | Ruby/Packwerk 자체는 Kotlin에 부적용 → 등가물 ArchUnit/Konsist로 치환 |
| Spring Modulith + ArchUnit + Fitness Functions (Modular Monolith 2026 Guide) | 모놀리스 내 모듈 경계를 ArchUnit fitness function으로 검증, 모듈 간 접근은 명시 API/이벤트만 허용 | ① 모듈 간 통신을 동기 API(제공 UseCase) + 도메인 이벤트로 이원화하는 원칙 채택. ② ArchUnit 규칙을 테스트로 상시 실행(CI fitness function) 채택 | Spring Modulith 런타임 모듈 등록(@ApplicationModule)은 이 과제 범위(문서+규칙)를 넘는 코드 재배치 유발 → 미채택. ArchUnit 규칙만 차용 |
| Uber DOMA (반면교사) | 무통제 서비스 분해(2→2,200+) 후 도메인 구조를 사후 부과 | ”구조 없이 먼저 쪼갠 뒤 사후 정리”는 비용이 크다 → 경계·규칙을 코드 재배치보다 먼저 확정하는 본 과제 순서의 근거 | MSA 규모 조직 사례로 개인 모놀리스에 그대로 적용 불가 — 순서 원칙만 참조 |
Possible Solutions
방안 비교
| 방안 | 설명 | 왜 채택 / 미채택 |
|---|---|---|
| A. 문서만 (컨텍스트 맵 md + 코드 리뷰 체크리스트) | 컨텍스트 맵·의존 규칙을 문서로만 남기고 위반 감지는 사람이 리뷰 | 부분 채택 — 컨텍스트 맵·관계 표는 문서가 SSOT. 단 사람 리뷰만으로는 베이스라인 0건 회귀를 못 막음 → B 병행 |
| B. ArchUnit 정적 테스트로 의존 규칙 강제 | domain 레이어 교차 도메인 import·계층 의존 방향을 ArchUnit Kotest로 assert, ./gradlew test에서 실행 | 채택 — FR-7의 “정적 검증 방법”을 실행 가능하게 구현. 베이스라인 0건을 회귀 테스트로 고정. Shopify/Spring Modulith 벤치마크와 정합. 2분 이내 NFR 충족(ClassFileImporter 캐싱) |
C. Spring Modulith @ApplicationModule 런타임 모듈화 | 각 도메인을 Spring Modulith 모듈로 등록하고 런타임 경계 검증 | 미채택 — 모듈 메타데이터 부착·패키지 재배치를 유발해 이 과제 Non-Goals(코드 재배치 없음) 위반. 지금 규모(12 도메인 모놀리스)에 과함 |
| D. 멀티모듈 Gradle 분리 (도메인당 서브모듈) | 도메인을 Gradle 서브모듈로 물리 분리해 컴파일 타임에 의존 차단 | 미채택 — MSA 전 단계의 물리 분리로 Non-Goals 위반. 빌드 그래프·순환 의존 정리에 큰 비용, 지금 필요 없음(단순함 우선) |
| E. FR-4 제공 인터페이스를 DomainService로 노출 | 파트너가 GoodsDomainService.createProduct를 직접 경유 | 미채택 — 아래 [FR-4 결정] 참조. 트랜잭션·소유권 오케스트레이션을 우회해 중복 유발 |
단순함 우선 결론: A(문서 SSOT) + B(ArchUnit 규칙 테스트) 조합. C·D는 지금 규모에 과한 물리 분리로 미채택.
Detail Design
컨텍스트 맵 — 5개 분류 확정 (FR-1, FR-3, FR-6)
| 분류 | 도메인 | 분류 근거 |
|---|---|---|
| 코어(Core, 거래) | booking, facility, goods, payment, ticketing, user, post, message | 고객 대상 거래·핵심 자산을 소유(Aggregate Root + 쓰기). user는 신원·인가의 기반 코어 |
| 지원(Supporting) | notification, operator, weather, alerting | 코어를 보조. notification=코어 이벤트 소비 발송, operator=내부 운영자 인박스, weather=외부 기상 조회(generic subdomain, Entity 없음), alerting=인프라 장애 알림(⑥ 신규 — 신호·심각도·소스·쿨다운·LLM분석·이력 소유) |
| 서브시스템(Subsystem) | mcp | AI 에이전트 토큰·감사·이상탐지. 코어 거래와 라이프사이클·변경 주기 독립 → 격리 서브시스템 |
| 공유 커널(Shared Kernel) | common | AggregateRoot/DomainEvent/OwnershipGuard/storage 등 최소 공통 계약. 3계층 분류 대상 아님(PRD 명시) |
| 조회·유틸(Read/Utility) | dashboard, image | Aggregate Root·쓰기 없음. dashboard=코어 조회 조합(Conformist read model), image=파일 업로드 유틸(도메인 로직은 common/storage) |
weather특기사항: Entity 0개 = 자체 소유 데이터 없음. 외부 기상 API를 조회해 반환하는 Generic Subdomain. 지원 도메인으로 분류하되 Aggregate Root 표(FR-5)에서는 “소유 데이터 없음”으로 표기.image특기사항: 도메인 로직이domain/common/storage(공유 커널)에 존재.image컨텍스트는 application/presentation만 가진 얇은 유틸 진입점. 조회·유틸로 분류하고, 코드 재배치(common/storage → domain/image 승격 여부)는 M3 후속 티켓에서 판단.alerting특기사항 (⑥ 지능형 장애 알림 신규): 인프라 장애 알림의 라이프사이클(신호·심각도·소스·쿨다운·LLM 원인분석·이력)을 소유하는 신규 지원 도메인. Aggregate Root=Alert(신규alerts테이블), Redis 쿨다운 키 네임스페이스 1개 소유. 사용자 알림(notification.Notification)과 라이프사이클·데이터 소유가 분리되므로 notification 편입이 아닌 별도 도메인으로 분리(①의 “독립 데이터 소유·다른 변경 주기” 기준 충족). 발송은 notificationDISCORD채널을 재사용하되 domain.notification을 import하지 않고 이벤트 경유(아래 관계표 참조). 코어를 동기 호출하지 않으므로 R3 화이트리스트 추가는 불필요.
도메인 관계 유형·허용 의존 방향 (FR-2, FR-3)
방향은 “행 → 열”이 호출/의존하는 방향. 관계 유형은 DDD 표기.
| 소비자 → 공급자 | 관계 유형 | 통신 방식 | 허용/금지 |
|---|---|---|---|
| booking → payment | Customer-Supplier (payment=Supplier) | booking이 정의한 PaymentRefundGateway interface + app 오케스트레이션 | 허용 (동기) |
| goods → payment | Customer-Supplier | app 오케스트레이션 (application/goods) | 허용 (동기) |
| ticketing → payment | Customer-Supplier | app 오케스트레이션 | 허용 (동기) |
| payment → booking/goods/ticketing | Conformist (ACL) | payment가 정의한 OrderConfirmationGateway interface (콜백), infra에서 구현 | 허용 (payment는 코어 도메인을 import하지 않음 — Gateway 역전) |
| facility → booking | Customer-Supplier | app 오케스트레이션 | 허용 (동기) |
| dashboard → booking/facility/goods/ticketing/user | Conformist (읽기 전용) | app 레이어 조회 조합 | 허용 (조회만, 쓰기 금지) |
| partner → user (② B2B) | Customer-Supplier (쓰기 오케스트레이션) | app 레이어 (CreatePartnerUseCase가 UserDomainService로 연동 전용 User 계정 프로비저닝) | 허용 (명시 예외 — admin 프로비저닝 orchestration. dashboard 읽기 예외보다 넓은 쓰기 예외. R3 화이트리스트 등록, 아래 규칙 4·FR-7 R3 참조) |
| 코어(payment, booking) → notification | 발행-구독 | 도메인 이벤트 (Kafka *.v1 / AFTER_COMMIT) | 허용 (비동기, 단방향) |
| 코어 → mcp | 발행-구독 | 도메인 이벤트 (AFTER_COMMIT anomaly) | 허용 (비동기, 단방향) |
| 코어 → operator | 발행-구독 | 도메인 이벤트 / in-app 채널 | 허용 (비동기, 단방향) |
| alerting → notification (⑥) | 발행-구독 (발송 재사용) | 도메인 이벤트 → presentation → application 경로로 notification DISCORD 채널 발송. domain.notification 미import, 도메인 간 ID(Long) 참조만 | 허용 (비동기, 단방향 — 코어 동기 미호출) |
| operator/mcp/weather/alerting → 코어 | — | — | 금지 (지원·서브시스템은 코어를 동기 호출하지 않는다) |
| 모든 도메인 → common | Shared Kernel | 직접 import | 허용 (유일한 import 예외) |
| domain.X → domain.Y (X≠Y, Y≠common) | — | Entity/VO 직접 import | 금지 (FR-7 스캔 대상, 베이스라인 0건) |
핵심 의존 규칙 (후속 과제 인용 대상):
- domain 레이어는 타 도메인을 import하지 않는다 —
common만 예외. 도메인 간 데이터 참조는 ID(Long)만. - 도메인 간 협력은 두 경로만 — ① application 레이어 오케스트레이션(여러 UseCase/DomainService 조합) ② 도메인 이벤트(비동기).
- 코어 → 코어 동기 호출은 소비자 도메인이 정의한 Gateway interface를 통한다(ACL). 공급자 도메인 Entity를 직접 참조하지 않는다.
- 지원 도메인·서브시스템(notification/operator/mcp/weather/alerting)은 코어를 동기 호출하지 않는다 — 코어가 발행한 이벤트를 소비하는 방향만 허용. 명시 예외 2건: ①
dashboard → 코어읽기 전용 조합, ②application.partner → domain.user(② B2B) admin 프로비저닝 쓰기 오케스트레이션(CreatePartnerUseCase가 연동 전용 User 계정 생성). 두 예외 모두 FR-7 R3 화이트리스트에 등록한다 — 그 외 지원→코어 동기 접근은 금지. - operator·mcp 서브시스템 격리 — 코어는 operator/mcp를 동기 참조하지 않고, operator/mcp도 코어를 동기 참조하지 않는다. 유일한 결합은 코어→서브시스템 이벤트 단방향.
FR-4 결정 — goods·ticketing 제공 인터페이스 경계 = UseCase 레이어
결정: goods·ticketing이 B2B 파트너에게 노출하는 경계는 UseCase 레이어다.
제공 인터페이스 목록:
| 도메인 | 제공 UseCase (경계) | 소유권 해석 방식 | B2B 경유 방식 |
|---|---|---|---|
| goods | CreateMyProductUseCase | OwnershipGuard.authUserId() (SecurityContext) | 인증 필터가 연동 전용 User로 SecurityContext 채움 → 코드 무변경 경유 |
| ticketing | CreateMyEventUseCase | CreateMyEventCommand.ownerUserId (Command 인자) | Controller가 SecurityContext id를 Command에 채워 전달 → 코드 무변경 경유 |
근거:
- 형제 PRD 정합 — B2B 파트너 연동 FR-4/FR-5가 두 UseCase를 “코드 변경 없이 그대로 호출”하고 경유 접점을 인증 계층으로 명시. DomainService가 아니라 UseCase가 실제 소비 지점.
- 트랜잭션·소유권 경계 일치 — UseCase가
@Transactional과OwnershipGuard소유권 해석을 소유. DomainService를 노출하면 파트너 컨텍스트가 트랜잭션·소유권 오케스트레이션을 중복 구현해야 함(방안 E 미채택 사유). - 캡슐화 — DomainService는 도메인 내부 협력자. 컨텍스트 밖에 노출하면 도메인 내부가 새어나가 경계가 무너진다.
- 소유권 해석 비대칭은 인증 계층에서 흡수 — 두 UseCase의 시그니처 비대칭(
authUserId()vsCommand.ownerUserId)은 파트너 인증 필터가 SecurityContext를 채우는 것으로 통일 해결. 이 TDD는 두 UseCase 시그니처를 고정 계약으로 동결(BE-06 계약 테스트)해, 후속 ② 과제가 시그니처 변경 없이 경유하도록 보장한다.
booking·payment는 소비처가 없어 제공 인터페이스를 정의하지 않는다(PRD Non-Goals). 필요 시점에 별도 정의.
시스템 역할 경계 (의무)
| 단위 | 역할 | 소유 데이터/책임 | 노출 인터페이스 | 의존 |
|---|---|---|---|---|
| Core 도메인 (8) | 거래·핵심 자산 소유 | 각자 Aggregate Root + 쓰기 | 제공 UseCase(예: goods/ticketing), 도메인 이벤트 | common, (코어→코어는 자기 Gateway interface) |
| Support 도메인 (3) | 코어 보조 | notification/operator 자체 Entity, weather 무소유 | 조회 UseCase, 이벤트 Consumer | common, 코어 이벤트(소비) |
| Subsystem (mcp) | AI 에이전트 운영 | McpToken/AuditLog/AnomalyEvent | mcp 자체 API, anomaly Consumer | common, 코어 이벤트(소비) |
| Shared Kernel (common) | 공통 계약 | AggregateRoot/DomainEvent/OwnershipGuard/storage | 전 도메인이 import | (없음 — 순수) |
| Read/Utility (dashboard/image) | 조회 조합·파일 유틸 | 무소유(쓰기 없음) | 조회 UseCase | 코어 도메인 서비스(읽기), common/storage |
| ArchUnit 규칙 테스트 (신규) | 의존 방향·교차 참조 정적 검증 | 규칙 assert | ./gradlew test fitness function | ArchUnit, 소스 전체 |
인터페이스 시그니처 — 동결 대상 (FR-4 경계 계약)
후속 ② 과제가 코드 변경 없이 경유하도록 아래 시그니처를 계약으로 고정한다(BE-06 테스트가 회귀 감시).
// application/goods/usecase/CreateMyProductUseCase
fun execute(command: CreateMyProductCommand): ProductWithStock
// 소유자: OwnershipGuard.authUserId() 로 SecurityContext에서 해석
// application/ticketing/usecase/CreateMyEventUseCase
fun execute(command: CreateMyEventCommand): CreateMyEventResult
// 소유자: command.ownerUserId (Controller가 SecurityContext id를 채움)
FR-7 정적 검증 방법 (실행 가능)
- 스캔 대상:
com.sportsapp.domain.**(공유 커널common제외)의 Entity/VO가 타 도메인 패키지를 import하는지. application 레이어의 다중 도메인 조합 호출은 정상 오케스트레이션이므로 스캔 제외(PRD FR-7 명시). - 구현: ArchUnit
ClassFileImporter로com.sportsapp임포트 → Kotest FunSpec에서 규칙 assert. 규칙:- R1:
domain.X는domain.Y(Y≠X, Y≠common)를 import하지 않는다. - R2:
domain.**는infrastructure.**·application.**·presentation.**를 import하지 않는다(레이어 방향). - R3:
application.**·domain.**는 지원/서브시스템이 코어를 동기 의존하지 않는 방향 규칙(operator/mcp/weather/alerting → 코어 금지). 화이트리스트 예외 2건: ①application.dashboard → 코어(읽기 조합) ②application.partner → domain.user(② B2B —CreatePartnerUseCase의 admin 프로비저닝 쓰기 오케스트레이션). 이 2건은 규칙에서 명시 제외한다. - R4:
common은 어떤 도메인도 import하지 않는다(공유 커널 순수성).
- R1:
- 베이스라인: 현재 R1 위반 0건. R3는 예외 2건(dashboard·partner) 제외 후 위반 0건 — 단
application.partner는 ② 과제에서 신설되므로 예외는 사전 등록(현 시점 partner 패키지 부재, 화이트리스트만 선등록). 테스트는 이 0건을 회귀 감시. - 성능:
ClassFileImporter결과를 스펙 간 공유(캐싱 fixture)해 전체 스캔 2분 이내 충족.
컨텍스트 맵 갱신 절차 (FR-8)
신규 도메인 추가 시 반드시 거치는 단계:
- 신규 도메인의 분류(코어/지원/서브시스템/조회·유틸)를 이 TDD 컨텍스트 맵 표에 추가.
- 관계 유형·허용 의존 방향을 관계 표에 추가(누구를 호출하고 누가 호출하는가, 동기/이벤트).
- Aggregate Root·소유 데이터를 FR-5 표에 추가.
- ArchUnit 규칙(R1~R4)에 신규 패키지가 자동 포함되는지 확인(패키지 prefix 스캔이므로 자동 포함, 예외 도메인만 화이트리스트 갱신). 코어에 쓰기 오케스트레이션을 수행하는 신규 지원 도메인은 R3 화이트리스트에 명시 등록(현재 등록:
dashboard읽기,partner→user쓰기). - 위 4단계를 문서 리뷰로 확인한 뒤 코드 추가.
Component Diagram — 계층 분류 (Mermaid flowchart LR)
flowchart LR subgraph Core["Core (거래)"] booking goods ticketing payment user facility end subgraph Support["Supporting"] notification operator weather end subgraph Sub["Subsystem"] mcp end subgraph Read["Read/Utility"] dashboard end SK["common (Shared Kernel)"] Core --> SK Support --> SK Sub --> SK Read --> SK
post·message(코어)·image(조회·유틸)는 격리 컨텍스트로 다이어그램에서 생략(노드 15개 제한). 분류는 위 표가 SSOT.
Sequence Diagram — 핵심 의존 방향 (동기 + 이벤트)
sequenceDiagram participant G as goods (Core) participant P as payment (Core) participant N as notification (Support) participant M as mcp (Subsystem) G->>P: app 오케스트레이션 (Customer-Supplier) P-->>G: OrderConfirmationGateway 콜백 (ACL) P->>N: payment.completed.v1 (이벤트, 비동기) P->>M: anomaly (AFTER_COMMIT, 비동기) Note over N,M: 지원/서브시스템은 코어를 동기 호출하지 않음
ERD
이 과제는 스키마를 변경하지 않는다(PRD Non-Goals). 소유권 경계 관점 요약만 표기(전체 컬럼은 각 도메인 마이그레이션이 SSOT).
erDiagram USER ||--o{ PRODUCT : owns USER ||--o{ EVENT : owns PRODUCT { bigint id PK bigint owner_id "User ID 네임스페이스" } EVENT { bigint id PK bigint owner_id "User ID 네임스페이스" }
owner_id는 현재 User ID를 그대로 사용. 후속 ② 과제가 파트너 연동 시에도 이 네임스페이스를 유지(별도 Partner 신원 테이블은 ② 범위).
Testing Plan
이 과제의 “테스트”는 아키텍처 규칙 fitness function(ArchUnit)이 중심이다. 비즈니스 로직 신규 없음.
| 레벨 | 대상 | 범위 |
|---|---|---|
| architecture (신규) | ArchUnit 규칙 R1~R4 | domain 교차 import 0건(R1), 레이어 방향(R2), 지원→코어 금지(R3), common 순수성(R4) |
| architecture (신규) | FR-4 경계 계약 | CreateMyProductUseCase/CreateMyEventUseCase 시그니처·반환 타입 존재 검증(회귀 동결) |
| — | domain/application/infra/presentation | 신규 비즈니스 로직 없음 → 신규 단위/통합 테스트 없음 (문서+규칙 과제) |
핵심 실패 경로 시나리오(규칙 테스트가 잡아야 하는 위반):
domain.goods가domain.ticketing.Event를 import → R1 위반으로 빌드 실패.domain.booking이infrastructure.*를 import → R2 위반.application.operator가domain.payment를 동기 호출 → R3 위반.application.partner가domain.user를 호출(② B2B admin 프로비저닝) → R3 화이트리스트 예외로 통과(false RED 방지). 그 외application.partner → 코어(user 제외)접근은 R3 위반.common이domain.user를 import → R4 위반.CreateMyProductUseCase.execute시그니처 변경 → 경계 계약 테스트 실패(② 과제 경유 파손 조기 감지).
Release Scenario — 무중단 배포 (의무)
이 과제는 런타임 동작을 바꾸지 않는다(문서 + 테스트 전용, 프로덕션 코드 무변경). 배포 리스크는 “규칙 테스트가 기존 코드를 잘못 붉게(RED) 만들어 CI를 막는 것”뿐이다.
- 배포 순서: 코드 먼저 불필요. 규칙 테스트만 추가되므로 단일 PR로 배포.
- 1단계 — 관찰 모드(피처 플래그 등가): ArchUnit 규칙 테스트를 추가하되, CI 게이트로 강제하지 않고 결과만 로그로 수집(테스트는
@Ignore아닌 별도 태스크archTest로 분리,check에 미포함). 베이스라인 0건 재확인. - 2단계 — 게이트 승격:
archTest를check/test에 편입해 위반 시 빌드 실패. Open Question(CI 게이트 강제 vs 리뷰 체크리스트)은 여기서 “게이트 강제”로 확정하되 승격은 1단계 0건 확인 후. - 전환 조건: 1단계에서
./gradlew archTest결과 위반 0건 → 2단계 승격. - 롤백: 규칙 테스트가 예상치 못한 위반을 내면
archTest를check에서 분리(2→1단계 역전환)로 즉시 게이트 해제. 프로덕션 영향 없음. 규칙 자체가 잘못됐으면 해당 규칙 스펙 파일만 revert. - 코드 재배치(M3)는 이 과제 밖 — dashboard/image 이동 등은 각기 expand-contract(신규 패키지 추가 → import 갱신 → 구 패키지 제거) 순서로 별도 티켓에서 무중단 처리.
Open Questions
- (PRD Open Q1) 코드 재배치(dashboard/image 이동)를 즉시 티켓화할지 보류할지 → 보류 권고: 다른 과제가 해당 패키지를 건드릴 때 그 과제 범위에서 처리(M3). 지금은 컨텍스트 맵·규칙만 확정.
- (PRD Open Q2) FR-7을 CI 게이트로 강제할지 → 게이트 강제로 확정(Release Scenario 2단계). 단 1단계 관찰 후 승격.
image의 도메인 로직(common/storage/ImageDomainService)을domain/image로 승격할지 → M3 후속 판단(공유 커널 비대화 vs 단일 소비처).
Document History
| 날짜 | 변경 내용 |
|---|---|
| 2026-07-03 | 최초 작성 — 실측 AS-IS 컨텍스트 맵, 5개 분류 확정, 관계·의존 방향 표, FR-4 UseCase 경계 결정, FR-7 ArchUnit 검증 방법, 무중단 배포 시나리오 |
| 2026-07-03 | ② B2B 정합 반영 — R3 화이트리스트에 application.partner → domain.user(admin 프로비저닝 쓰기 오케스트레이션) 예외 사전 등록. 관계 표·규칙 4·FR-7 R3·FR-8 갱신 절차·Testing Plan 실패 경로 반영 |
| 2026-07-03 | ⑥ 지능형 장애 알림 정합 반영(FR-8 신규 도메인 등록 절차) — 신규 alerting 지원 도메인을 5계층 분류표·관계표(alerting → notification 이벤트 단방향)·규칙 4·FR-7 R3에 편입. 코어 동기 미호출이므로 R3 화이트리스트 추가 불필요 |
Success Metrics 확인 체크리스트 (PRD 판정 기준)
12개 도메인 대표 유즈케이스 1개씩을 관계·의존 방향 표와 대조 — 불일치 0건 확인.
| # | 도메인 | 대표 유즈케이스 | 대조 결과 |
|---|---|---|---|
| 1 | booking | 예약 생성→payment 환불 Gateway | 코어→payment Customer-Supplier 일치 |
| 2 | facility | 시설 조회/등록 | 코어, facility→booking(app) 일치 |
| 3 | goods | CreateMyProductUseCase | 코어, 제공 UseCase 경계 일치 |
| 4 | payment | 결제 완료→이벤트 발행 | 코어, payment→notification 이벤트 일치 |
| 5 | ticketing | CreateMyEventUseCase | 코어, 제공 UseCase 경계 일치 |
| 6 | user | 로그인/인가 | 코어(신원 기반) 일치 |
| 7 | post | 게시글 작성 | 코어(격리) 일치 |
| 8 | message | 채팅방 메시지 | 코어(격리) 일치 |
| 9 | notification | payment.completed.v1 소비 | 지원, 코어 이벤트 소비 방향 일치 |
| 10 | operator | 운영자 인박스 조회 | 지원(격리), 코어 동기 미호출 일치 |
| 11 | weather | 기상 조회 | 지원(generic, 무소유) 일치 |
| 12 | mcp | 이상탐지 이벤트 소비 | 서브시스템, 코어 이벤트 소비 방향 일치 |
불일치 0건. dashboard(조회 조합)·image(유틸)·common(공유 커널)도 분류표에 반영됨 — 커버 누락 0건.