도메인 경계 재설계 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저장소
bookingBooking, Slot / BookingStatusBookingDomainService, SlotDomainServicePaymentRefundGatewayMySQL
facilityFacilityFacilityDomainService, FacilityOwnerDomainService, PublicFacilityImportServiceGeocodingGateway, PublicSportsFacilityGatewayMongoDB
goodsProduct, Cart, GoodsOrder, Stock, CartItem, GoodsOrderItemGoodsDomainService, CartDomainService(없음)MySQL + Redis
paymentPaymentPaymentDomainServicePaymentGateway, OrderConfirmationGatewayMySQL
ticketingEvent, TicketOrder, Seat, TicketTicketingDomainServiceSeatLockStoreMySQL
userUser, Role, UserRole, RolePermissionUserDomainService, AuthDomainServiceJwtIssuer, JwtBlacklistStoreMySQL + Redis
postPost, CommentPostDomainService(없음)MySQL
messageRoom, Message, RoomParticipantMessageDomainService(없음)MySQL
notificationNotification, PushTokenNotificationDomainService, PushTokenDomainServiceNotificationChannelGateway, TemplateRendererMySQL
operatorOperatorInboxNotificationOperatorInboxNotificationDomainService(없음)MySQL
weather없음 (Entity 0개, gateway+service+vo만)WeatherDomainServiceWeatherGateway외부 API
mcpMcpToken, McpAuditLog, McpAnomalyEventMcpTokenDomainService, McpAuditLogDomainService, McpAnomalyEventDomainService, McpAnomalyDetectorConfirmationTokenGatewayMySQL

조회·유틸 컨텍스트 2개 (domain 레이어 없음):

  • dashboardapplication/dashboard·presentation/dashboard만 존재. GetMyDashboardSummaryUseCase·GetOperationKpiUseCasebooking·facility·goods·ticketing·user의 도메인 서비스를 조합 호출해 조회한다. Aggregate Root·쓰기 없음.
  • imageapplication/image·presentation/image만 존재. CreatePresignedUploadUrlUseCasecommon/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.v1NotificationEventWorker(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 베이스라인).

문제점:

  1. 코어 거래 도메인과 운영 지원 서브시스템(operator·mcp)이 같은 레벨 패키지로 평면 나열 — 계층 구분 없음.
  2. dashboard·image가 컨텍스트 맵에 분류된 적 없음 (특히 image는 이전 AS-IS 파악에서 누락).
  3. 도메인 쌍별 관계 유형·허용 의존 방향이 문서로 존재하지 않음 — 코드에 암묵적.
  4. 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)mcpAI 에이전트 토큰·감사·이상탐지. 코어 거래와 라이프사이클·변경 주기 독립 → 격리 서브시스템
공유 커널(Shared Kernel)commonAggregateRoot/DomainEvent/OwnershipGuard/storage 등 최소 공통 계약. 3계층 분류 대상 아님(PRD 명시)
조회·유틸(Read/Utility)dashboard, imageAggregate 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 편입이 아닌 별도 도메인으로 분리(①의 “독립 데이터 소유·다른 변경 주기” 기준 충족). 발송은 notification DISCORD 채널을 재사용하되 domain.notification을 import하지 않고 이벤트 경유(아래 관계표 참조). 코어를 동기 호출하지 않으므로 R3 화이트리스트 추가는 불필요.

도메인 관계 유형·허용 의존 방향 (FR-2, FR-3)

방향은 “행 → 열”이 호출/의존하는 방향. 관계 유형은 DDD 표기.

소비자 → 공급자관계 유형통신 방식허용/금지
booking → paymentCustomer-Supplier (payment=Supplier)booking이 정의한 PaymentRefundGateway interface + app 오케스트레이션허용 (동기)
goods → paymentCustomer-Supplierapp 오케스트레이션 (application/goods)허용 (동기)
ticketing → paymentCustomer-Supplierapp 오케스트레이션허용 (동기)
payment → booking/goods/ticketingConformist (ACL)payment가 정의한 OrderConfirmationGateway interface (콜백), infra에서 구현허용 (payment는 코어 도메인을 import하지 않음 — Gateway 역전)
facility → bookingCustomer-Supplierapp 오케스트레이션허용 (동기)
dashboard → booking/facility/goods/ticketing/userConformist (읽기 전용)app 레이어 조회 조합허용 (조회만, 쓰기 금지)
partner → user (② B2B)Customer-Supplier (쓰기 오케스트레이션)app 레이어 (CreatePartnerUseCaseUserDomainService로 연동 전용 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 → 코어금지 (지원·서브시스템은 코어를 동기 호출하지 않는다)
모든 도메인 → commonShared Kernel직접 import허용 (유일한 import 예외)
domain.X → domain.Y (X≠Y, Y≠common)Entity/VO 직접 import금지 (FR-7 스캔 대상, 베이스라인 0건)

핵심 의존 규칙 (후속 과제 인용 대상):

  1. domain 레이어는 타 도메인을 import하지 않는다common만 예외. 도메인 간 데이터 참조는 ID(Long)만.
  2. 도메인 간 협력은 두 경로만 — ① application 레이어 오케스트레이션(여러 UseCase/DomainService 조합) ② 도메인 이벤트(비동기).
  3. 코어 → 코어 동기 호출은 소비자 도메인이 정의한 Gateway interface를 통한다(ACL). 공급자 도메인 Entity를 직접 참조하지 않는다.
  4. 지원 도메인·서브시스템(notification/operator/mcp/weather/alerting)은 코어를 동기 호출하지 않는다 — 코어가 발행한 이벤트를 소비하는 방향만 허용. 명시 예외 2건: ① dashboard → 코어 읽기 전용 조합, ② application.partner → domain.user(② B2B) admin 프로비저닝 쓰기 오케스트레이션(CreatePartnerUseCase가 연동 전용 User 계정 생성). 두 예외 모두 FR-7 R3 화이트리스트에 등록한다 — 그 외 지원→코어 동기 접근은 금지.
  5. operator·mcp 서브시스템 격리 — 코어는 operator/mcp를 동기 참조하지 않고, operator/mcp도 코어를 동기 참조하지 않는다. 유일한 결합은 코어→서브시스템 이벤트 단방향.

FR-4 결정 — goods·ticketing 제공 인터페이스 경계 = UseCase 레이어

결정: goods·ticketing이 B2B 파트너에게 노출하는 경계는 UseCase 레이어다.

제공 인터페이스 목록:

도메인제공 UseCase (경계)소유권 해석 방식B2B 경유 방식
goodsCreateMyProductUseCaseOwnershipGuard.authUserId() (SecurityContext)인증 필터가 연동 전용 User로 SecurityContext 채움 → 코드 무변경 경유
ticketingCreateMyEventUseCaseCreateMyEventCommand.ownerUserId (Command 인자)Controller가 SecurityContext id를 Command에 채워 전달 → 코드 무변경 경유

근거:

  1. 형제 PRD 정합B2B 파트너 연동 FR-4/FR-5가 두 UseCase를 “코드 변경 없이 그대로 호출”하고 경유 접점을 인증 계층으로 명시. DomainService가 아니라 UseCase가 실제 소비 지점.
  2. 트랜잭션·소유권 경계 일치 — UseCase가 @TransactionalOwnershipGuard 소유권 해석을 소유. DomainService를 노출하면 파트너 컨텍스트가 트랜잭션·소유권 오케스트레이션을 중복 구현해야 함(방안 E 미채택 사유).
  3. 캡슐화 — DomainService는 도메인 내부 협력자. 컨텍스트 밖에 노출하면 도메인 내부가 새어나가 경계가 무너진다.
  4. 소유권 해석 비대칭은 인증 계층에서 흡수 — 두 UseCase의 시그니처 비대칭(authUserId() vs Command.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, 이벤트 Consumercommon, 코어 이벤트(소비)
Subsystem (mcp)AI 에이전트 운영McpToken/AuditLog/AnomalyEventmcp 자체 API, anomaly Consumercommon, 코어 이벤트(소비)
Shared Kernel (common)공통 계약AggregateRoot/DomainEvent/OwnershipGuard/storage전 도메인이 import(없음 — 순수)
Read/Utility (dashboard/image)조회 조합·파일 유틸무소유(쓰기 없음)조회 UseCase코어 도메인 서비스(읽기), common/storage
ArchUnit 규칙 테스트 (신규)의존 방향·교차 참조 정적 검증규칙 assert./gradlew test fitness functionArchUnit, 소스 전체

인터페이스 시그니처 — 동결 대상 (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 ClassFileImportercom.sportsapp 임포트 → Kotest FunSpec에서 규칙 assert. 규칙:
    • R1: domain.Xdomain.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 위반 0건. R3는 예외 2건(dashboard·partner) 제외 후 위반 0건 — 단 application.partner는 ② 과제에서 신설되므로 예외는 사전 등록(현 시점 partner 패키지 부재, 화이트리스트만 선등록). 테스트는 이 0건을 회귀 감시.
  • 성능: ClassFileImporter 결과를 스펙 간 공유(캐싱 fixture)해 전체 스캔 2분 이내 충족.

컨텍스트 맵 갱신 절차 (FR-8)

신규 도메인 추가 시 반드시 거치는 단계:

  1. 신규 도메인의 분류(코어/지원/서브시스템/조회·유틸)를 이 TDD 컨텍스트 맵 표에 추가.
  2. 관계 유형·허용 의존 방향을 관계 표에 추가(누구를 호출하고 누가 호출하는가, 동기/이벤트).
  3. Aggregate Root·소유 데이터를 FR-5 표에 추가.
  4. ArchUnit 규칙(R1~R4)에 신규 패키지가 자동 포함되는지 확인(패키지 prefix 스캔이므로 자동 포함, 예외 도메인만 화이트리스트 갱신). 코어에 쓰기 오케스트레이션을 수행하는 신규 지원 도메인은 R3 화이트리스트에 명시 등록(현재 등록: dashboard 읽기, partner→user 쓰기).
  5. 위 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~R4domain 교차 import 0건(R1), 레이어 방향(R2), 지원→코어 금지(R3), common 순수성(R4)
architecture (신규)FR-4 경계 계약CreateMyProductUseCase/CreateMyEventUseCase 시그니처·반환 타입 존재 검증(회귀 동결)
domain/application/infra/presentation신규 비즈니스 로직 없음 → 신규 단위/통합 테스트 없음 (문서+규칙 과제)

핵심 실패 경로 시나리오(규칙 테스트가 잡아야 하는 위반):

  • domain.goodsdomain.ticketing.Event를 import → R1 위반으로 빌드 실패.
  • domain.bookinginfrastructure.*를 import → R2 위반.
  • application.operatordomain.payment를 동기 호출 → R3 위반.
  • application.partnerdomain.user를 호출(② B2B admin 프로비저닝) → R3 화이트리스트 예외로 통과(false RED 방지). 그 외 application.partner → 코어(user 제외) 접근은 R3 위반.
  • commondomain.user를 import → R4 위반.
  • CreateMyProductUseCase.execute 시그니처 변경 → 경계 계약 테스트 실패(② 과제 경유 파손 조기 감지).

Release Scenario — 무중단 배포 (의무)

이 과제는 런타임 동작을 바꾸지 않는다(문서 + 테스트 전용, 프로덕션 코드 무변경). 배포 리스크는 “규칙 테스트가 기존 코드를 잘못 붉게(RED) 만들어 CI를 막는 것”뿐이다.

  • 배포 순서: 코드 먼저 불필요. 규칙 테스트만 추가되므로 단일 PR로 배포.
  • 1단계 — 관찰 모드(피처 플래그 등가): ArchUnit 규칙 테스트를 추가하되, CI 게이트로 강제하지 않고 결과만 로그로 수집(테스트는 @Ignore 아닌 별도 태스크 archTest로 분리, check에 미포함). 베이스라인 0건 재확인.
  • 2단계 — 게이트 승격: archTestcheck/test에 편입해 위반 시 빌드 실패. Open Question(CI 게이트 강제 vs 리뷰 체크리스트)은 여기서 “게이트 강제”로 확정하되 승격은 1단계 0건 확인 후.
  • 전환 조건: 1단계에서 ./gradlew archTest 결과 위반 0건 → 2단계 승격.
  • 롤백: 규칙 테스트가 예상치 못한 위반을 내면 archTestcheck에서 분리(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건 확인.

#도메인대표 유즈케이스대조 결과
1booking예약 생성→payment 환불 Gateway코어→payment Customer-Supplier 일치
2facility시설 조회/등록코어, facility→booking(app) 일치
3goodsCreateMyProductUseCase코어, 제공 UseCase 경계 일치
4payment결제 완료→이벤트 발행코어, payment→notification 이벤트 일치
5ticketingCreateMyEventUseCase코어, 제공 UseCase 경계 일치
6user로그인/인가코어(신원 기반) 일치
7post게시글 작성코어(격리) 일치
8message채팅방 메시지코어(격리) 일치
9notificationpayment.completed.v1 소비지원, 코어 이벤트 소비 방향 일치
10operator운영자 인박스 조회지원(격리), 코어 동기 미호출 일치
11weather기상 조회지원(generic, 무소유) 일치
12mcp이상탐지 이벤트 소비서브시스템, 코어 이벤트 소비 방향 일치

불일치 0건. dashboard(조회 조합)·image(유틸)·common(공유 커널)도 분류표에 반영됨 — 커버 누락 0건.