사용자 정의 vs 코드 도메인 경계 갭 분석 아키텍처 문서

  • 대상: sports-application (스포츠앱), 백엔드 단일 모듈 모놀리스
  • 관점: 도메인 경계 적합성 (확장성·재사용성·통합)
  • 작성: 2026-07-06
  • 근거 입력: docs/domain-context-map.md, 스포츠앱/도메인 경계 재설계/{PRD,TDD}.md, 스포츠앱/채팅 시스템/20260704-채팅시스템고도화-prd.md, 실제 코드(backend/src/main/kotlin/com/sportsapp/**)
  • 성격: 의사결정 문서 (오버엔지니어링 경계 = 1급 책임). 상세 TDD·티켓은 senior-be/dba로 이어집니다.

⚠️ 정정 고지 (2026-07-06 재진단, stale 트리 오판 정정)

이 문서의 초판·1차 재검증(§1 row 14·15, §1-1 cold 재검증 “모임” 행, §5, §7 P1, §10 즉시착수 1)은 origin/dev보다 뒤처진 stale 로컬 트리에서 수행돼 community를 “VO 4개만 있는 orphan(본체 미구축)“으로 오판정했습니다. 실제 현재 dev에는 community가 완성된 바운디드 컨텍스트로 존재합니다.

재진단 트리 신선도 근거 (분석 전 자체 검증, 필수 게이트):

검증결과
git rev-list --left-right --count HEAD...origin/dev0 0 (HEAD == origin/dev)
HEAD5ff07c7c 2026-07-06 21:15 [BE-14] 채팅 응답 보강
git diff --stat HEAD -- backend변경 0 (backend 워킹트리 clean)
community 본체 도입 커밋eac1c925 커뮤니티 도메인(개설·가입·역할·승인) BE-08 — 초판이 놓친 커밋

stale 영향 범위: community 한 도메인에만 국한됩니다. 나머지 도메인(goods·ticketing·payment·booking·facility·user/partner·post)의 load-bearing 판정은 fresh tree에서 전부 재현됨(§AS-IS·§1 row 113·1618 유효). goods/booking/facility의 7월 커밋은 부하테스트 fix·지역 확장이라 도메인 경계 무변경, payment/ticketing/post는 6/4 패키지 마이그레이션 이후 무변경.

정정된 판정 요지 (상세는 §1 재정정 표·§10 재작성):

항목초판(오판)재진단(정정)
community 본체VO 4개 orphan, Entity/Repo/Service 0완성 컨텍스트 — Entity 2·DomainService(12메서드)·UseCase 10·Repository 3·이벤트 3·예외 6·VO 4·테스트 22
모임(폐쇄형)누락(스캐폴딩만)부분일치 — 멤버십·HOST/MEMBER 계층·PRIVATE 승인흐름·전용 채팅방 존재, 소모임 예약·모임 게시글 미연동
커뮤니티(개방형)부분일치(post+VO)부분일치 — PUBLIC 멤버십·SportCategory(종목별)·채팅 존재, 종목별 게시글·모집·체육관 예약선택 미연동
채팅↔community 연동진행 중(VO만 예약)구현 완료CommunityChatIntegrationEventWorker(Layer 1 AFTER_COMMIT, 이벤트+ID 연결)
§10 즉시착수 1 (community 본체 설계)미착수완료 — 실제 남은 즉시 착수로 재작성

아래 본문에서 오판정 행에는 [정정] 표기로 원문과 정정 내용을 병기합니다.


Background

사용자가 원본 요구사항으로 정의한 8개 도메인(유저·모임·커뮤니티·운동시설·상품·결제·주문 + 채팅)이 있고, 코드에는 19개 도메인 패키지가 있습니다. 두 목록은 명칭·경계가 어긋나 있어 “사용자가 1급 개념으로 본 것이 코드에는 어디에 있는가”를 정밀 대조했습니다.

이미 도메인 경계 재설계 TDD가 코드 기준 12개 도메인을 5계층으로 분류하고 ArchUnit 규칙(R1~R4)으로 교차 참조 0건을 강제하는 거버넌스를 확립했습니다. 이 문서는 그 위에서 “사용자 멘탈 모델과의 갭” 이라는 다른 축을 다룹니다 — 코드 내부 정합성이 아니라 제품이 정의한 개념과의 정합성입니다.


AS-IS 실측 (코드 근거)

코드 도메인Aggregate Root핵심 사실 (파일 근거)
goodsProduct, Cart, GoodsOrder, LimitedDrop, StockProduct.kt category = EQUIPMENT/APPAREL/FOOTWEAR/ACCESSORY, ownerId: Long만 보유 — 중고/브랜드 구분 없음. LimitedDrop.ktproductId 참조하는 별도 회차 Aggregate (한정판)
ticketingEvent, TicketOrder, Seat, TicketEvent.kt ownerId 보유, 좌석 재고 모델. TicketOrder.kt lockedSeatIds: List<Long>
bookingBooking, SlotSlot.kt 소유자가 timeRange(HH:mm-HH:mm)·capacity1건씩 수동 생성. Booking.kt 상태 전이만, 취소 정책 객체 없음
facilityFacility (MongoDB)공공데이터 임포트 카탈로그. ownerUserId 있음. 운영시간·휴무·자동 오픈/클로즈 필드 전무
paymentPaymentOrderConfirmationGateway.kt confirm/cancel(orderType, orderId, paymentId)OrderType = BOOKING/TICKETING/GOODS
postPost, CommentPostType.kt = FREE/NOTICE/QUESTION/REVIEW — RECRUITMENT 없음
messageRoom, Message, RoomParticipantRoom.kt contextType(COMMUNITY/GOODS_PRODUCT)·contextId 소프트 참조 + createForContext()
community[정정] Community, CommunityMember (완성 컨텍스트)초판: (VO 4개만) — Entity/Repository/Service 전무, orphan정정: Community.kt(name/description/visibility/sportCategory/hostUserId + 방장 위임·이벤트 적재), CommunityMember.kt(role/status/joinedAt, approve/kick/leave/rejoin/promote/demote 상태전이 캡슐화), CommunityDomainService.kt(create/join/approve/kick/leave/transfer + 조회 6종 = 12메서드), Repository 3(Community/CommunityMember/Custom), 이벤트 3(Created/MemberJoined/MemberLeft), 예외 6, VO 4(HOST/MEMBER·PUBLIC/PRIVATE·ACTIVE/PENDING_APPROVAL/LEFT/KICKED·SportCategory 12종). 채팅 연동 워커까지 구현
userUser, Role, UserRoleUserRoleName = USER/ADMIN/FACILITY_OWNER/EVENT_HOST/GOODS_SELLER/OPERATIONS_MANAGER — b2c/b2b는 RBAC 롤로 표현
partnerPartner, PartnerApiKeyPartner.kt linkedUserId — b2b API 연동은 User에 연결된 별도 신원

결정적 실측 3건:

  1. 모집(구인/구직) 개념 — grep -rni "recruit|모집|applicant" 결과 0건. 완전 부재.
  2. 취소·환불 정책 — Booking.refund()refundAmount외부에서 받은 인자로만 사용(BookingDomainService.kt:174). “하루 전 무료·체육관별 상이” 정책 객체 없음, 하드코딩 상태 머신뿐.
  3. [정정] 초판: community VO 4개는 채팅 TDD 첫 산출물, 도메인 본체는 미구현(진행 중)정정: community 본체는 구현 완료. eac1c925 BE-08로 Entity·Service·Repository·이벤트가 들어왔고, a701840b BE-09로 컨텍스트 방 provisioning(CommunityChatIntegrationEventWorker)이 붙었으며, 5ff07c7c BE-14로 컨텍스트 방 이름(CommunityCreatedEvent.name)까지 확장됨. Room contextType.COMMUNITY가 실제 소비됨(예약이 아니라 사용 중).

1. 도메인 일치 매트릭스 (사용자 정의 계층 vs 코드 배치)

범례: 일치 / 부분일치 / 경계불일치 / 누락 / 코드전용(사용자 정의에 없음)

판정 원칙 (재작성 계기): 이전 매트릭스는 “개념이 코드에 존재하는가”만 보고 티켓을 일치로 오판정했습니다. 사용자가 정의한 계층(상품 ⊃ 중고·브랜드·한정판·티켓·시설상품)이 경계의 SSOT이므로, “존재”가 아니라 “사용자 계층·경계와 코드 배치가 같은가” 로 판정합니다. 하위 유형이 상위(상품) 그룹핑 밖 독립 최상위에 놓이면 경계불일치, 상위 개념 자체가 코드에 없으면 별도 행으로 누락을 기록합니다.

각 판정에 근거 2종을 답니다 — ① 존재 위치(코드에 있는 곳) + ② 경계 일치 판단 근거(사용자 계층과의 배치 대조).

#사용자 정의 개념 (계층)코드 위치판정① 존재 위치 (파일:라인)② 경계 일치 판단 근거 (파일:라인 / grep)
1유저 ⊃ b2c/b2buser + partner부분일치domain/user/entity/User.kt, UserRoleName(USER/ADMIN/FACILITY_OWNER/EVENT_HOST/GOODS_SELLER/OPERATIONS_MANAGER), domain/partner/entity/Partner.kt(linkedUserId)유저 상위는 존재하나 b2c/b2b 하위 1급 타입 없음 — RBAC 롤로 대체. 사용자 계층의 “b2c/b2b” 구분이 계정 타입이 아닌 롤로 흡수됨(경계는 유지, 하위 판별만 부분)
2상품 (상위 개념 자체)(없음)누락grep -rni "Sellable|abstract class Product|SellableItem|공통 Catalog" domain/ = 0건. goods·ticketing을 아우르는 공통 상위 타입/추상 부재 (domain/goods/gateway/ReservationResult.kt:13의 sealed interface는 예약 결과 타입일 뿐 상품 상위 아님) — 사용자의 “상품” 상위 개념이 코드에 앵커 없음
3상품 ⊃ 중고(b2c)goods.Product부분일치domain/goods/entity/Product.kt:20(Product), vo/ProductCategory.kt:3(EQUIPMENT/APPAREL/FOOTWEAR/ACCESSORY)Product는 goods(=상품 그룹핑) 안에 있어 위치 경계는 정합. 그러나 Product.kt:20-42 생성자 필드에 중고/브랜드 판별 필드(condition·used·brand) 없음 — category는 물품종류만. 하위 구분 미표현
4상품 ⊃ 브랜드(b2b)goods.Product부분일치domain/goods/entity/Product.kt:20, Stock(재고)3번과 동일 aggregate. Product.kt:42 ownerId: Long만 보유 — b2b 판별은 소유자 롤(GOODS_SELLER)로만. 위치는 정합, 하위 판별 필드 없음
5상품 ⊃ 한정판goods.LimitedDrop일치domain/goods/entity/LimitedDrop.kt:33(private constructor), LimitedDropStatus.kt, service/LimitedDropDomainService.ktLimitedDrop.kt:35 val productId: Long — goods 산하에서 Product를 참조하는 회차 aggregate. 사용자 계층(상품⊃한정판)과 동일하게 goods(=상품) 그룹핑 내부에 배치. 경계 정합
6상품 ⊃ 티켓ticketing (독립 최상위)경계불일치domain/ticketing/entity/Event.kt:19(Event), Ticket.kt, Seat.kt, TicketOrder.kt — 독립 최상위 패키지 domain/ticketing/grep "import com.sportsapp.domain.goods" domain/ticketing/ = 0건, 역방향 goods→ticketing = 0건. Event.kt:19 class Event가 Product 상속·참조 없이 ownerId(Event.kt:38) 자체 보유. 사용자 계층은 상품⊃티켓인데 코드는 티켓을 goods와 형제인 독립 최상위로 둠 → 계층 불일치
7상품 ⊃ 시설상품(PT·클래스)(없음)누락find domain/facility = program/lesson/PT/class 파일 0. grep -rni "program|lesson|pilates|personal.training" facility 실질 매치 0건(매치는 “application” 언급·data class 키워드뿐). PT/클래스 판매 개념 부재
8주문 (단일)goods.GoodsOrder + ticketing.TicketOrder + booking.Booking부분일치domain/goods/entity/GoodsOrder.kt, domain/ticketing/entity/TicketOrder.kt, domain/booking/entity/Booking.kt사용자는 주문을 단일 개념으로 보나 코드는 3분할. domain/payment/vo/OrderType.kt(BOOKING/TICKETING/GOODS) + OrderConfirmationGateway로 결제 접점만 통합. 상위 “주문” 경계는 없고 판매 타입별 분산
9결제payment.Payment일치domain/payment/entity/Payment.kt, gateway/OrderConfirmationGateway.kt(confirm/cancel(orderType,orderId,paymentId))사용자 정의 “결제” 단일 개념과 코드 payment 단일 컨텍스트가 1:1. 3개 판매 도메인의 공급자 허브·ACL 역콜백. 경계 정합
10운동시설 등록facility.Facility (MongoDB)일치domain/facility/entity/Facility.kt:22(Facility), service/PublicFacilityImportService.kt, FacilityOwnerDomainService.ktFacility.kt:82-86 assignOwner/isOwnedBy(ownerUserId). 등록·소유자 지정 개념이 facility에 정합 배치
11시설 타임슬롯 예약booking.Slot + Booking부분일치domain/booking/entity/Slot.kt:16(private constructor), Booking.ktSlot.kt:24 timeRange: String(HH:mm-HH:mm)·capacity(Slot.kt:38)로 수동 1건 생성. 예약 개념은 존재하나 사용자가 정의한 “타임슬롯” 자동 생성 흐름 미비
12시설 자동 오픈/클로즈·휴무·수동 제어(없음)누락Facility.kt:22-58 생성자 필드 = code/name/gu/type/address/location/parking/tel/homePage/eduYn/meta/ownerUserId/sido·sigungu. operatingHours·holiday·openTime/closeTime 필드 전무. 스케줄러 없음
13체육관 취소 정책(하루 전 무료·시설별 상이)(없음)누락Booking.refund()refundAmount를 외부 인자로만 수령(domain/booking/service/BookingDomainService.kt:174). CancellationPolicy 정책 객체·시간 기준·수수료율 부재
14모임 (모임장·유저 계층, 소모임 예약, 모임 게시글)community (PRIVATE)[정정] 부분일치 (초판: 누락(스캐폴딩만))domain/community/entity/Community.kt:42(hostUserId)·CommunityMember.kt:42(role), CommunityRole.kt(HOST/MEMBER), CommunityVisibility.PRIVATE(승인 후 가입), CommunityChatIntegrationEventWorker.kt:45-100(전용 방 자동 provision/join/leave)[정정]: 모임장·유저 계층(HOST/MEMBER) ✅, 유저·그룹 채팅(자동 전용 방) ✅, PRIVATE=폐쇄형 승인흐름 ✅ 존재. Community.kt:68 transferHostTo로 방장 위임까지. 남은 갭: 소모임 예약(booking 연동)·모임 게시글(post 연동) 미연결. 모임장 중간 계층(MANAGER)은 HOST/MEMBER 2계층뿐이나 사용자가 “확장 가능”으로 명시 → YAGNI 정당(누락 아님)
15커뮤니티 (종목별 게시글, 모집, 체육관 예약 선택)community (PUBLIC) + post부분일치 (판정 유지, 근거 정정)domain/community/entity/Community.kt:34-40(visibility·sportCategory), SportCategory.kt(12종목), CommunityVisibility.PUBLIC(즉시 가입), domain/post/vo/PostType.kt(FREE/NOTICE/QUESTION/REVIEW)[정정]: 개방형 멤버십(PUBLIC)·종목별 분류(SportCategory 12종) ✅, 검색(findPublicByKeyword) ✅ 존재. 남은 갭: 종목별 게시글 연동(post ID참조 없음)·모집(recruitment 부재)·체육관 예약선택(booking 연동 없음) 미구현. post는 범용 게시판이라 community↔post 결합 아직 0
16커뮤니티 모집(구인/구직, 정원, 취소 10% 수수료)(없음)누락grep -rni "recruit|모집|applicant" domain/ 실질 매치 0건(매치는 “application” 레이어·ApplicationEvent뿐). recruitment/신청/정원 개념 전무
17유저간/그룹 채팅message.Room/Message일치 (근거 강화)domain/message/vo/RoomContextType.kt(COMMUNITY/GOODS_PRODUCT), application/message/usecase/{Provision,Join,Leave}ContextRoomUseCase.kt[정정]: 초판 “실시간 STOMP 확장 진행 중”은 stale. 현재 message가 범용 컨텍스트 방 메커니즘(Provision/Join/Leave UseCase + RoomContextType)을 제공하고, community가 이를 이벤트 기반(Layer 1)으로 재사용 — 채팅 백본이 실제로 2개 도메인(community 이벤트연동·goods GOODS_PRODUCT)에 재사용됨. 사용자 정의 “모임·그룹 채팅”과 정합
18— (사용자 정의에 없음)mcp, alerting, featureflag, featuredemo, operator, weather, airquality, notification코드전용domain/{mcp,alerting,featureflag,featuredemo,operator,weather,airquality,notification}사용자 8개 정의 도메인에 없는 운영·플랫폼·부가 도메인. 인프라·운영 관심사라 정상(경계 판정 대상 아님)

판정 기준: 사용자 정의 계층(상품 ⊃ 중고·브랜드·한정판·티켓·시설상품)이 SSOT이며, 개념 존재가 아니라 경계·계층 일치로만 일치로 판정한다.

재집계 [정정, fresh tree]: 일치 4(한정판·결제·운동시설등록·채팅) / 부분일치 7(유저·중고·브랜드·주문·시설타임슬롯예약·모임·커뮤니티) / 경계불일치 1(티켓) / 누락 5(상품상위·시설상품·시설운영시간·취소정책·모집) / 코드전용 1(그룹행: 8개 운영 도메인).

초판: 부분일치 6, 누락 6(…·모임·모집)정정: 모임이 누락(스캐폴딩만)부분일치로 이동해 부분일치 6→7, 누락 6→5. 모집(recruitment)만 여전히 완전 누락(grep -rli "recruit|모집|구인|구직" domain/ = 0, fresh tree 재확인).

핵심 갭 2종:

  1. 경계불일치 — 티켓: 사용자는 상품의 하위 유형으로 보나 코드는 ticketing을 goods와 형제인 독립 최상위 도메인으로 둠(goods↔ticketing import 0건). 이전 매트릭스가 “티켓이 코드에 존재한다”만 보고 일치로 오판정한 지점을 바로잡음.
  2. 누락 — “상품” 상위 개념 자체: goods·ticketing·(부재하는)시설상품을 아우르는 공유 상위 추상(Sellable/공통 카탈로그)이 grep 0건. 사용자 멘탈 모델의 최상위 그룹핑이 코드에 앵커 없음. 단 §4 문제A 판정대로 통합 aggregate 신설은 미채택(라이프사이클 이질) — 통합축은 payment.OrderType이며, 이 “누락”은 결함이 아니라 의도된 분산의 부산물로 해석함.

1-1. Cold 재검증 (2026-07-06, 독립 스폰 — 존재≠경계정합 회귀 확인)

답을 흘리지 않은 cold 스폰이 §1 매트릭스의 load-bearing 판정을 코드로 직접 재검증했습니다. 모든 판정이 재현됨 — 특히 티켓이 일치가 아니라 경계불일치로 재확인되어 “존재=일치” 회귀가 없음을 검증했습니다.

판정재검증 명령 / 근거결과
티켓 경계불일치 (독립 최상위)grep "import com.sportsapp.domain.goods" domain/ticketing/ = 0, 역방향 = 0. Event.kt:19-39 Product 상속·참조 없이 ownerId 자체 보유재현 — goods와 형제 독립 도메인 확정
한정판 일치 (goods 하위)LimitedDrop.kt:35 val productId: Long, 패키지 domain/goods/entity/재현 — 상품(goods) 그룹핑 내부 정합
중고/브랜드 부분일치Product.kt:20-42 필드 = name/category/price/description/imageUrl/status/ownerId. condition·used·brand 판별 필드 없음, ProductCategory는 물품종류(EQUIPMENT/APPAREL/…)만재현 — 위치 정합, 하위 판별 필드 없음
상품 상위 개념 누락grep -rni "sellable|abstract class Product|SellableItem" domain/ = 0재현
시설상품(PT·클래스) 누락grep -rni "program|lesson|pilates|수업|클래스" domain/facility/ = 0재현
시설 운영시간·휴무 누락grep -rni "operatingHour|openTime|closeTime|holiday|휴무" domain/facility/ = 0재현
모집(구인/구직) 누락grep -rni "recruit|모집|구인|구직" domain/ (application 제외) = 0재현
취소 정책 누락BookingDomainService.kt:174 refundBooking(..., refundAmount: BigDecimal, ...) 외부 인자 수령. CancellationPolicy 0건재현
모임 도메인 누락(스캐폴딩만)[정정] 부분일치stale 오판. fresh tree 재실측: group/gathering/meetup 별도 패키지 0개(모임 전용 도메인은 없음, 이 부분만 재현)이나 community 본체 존재find domain/community -type f = 19파일(entity 2·service 1·repository 3·event 3·exception 6·vo 4), CommunityChatIntegrationEventWorker.kt 외부 연동 있음(orphan 아님)정정 — 모임은 별도 도메인이 아니라 community(PRIVATE)로 표현됨. “orphan/본체 0” 판정 철회
OrderType 3분기 (확장 마찰)OrderConfirmationGatewayImpl.kt:18-29 when(orderType) × 2(confirm/cancel), OrderType=BOOKING/TICKETING/GOODS재현 — 타입 추가 시 선형 증가 확인

재검증 결론: §1 매트릭스는 사용자 정의 계층(상품 ⊃ 중고·브랜드·한정판·티켓·시설상품)을 SSOT로 정확히 판정함. “패키지가 존재하니 일치”라는 이전 오판 패턴이 재발하지 않음.

⚠️ 단, 위 표 자체가 stale 트리 기반이라 “모임” 행은 오판이었음 (2026-07-06 재진단에서 정정). fresh tree(HEAD=5ff07c7c)에서 community 본체가 확인돼 모임을 부분일치로 정정함. 티켓/상품상위/시설상품/시설운영시간/모집/취소정책 등 community 외 판정은 fresh tree에서 전부 재현됨(§정정 고지 stale 영향 범위 참조). 이 재검증 표는 community 오판을 걸러내지 못한 것이 한계 — cold 스폰도 같은 stale 트리를 봤기 때문. 트리 신선도 게이트를 재검증 앞단에 두는 것이 재발 방지책(§10 반영).


2. 트래픽↔구조 적합성 진단 (3단계 verdict)

개인 프로젝트로 실측 트래픽은 없습니다. 추정 전제(사용자 규모·기능 특성 기반, “추정” 명시):

지표추정근거
상시 RPS10 미만개인 프로젝트, 단일 브로커 로컬 docker compose
마케팅/한정판 피크수백 RPS 순간마케팅 이벤트 고부하 대응 PRD 존재 = LimitedDrop 오버셀 대응이 이미 설계됨
데이터 증가율낮음거래·게시글 소량

전체 verdict: 적합 (과잉 아님, 부족 아님)

  • 상시 RPS <10에 단일 모놀리스 + 단일 MySQL + MongoDB + 단일 Redis + 단일 Kafka 브로커는 정확히 규모에 맞음. 모놀리스 분리·다중 인스턴스·읽기 복제는 지금 과잉(조기 도입).
  • 단 한정판/마케팅 피크(수백 RPS 순간, 재고 경합)는 특정 컨텍스트에서 임계점 근접 — 이 부분만 이미 Redis 락·예약 스토어로 선제 대응됨(§3 캐싱 참조).

컨텍스트별 판정 (부하 특성이 갈림)

3단계 = 지금은 충분(적합) / 임계점 근접 / 이미 초과.

컨텍스트부하 특성판정근거 수치·코드다음 단계 임계 조건
ticketing (티켓·좌석 오픈)순간 스파이크 + 좌석 경합임계점 근접피크 수백 RPS 추정, SeatLockStoreImpl.kt(Redis 좌석 락) 이미 존재락 경합으로 P95 예약 지연 >1s 관측 시 → 재고/좌석 서비스 분리·큐 버퍼링
goods (한정판 드롭)오픈 순간 스파이크 + 재고 차감 경합임계점 근접DropReservationStoreImpl.kt·PopularProductsRedisRepository.kt·LimitedDrop 오버셀 감지 이미 존재재고 차감 경합으로 오버셀 이벤트 빈발 시 → 재고 원장 분리
payment (결제 확정)판매 스파이크 동반 + 외부 PG 지연임계점 근접(동반)판매 3종의 공급자, PG 왕복이 지배적PG 타임아웃 누적·확정 콜백 적체 시 → 결제 워커 분리·비동기 확정
booking / facility낮은 상시, 스파이크 없음적합(여유)슬롯 수동 생성, 예약 소량시설 전국 확장으로 슬롯 조회 P95 >500ms 시 → 조회 캐싱
community / message / post낮은 상시, 팬아웃형 쓰기적합(여유)채팅 STOMP 세션, 상시 소량동시 소켓 세션 수천 초과 시 → 소켓 계층 수평 확장 + Redis pub/sub 브로커 릴레이
notification / dashboard / weather / airquality비동기 소비·읽기 집계적합(여유)Kafka 소비·읽기 조합대시보드 집계 쿼리 P95 >2s 시 → 사전 집계 읽기 모델

구조 판정의 기본 자세: 상시 부하 기준 현행 구조는 적합하며, 통합/분리 대공사는 대부분 조기 도입입니다. 유일하게 관측할 지점은 ticketing/payment/goods의 피크 경합이고, 이는 이미 Redis 락·예약 스토어로 1차 방어됨.


3. 인프라 구조적 해결책 판정 (지금 불필요 + 도입 트리거)

각 패턴을 “지금 판정 / 도입 트리거(측정 가능 조건) / 미도입 리스크”로 명시합니다. 선제 도입은 오버엔지니어링으로 경계합니다. Redis·Kafka는 compose에 이미 있어 코드 실사용처를 grep 실측해 “도입됨/부분/확장 여지”로 구분했습니다.

패턴지금 판정실측 사용처 (파일 근거)도입/승격 트리거미도입 리스크
캐싱 (Redis)이미 도입됨look-aside: PopularProductsRedisRepository.kt·AirQualityRedisCache.kt·CacheConfig.kt. 락: RedisDistributedLock.kt·SeatLockStoreImpl.kt. 예약: DropReservationStoreImpl.kt. 세션/토큰: RedisJwtBlacklistStore.kt·RedisRefreshTokenRepository.kt. 플래그: RedisFeatureFlagCacheStore.kt. 쿨다운: AlertCooldownRepositoryImpl.kt확장 여지: booking 슬롯 조회 캐싱(전국 확장 시), dashboard 사전 집계해당 없음 — 핵심 경합 지점은 이미 커버
큐 버퍼링 (Kafka)이미 도입됨(Layer 2)발행 KafkaDomainEventPublisher.kt, 소비 NotificationEventWorker.kt, 설정 KafkaProducerConfig.kt. payment/booking/ticketing → notification 비동기확장 여지: 티켓 오픈 시 예약 요청을 큐로 버퍼링(선착순 완화)피크 시 동기 처리 적체 — 단 상시 부하선 문제 없음
로드밸런싱 / 수평 확장지금 불필요단일 인스턴스 (docker compose)단일 인스턴스 CPU >70% 지속 또는 P95 SLA 초과 관측없음 — 상시 RPS <10
API 게이트웨이지금 불필요Spring Security 필터가 인증·라우팅 담당, 단일 앱앱이 2개 이상 배포 단위로 쪼개져 공통 인증·라우팅·rate-limit 필요 시없음 — 단일 앱에 게이트웨이는 홉 추가만
BFF (Backend for Frontend)지금 불필요web/app이 동일 REST 소비 (공용 API·useChatSocket 등)web·app 응답 형태가 크게 갈려 조합 로직 중복 심화 시없음 — 현재 응답 형태 공유로 충분
CQRS / 읽기 모델 분리부분 도입(읽기 조합)dashboard가 읽기 전용 조합(Conformist). 쓰기와 분리된 별도 저장소는 아직 없음집계 쿼리 P95 >2s 또는 쓰기 경합 시 → 사전 집계 읽기 모델(§6 catalog/order-query)없음 — 집계량 소량
MSA 물리 분리지금 불필요 (명시 미채택)모놀리스 유지 (도메인 경계 재설계 PRD Non-Goals)ticketing/payment 피크 부하가 측정상 모놀리스 임계 초과 시에만 (§7 P6)없음 — 조기 분리는 분산 트랜잭션·운영 비용만 유발

핵심: 필요한 인프라(캐싱·큐·락)는 경합 지점에 이미 정확히 도입돼 있고, 나머지(LB·게이트웨이·BFF·MSA)는 상시 부하선에서 전부 불필요합니다. 도입 트리거는 모두 측정 가능한 관측치로 걸어 두었습니다.


4. 구조적 해결책 — 도메인 통합/분리 문제별 후보 비교

문제 A. 상품이 5종으로 분산 (중고/브랜드/한정판/티켓/시설상품)

사용자는 상품을 단일 상위 개념으로 봅니다. 코드는 goods(물품+한정판)·ticketing(티켓)에 흩어져 있고 시설상품은 없습니다.

후보설명처리량/복잡도판정
A-1 단일 Product 아그리게잇 통합Sellable 하나로 5종 흡수, 타입별 필드 다형화라이프사이클 이질성(중고=단건 C2C, 브랜드=SKU 재고, 티켓=좌석재고+공연, 한정판=시간게이트, 시설상품=슬롯)을 한 aggregate에 밀어넣어 God-aggregate·다형 필드 폭증미채택 — 통합 이득(공통 CRUD) < 이질성 비용. 오버엔지니어링
A-2 현행 분산 유지 + 판매 접점만 통합goods/ticketing 쓰기 모델 유지, payment OrderType·OrderConfirmationGateway가 이미 결제 접점 통합추가 비용 0, 이미 동작채택(현행 유지) — 결제라는 진짜 공통 지점은 이미 ACL로 통합됨
A-3 통합 Catalog 읽기 모델(조회 파사드)교차 타입 검색·통합 피드가 실제로 필요해질 때 dashboard처럼 읽기 전용 조합 컨텍스트 신설읽기 조합만, 쓰기 소유권 불변조건부 채택 — 통합 상품 피드/검색 화면이 스펙에 잡히면
A-4 시설상품(PT·클래스) 신규 모델링facility 산하 program(수업 회차) 또는 신규 컨텍스트신규 기능, goods 재사용 부적합(슬롯·정원 기반)조건부 채택 — 시설상품 FR이 잡히면. goods에 넣지 말 것

결론: 상품 통합 아그리게잇은 만들지 않습니다. “상품”은 사용자 멘탈의 상위 개념일 뿐, 코드에선 결제 접점(OrderType)이 이미 통합축입니다. 시설상품은 goods가 아니라 facility/booking 계열에 새로 모델링 — 슬롯·정원·스케줄이 본질이라 물품 커머스와 다릅니다.

경쟁사 비교:

  • 당근마켓 — 중고(C2C)와 비즈프로필 상품(B2C)을 별도 도메인으로 유지, 통합은 검색/피드 읽기 레이어에서만. A-2/A-3 근거. (당근 채팅/거래 구조)
  • 쿠팡/네이버 스마트스토어 — 상품(카탈로그)과 예약형 상품(로컬/클래스)을 서로 다른 판매 모델로 분리 운영. 단일 aggregate 통합 안 함.

문제 B. 주문이 3분할 (GoodsOrder / TicketOrder / Booking)

후보설명판정
B-1 단일 Order 아그리게잇 통합3종을 다형 line-item 하나로미채택 — 아이템 구조 이질(품목 리스트 vs 좌석락 vs 단일 슬롯). 다형 라인아이템 = 복잡도 폭증, 지금 규모에 과함
B-2 현행 3분할 + OrderType 접점 유지이미 payment가 OrderType + OrderConfirmationGateway로 확정/취소 통합채택(현행 유지) — 결제/확정이라는 공통 관심사는 이미 통합됨
B-3 통합 “내 주문” 조회 파사드유저의 주문 이력 교차 조회가 필요할 때 읽기 전용 Query 컨텍스트(dashboard 패턴)조건부 채택 — 통합 주문 이력 화면이 잡히면. 쓰기 aggregate 아님

결론: 통합 Order 쓰기 aggregate는 미채택. 통합이 진짜 필요해지는 트리거는 주문 레벨 횡단 관심사(통합 환불 정책, 쿠폰/프로모션이 타입 교차 적용, 통합 주문 상태 추적)가 스펙에 등장할 때입니다. 그전까지 B-2 유지가 옳습니다.

경쟁사 비교:

  • Shopify — 물리 상품/디지털/구독을 Order 하위 line-item 다형으로 통합하지만, 이는 대규모·단일 커머스 도메인 전제. 개인 프로젝트 규모엔 부적용(미참고). (Shopify 모놀리스)
  • Uber Eats/배민 — 주문(음식)과 예약은 별도 도메인 유지, 결제만 공유. B-2 근거.

문제 C. 모임/커뮤니티/모집 신규 컨텍스트

사용자는 모임(폐쇄형 소모임: 모임장·유저 계층, 소모임 예약, 모임 내 게시글)커뮤니티(개방형: 종목별 게시글, 모집, 체육관 예약 선택) 를 구분합니다. 채팅 고도화 PRD는 둘을 “커뮤니티(동아리·모임)” 하나로 합쳐 진행 중(VO만 존재).

후보설명판정
C-1 모임·커뮤니티를 2개 컨텍스트로 분리폐쇄형·개방형을 별도 aggregate미채택(현 단계) — 멤버십·역할(HOST/MEMBER)·채팅 연동이 동일. 조기 분리 = 중복. Visibility(PUBLIC/PRIVATE)로 한 컨텍스트에서 구분 가능
C-2 단일 community 컨텍스트 (Visibility로 개방/폐쇄 구분)채팅 PRD 노선. Community + CommunityMember aggregate, Room contextType 연동채택 — VO·Room enum 이미 예약됨. 진행 중 로드맵과 정합
C-3 recruitment(모집)을 별도 컨텍스트모집 게시글·신청·정원·취소 10% 수수료를 Recruitment+Application aggregate로. post/community ID 참조, payment로 수수료채택(후속 단계) — post(범용 콘텐츠)·community(멤버십)와 라이프사이클 다름. payment 수수료 접점 필요

모임장·유저 중간 계층 확장: 현재 CommunityRole은 HOST/MEMBER 2계층. 사용자는 “중간 계층 확장 가능”이라 했으나 지금 MANAGER 계층 선제 도입은 YAGNI — enum 확장은 저비용이므로 실제 운영 위임 요구가 생길 때 추가. 현행 2계층 유지가 옳습니다.

모집을 어디에 둘 것인가: post에 넣으면(RECRUITMENT PostType 추가) 게시판이 정원·신청·수수료·상태 전이를 떠안아 오염됩니다. recruitment는 결제(수수료)·정원 상태 머신·신청 라이프사이클을 소유하는 독립 컨텍스트가 맞습니다. post/community는 ID로만 참조.

경쟁사 비교:

  • 네이버 밴드 — 모임(밴드) 단위 멤버십 + 전용 그룹채팅 1:1 종속. C-2 근거(채팅 PRD FR-4와 동일 패턴). (밴드)
  • 소모임/프립 — 모임(그룹)과 모집(신청·정원·환불 수수료)을 별도 개념으로 운영. 신청 취소 수수료 정책 존재. C-3 근거.

문제 D. 체육관 취소 정책 확장성 (하드코딩 → 정책 객체)

현재 Booking.refund(refundAmount)은 금액을 외부 인자로 받고, cancel()은 상태 전이만 검사. “하루 전 무료·시설별 상이”가 코드에 없습니다.

후보설명판정
D-1 지금 취소 정책 엔진 구축CancellationPolicy 전략 객체 + 시설별 설정미채택(지금) — 취소 수수료 기능 자체가 아직 스펙에 없음. 소비처 없는 정책 엔진 = 오버엔지니어링
D-2 기능 도입 시 정책을 VO/전략으로취소 FR이 잡히면 CancellationPolicy(취소 가능 시점·수수료율)를 facility가 소유, booking이 조회해 환불액 계산채택(설계 원칙 예약) — 전략 패턴. refundAmount 외부 주입을 정책 계산으로 대체. 시설별 상이 = 시설이 정책 소유

결론: 지금 만들지 않되, 설계 원칙을 못박습니다 — 취소/환불 수수료가 스펙에 들어오는 순간 금액을 인자로 받지 말고 CancellationPolicy 전략 객체로 계산하며, 정책 소유권은 facility(체육관별 상이)에 둡니다. 모집 취소 10% 수수료도 같은 패턴을 recruitment 컨텍스트에서 재사용.

문제 E. 시설 운영시간·자동 슬롯·휴무 (누락)

Slot이 수동 1건 생성이라 “실내 시설 자동 오픈/클로즈 + 휴무 + 수동 오픈/클로즈”가 불가.

후보판정
E-1 facility에 운영시간(OperatingHours)·휴무(Holiday) VO 추가 + 스케줄러가 Slot 자동 생성채택(후속) — 운영시간은 시설 속성(facility 소유), 슬롯 생성은 booking. 수동 오픈/클로즈는 Slot 상태 필드

지금은 갭으로 기록만. 시설 예약 고도화 FR이 잡힐 때 착수.

문제 F. b2c/b2b 유저 표현

현재 RBAC 롤(GOODS_SELLER/EVENT_HOST/FACILITY_OWNER) + Partner(API 연동 신원). 별도 계정 타입 없음.

후보판정
F-1 b2c/b2b 계정 타입 1급 분리미채택 — 한 유저가 중고 판매(b2c)도 하고 브랜드 판매(b2b)도 할 수 있음. 타입 분리는 이 다중성을 깨뜨림
F-2 현행 RBAC 롤 + Partner 유지채택 — 롤은 능력(capability) 부여라 다중 역할 자연 지원. b2b API 통합만 Partner로 분리(적절). 소유권은 userId 네임스페이스 일원화

결론: 현행이 확장상 옳습니다. b2c/b2b는 “계정 종류”가 아니라 “유저가 가진 롤 + (선택) Partner 연동”으로 표현하는 것이 다중 역할·점진 승격에 유리.


5. 바운디드 컨텍스트 재설계 제안 (현재 → 제안)

현재 경계제안 경계근거(변경 주기·소유·라이프사이클)
[정정] community 컨텍스트 (본체 구축 완료)본체 유지 + 콘텐츠·모집·예약 연동 추가초판: VO만 orphan → 본체 구축(예정)정정: 본체는 이미 완성(Community+CommunityMember aggregate, Visibility 개방/폐쇄 겸용, HOST/MEMBER 2계층, 채팅 자동연동). 남은 확장은 post(게시글)·booking(예약)·recruitment(모집) 연동
모집 개념 없음recruitment 신규 컨텍스트 (Recruitment + Application)정원 상태 머신·신청 라이프사이클·취소 수수료(payment)를 소유. post/community와 다른 변경 주기
상품 5종 분산분산 유지 + (조건부) catalog 읽기 파사드라이프사이클 이질. 통합축은 이미 payment.OrderType
시설상품(PT·클래스) 없음facility 산하 program 개념 (신규 시)슬롯·정원 기반 — goods 아님
주문 3분할분산 유지 + (조건부) 통합 주문 조회 파사드아이템 구조 이질. 결제 접점만 통합
취소 정책 하드코딩CancellationPolicy 전략(facility 소유) — 기능 도입 시체육관별 상이 = 시설이 정책 소유
user/partner현행 유지RBAC + Partner가 b2c/b2b 다중성에 적합
post (범용)현행 유지 — 모임 게시글은 community가 ID 참조post 오염 방지

계층 재분류 (기존 도메인 경계 재설계 TDD 5분류에 신규 편입)

분류도메인신규/변경
Core(거래)booking, facility, goods, payment, ticketing, user, post, message, communitycommunity를 Core로 편입(사용자 대상 핵심 자산·멤버십 소유). [정정] community 본체는 구현 완료 — 단 DomainClassification.core 상수에는 미등록(§10 즉시착수 2)
Core(거래, 후속)recruitment모집·신청 aggregate. payment Customer 관계
Supportingnotification, operator, weather, alerting불변
Subsystemmcp불변
Shared Kernelcommon불변
Read/Utilitydashboard, image, (조건부) catalog, order-query통합 조회 파사드는 여기로

6. 사용자 정의 도메인 앵커 재정렬 (TO-BE)

사용자가 원본으로 정의한 8개 도메인(유저 / 모임 / 커뮤니티 / 운동시설 / 상품 / 결제 / 주문 / 채팅)을 최상위 앵커로 두고, 그 아래 현재 코드 도메인이 어떻게 매핑·편입되는지 정렬합니다. 목표 기준은 확장·재사용·통합입니다.

사용자 도메인 → 코드 도메인 매핑

사용자 도메인 (앵커)편입되는 코드 도메인통합/재사용 판정확장 여지
유저user (신원·RBAC) + partner (b2b API 연동)통합 유지 — b2c/b2b는 롤 조합으로 표현, 계정 타입 분리 안 함신규 롤 enum 추가로 확장 (저비용)
모임 (폐쇄형)community (Visibility=PRIVATE) + message(전용 방) + post(모임 게시글, ID참조)community 한 컨텍스트로 개방/폐쇄 겸용 — 조기 2분할 안 함HOST/MEMBER → MANAGER 중간 계층은 필요 시 enum 확장
커뮤니티 (개방형)community (Visibility=PUBLIC) + post(종목별 게시글) + recruitment(모집) + booking(체육관 예약 선택)모임과 같은 community 컨텍스트, 모집만 recruitment로 분리종목 카테고리(SportCategory) 이미 VO 존재
운동시설facility (마스터) + booking (슬롯·예약)통합 유지 — 시설 속성/예약 라이프사이클 분리가 옳음OperatingHours·Holiday VO(facility), 자동 슬롯 생성(booking)
상품goods(중고·브랜드·한정판) + ticketing(티켓) + facility program(시설상품, 신규)통합 aggregate 안 함 — 통합축은 payment.OrderType. 필요 시 catalog 읽기 파사드만시설상품(PT·클래스)은 facility 계열 신규
주문goods.GoodsOrder + ticketing.TicketOrder + booking.Booking통합 aggregate 안 함 — 확정/취소는 OrderConfirmationGateway로 이미 통합. 필요 시 order-query 읽기 파사드통합 환불 정책 등장 시 order-query
결제payment그대로 (공급자 허브, ACL 역콜백)
채팅message (Room contextType/contextId)범용 확장 메커니즘 재사용 — community 1번째, goods 2번째 연동booking·ticketing 연동은 후속

TO-BE 컨텍스트 맵 (사용자 도메인 앵커, Mermaid LR)

flowchart LR
    subgraph 유저["유저 (user·partner)"]
        user
    end
    subgraph 모임커뮤["모임·커뮤니티 (community)"]
        community
        recruitment
    end
    subgraph 채팅콘텐츠["채팅·콘텐츠 (message·post)"]
        message
        post
    end
    subgraph 상품주문["상품·주문 (분산 유지)"]
        goods
        ticketing
        booking
    end
    subgraph 시설["운동시설 (facility)"]
        facility
    end
    subgraph 결제["결제 (payment)"]
        payment
    end
    goods -->|OrderType C/S| payment
    ticketing -->|OrderType C/S| payment
    booking -->|OrderType C/S| payment
    recruitment -->|취소수수료 C/S| payment
    facility -->|SlotQuery ACL| booking
    community -->|contextId 소프트참조| message
    community -->|게시글 ID참조| post
    recruitment -->|모집글 ID참조| post
    user -->|소유 userId 네임스페이스| 상품주문
  • 실선: 동기(app 오케스트레이션/Gateway ACL). 점선/ID참조: 도메인 교차 import 금지(ArchUnit R1 유지, ID만).
  • 통합 없이 재사용: 결제(OrderType)·채팅(contextType)·게시글(ID참조)이 여러 사용자 도메인에 재사용되는 축. 쓰기 aggregate 통합은 하지 않고 이 재사용 접점만 유지·확장.
  • catalog·order-query 읽기 파사드는 조건부(트리거 도달 시)라 이 맵에서 생략.

7. 장기 도메인 진화 계획 (사용자 정의 도메인 기준, 단계 + 트리거)

각 단계를 사용자 정의 도메인에 앵커링합니다 (진화 수준: 모듈 내 도메인 추가 → 서비스 분리 → DB 분리).

단계사용자 도메인 앵커작업진화 수준트리거 조건선행 조건
P0 (완료)전체컨텍스트 맵 + ArchUnit 거버넌스문서+규칙완료됨
P1 (완료) [정정]모임·커뮤니티, 채팅진행 중완료: community 본체(Entity/Repo/Service 19파일)·Room contextType 연동(CommunityChatIntegrationEventWorker Layer 1)·컨텍스트 방 이름(BE-14). 테스트 22모듈 내 도메인 추가✅ 커밋 eac1c925/a701840b/5ff07c7ccommunity를 Core 관계표 등록·ArchUnit R1 포함은 잔여 확인 필요(§10)
P2커뮤니티(모집)recruitment 신규 컨텍스트 (모집·신청·정원·취소 수수료)모듈 내 도메인 추가모집 FR 스펙 확정 시payment Customer 관계 등록, CancellationPolicy 전략 도입
P3운동시설, 상품시설상품(program: PT·클래스) + 시설 운영시간·자동 슬롯·휴무facility/booking 확장시설 예약 고도화 FR 시OperatingHours VO(facility 소유), 스케줄러 슬롯 생성
P4운동시설(예약 취소)CancellationPolicy 전략(체육관별 취소 정책)booking/facility 확장취소 수수료 기능 스펙 시refundAmount 외부주입 → 정책 계산 전환
P5상품, 주문통합 catalog / order-query 읽기 파사드Read 컨텍스트 추가교차 타입 검색·통합 주문 이력 화면 스펙 시dashboard 패턴 재사용, 쓰기 aggregate 불변
P6 (조건부·먼 미래)상품(티켓), 결제고부하 컨텍스트(ticketing/payment) 별도 배포·DB 분리서비스 분리 → DB 분리측정된 피크 부하가 단일 모놀리스 임계 초과(한정판 드롭 시 P95 지연 급증 관측)마케팅 고부하 PRD 실측 트리거. expand-contract로 무중단

무중단 전환 원칙: P1~P5는 모두 모놀리스 내 도메인 추가/확장 — 신규 패키지 추가 → import 갱신 → (필요 시) 구 경로 제거의 expand-contract 순서. P6만 물리 분리로, 반드시 실측 부하 트리거 후에만.

장기 도메인 진화 단계 다이어그램 (경계 변화, Mermaid LR)

flowchart LR
    subgraph P0["P0 현재 (모놀리스)"]
        core0["코어 12도메인 + community VO"]
    end
    subgraph P1P2["P1~P2 (도메인 추가)"]
        comm["community 본체"]
        recr["recruitment"]
    end
    subgraph P3P5["P3~P5 (확장·읽기모델)"]
        prog["시설 program·운영시간"]
        read["catalog·order-query 파사드"]
    end
    subgraph P6["P6 (조건부 물리분리)"]
        split["ticketing·payment 분리 배포"]
    end
    core0 -->|채팅 로드맵| P1P2
    P1P2 -->|기능 FR 트리거| P3P5
    P3P5 -.->|측정 부하 트리거만| P6

8. 이벤트 아키텍처 적합성 진단 (private-be-architecture-rule 기준)

§9의 “규칙 정합”과 별개로, 확장·재사용·통합 관점에서 이벤트 백본의 설계 적합성을 진단합니다.

(1) 토폴로지 실측 (파일 근거)

Layer 분기 근거: AbstractDomainEvent.topic이 non-null이면 Kafka(Layer 2), null이면 Spring(Layer 1) — RoutingDomainEventPublisher.ktevent.topic 유무로 분기(@Primary).

발행 이벤트Layer토픽/전달구독자 (파일)멱등 키
PaymentCompletedEvent2payment.completed.v1notification NotificationEventWorker.ktpaymentId
BookingConfirmedEvent2booking.confirmed.v1notification NotificationEventWorker.ktbookingId
TicketIssuedEvent2ticket.issued.v1notification NotificationEventWorker.ktticketOrderId
BookingRefundRequestedEvent1Spring AFTER_COMMITbooking BookingRefundEventWorker.kt
AlertProcessingRequestedEvent1Spring AFTER_COMMITalerting AlertProcessingEventWorker.kt
AlertDeliveryReadyEvent1Spring AFTER_COMMITnotification 경유 AlertDeliveryEventWorker.kt
NotificationDispatchRequestedEvent1Spring AFTER_COMMITnotification NotificationDispatchEventWorker.kt
McpAnomalyDetectedEvent1Spring AFTER_COMMIT (topic=null 명시)mcp McpAnomalyEventWorker.kt
LimitedDropOversoldEvent1Spring AFTER_COMMITgoods LimitedDropOversoldEventWorker.kt
FeatureFlagChangedEvent1Spring AFTER_COMMITfeatureflag FeatureFlagChangedEventWorker.kt

동기(이벤트 아님) 결합: booking/goods/ticketing → payment.createPending(app 오케스트레이션), payment → OrderConfirmationGateway.confirm/cancelwhen(OrderType) 콜백(OrderConfirmationGatewayImpl.kt).

flowchart LR
    subgraph Producers["코어 발행"]
        payment
        booking
        ticketing
        goods
    end
    subgraph L2["Layer 2 · Kafka .v1"]
        t1["payment.completed"]
        t2["booking.confirmed"]
        t3["ticket.issued"]
    end
    subgraph L1["Layer 1 · Spring AFTER_COMMIT"]
        refund["booking refund"]
        oversold["goods oversold"]
        anomaly["mcp anomaly"]
    end
    subgraph Consumers["구독"]
        notification
    end
    booking -->|topic| t1
    payment -->|topic| t1
    ticketing -->|topic| t3
    booking -->|topic| t2
    t1 --> notification
    t2 --> notification
    t3 --> notification
    booking -->|topic=null| refund
    goods -->|topic=null| oversold
    ticketing -->|anomaly| anomaly
    payment -.->|OrderConfirmationGateway when| booking

(2) 설계 적합성 판정 (4항목)

#진단판정근거
① 이벤트 백본이 얇은가Kafka 구독자가 notification 단일 도메인에 100% 쏠림 — 3개 토픽 전부 NotificationEventWorker만 소비. 사실상 “알림 전용 파이프”얇음 (재사용 여력만 있고 실사용 1곳)백본 자체는 건강(발행자-구독자 무결합, 구독자가 자체 DTO 보유). 단 재사용 폭이 아직 0 — recruitment/활동피드가 붙기 전까지 Kafka 도입 이득이 notification 하나에 그침
② 이벤트로 끊어야 할 결합이 동기로 남았나payment↔order가 양방향 동기 — 주문→createPending, payment→when(OrderType) 콜백. 상품/주문 타입 추가(시설상품 PT·클래스) 시 OrderType enum + confirm/cancel 2개 when 분기 + Gateway 생성자 DomainService 주입이 선형 증가확장 마찰 있으나 지금은 허용OrderConfirmationGatewayImpl.kt when 분기 2곳. 타입 3개(BOOKING/GOODS/TICKETING)까진 관리 가능. 4~5개 초과 시 마찰 가시화 → (3) 전환 트리거
③ Layer 1/2 판단이 적절한가payment→notification(무관 도메인)=Kafka ✅, 같은 컨텍스트 파생(booking refund·goods oversold·mcp anomaly)=Spring ✅, mcp는 topic=null 주석으로 의도 명시 ✅적절 (오분류 0건)무관 도메인을 Spring으로 묶거나 같은 컨텍스트를 근거 없이 Kafka로 쪼갠 사례 없음
④ 멱등·순서·최종일관성·스키마·재사용이 확장에 견디나eventId(UUID 기본)·occurredAt(UTC) 공통 필드. Consumer 멱등=NotificationDomainService.findByEventId dedup. 멱등 키는 비즈니스 키(paymentId) + push는 "$paymentId:push"로 분리 → 채널 충돌 없음. 스키마 .v1 3종, 파괴 변경은 새 버전 토픽 규칙견딤 (양호)순서: 토픽 key 설계는 계약 문서 확인 필요(현재 단일 소비자라 순서 리스크 낮음). 재사용: 구독자 자체 DTO(presentation/notification/worker/PaymentCompletedEvent.kt)라 producer 도메인 미import — 신규 구독자 추가가 안전

(3) payment↔order 동기 결합 전환 후보 + 트리거

후보설명판정
현행 동기 유지주문→createPending, payment→OrderConfirmationGateway 콜백채택(지금) — 모놀리스 단일 트랜잭션의 강일관성·단순함. 결제 확정이 즉시 주문 상태에 반영돼야 UX가 자연스러움. 이벤트화하면 최종일관성 창(주문이 잠시 PENDING) + 멱등·보상 트랜잭션 설계 부담
이벤트 기반 전환payment가 PaymentCompleted 발행 → 각 주문 컨텍스트가 구독해 자기 확정 처리 (when 분기 소멸)미채택(지금) / 전환 트리거 예약 — 조기 이벤트화는 오버엔지니어링

전환 트리거 (측정 가능 조건, OR):

  1. OrderType4개 초과로 늘어 when 분기·Gateway 의존 누적이 확장 마찰이 될 때 (시설상품·구독·번들 등 추가 시).
  2. 결제-주문 확정에 최종일관성이 허용 가능해질 때(주문 즉시확정 UX 요구 완화).
  3. 결제 워커를 별도 배포로 분리(§7 P6)해야 할 부하가 관측될 때.

전환 시 부담: 각 주문 컨텍스트가 PaymentCompleted를 구독해 멱등(eventId 기반) 확정 + 실패 시 보상(재고 복원·좌석 해제) + 주문 PENDING 노출 UX 처리. 이 부담이 when 분기 유지 비용보다 커지는 시점이 전환점입니다.

(4) 백본 확장 여지 — 장기 계획(§7) 연결

기존 .v1 이벤트를 신규 컨텍스트가 구독만 추가해 재사용(발행자 무변경):

신규 컨텍스트 (단계)재사용/구독할 이벤트연결
recruitment (P2)PaymentCompleted(취소 수수료 결제 확정) — 자체 구독자 DTO 추가payment 발행 무변경, Kafka 백본 재사용 2번째 사례
community 활동 피드 (P1 이후)booking.confirmed·ticket.issued·모집 완료 — 피드 항목 생성Kafka 구독자 추가로 백본 “알림 전용” 탈피 (①의 얇음 해소)
실시간 dashboard (P5)코어 .v1 3종 구독 → 사전 집계 읽기 모델 갱신CQRS 읽기 모델을 이벤트로 갱신(현 Conformist 폴링 대체)

→ ①의 “백본이 얇다”는 P1~P5에서 구독자가 늘며 자연 해소됩니다. 지금 선제로 토픽·구독자를 늘리지 않고, 신규 컨텍스트가 실제 착수될 때 구독자만 추가하는 것이 정답입니다.


9. 코드 컨벤션 정합 조사 (grep 실측)

private-be-code-convention 금지 패턴·레이어 규칙과 private-be-architecture-rule(Layer1/2 이벤트) 정합을 실측했습니다. 수정하지 않고 조사만 — 발견 시 /private-review로 연결하십시오.

ArchUnit이 커버하는 범위 (기존 R1~R4, 자동 회귀)

규칙대상현재 상태
R1domain.X → domain.Y 교차 import 금지0건 (정합) — 실측 `grep “import com.sportsapp.domain.” domain/
R2domain → infra/application/presentation 금지정합 (ArchUnit LayerDependencyRulesTest)
R3지원/서브시스템 → 코어 동기 금지 (dashboard·partner 화이트리스트)정합
R4common 공유 커널 순수성정합

ArchUnit이 커버 못 하는 컨벤션 (grep 실측 결과)

룰 ID실측 명령 결과판정근거
no-double-bang (!!)0건정합
no-jpa-query (@Query 비-락)0건정합QueryDSL CustomImpl 사용
no-lob (@Lob)0건정합TicketOrder.kt@Type(JsonStringType::class) 사용 확인
no-consumer-record (ConsumerRecord<String,String>)0건정합NotificationEventWorker가 DTO 직접 매핑
no-local-datetime (LocalDateTime/Instant/Clock)2건정합(허용 예외)JwtTokenProvider.kt:64(infra), JwtIssuer.kt:13(domain gateway) — JWT 만료는 Instant 외부 토큰 의미론. 도메인 상태 시간 아님. p3 수준 관찰 대상
no-optional-return (Optional<>)3건정합(프레임워크 SPI)SecurityAuditorAware.kt:12·MongoAuditorAware.kt:14(AuditorAware.getCurrentAuditorZonedDateTimeProvider.kt:19(DateTimeProvider.getNow) — 전부 Spring이 강제하는 SPI 시그니처, infra 레이어. 도메인 반환 아님
ApplicationEventPublisher/KafkaTemplate 직접 주입 (app/domain)1건정합DomainEventPublisher.kt:8 주석 언급만. 실제 주입은 infra(KafkaDomainEventPublisher·SpringDomainEventPublisher)로 은닉
no-junit (테스트 JUnit)4건부분 위반(p3 관찰)architecture/*RulesTest.kt 3건은 ArchUnit 관용상 JUnit @Test 사용(ArchUnit 표준 러너), HealthEndpointIntegrationTest.kt 1건. 신규 비즈니스 테스트 아님 — Kotest 전환 여지이나 강한 위반 아님

[정정 추가] community 컨텍스트 컨벤션 정합 (fresh tree 실측)

초판이 놓친 community 본체를 컨벤션 기준으로 실측했습니다.

항목상태근거
R1 도메인 교차 참조정합grep "import com.sportsapp.domain." domain/community/(common·community 제외) = 0. 채팅 연동도 이벤트+ID로만(CommunityChatIntegrationEventWorker는 presentation→application.message)
Rich Domain Model정합Community.kt/CommunityMember.kt private 생성자 + create/reconstitute 팩토리, private var 필드, 상태 전이 캡슐화(MembershipStatus.canTransitTo), 시간 인자 미전달(approve() 내부 ZonedDateTime.now())
UseCase 규칙정합UseCase 10개가 CommunityDomainService만 호출(Repository 직접 주입 없음)
이벤트 Layer 판단정합community→채팅(같은 앱 연관 컨텍스트)=Layer 1 Spring @TransactionalEventListener(AFTER_COMMIT) + @Async ✅. 무관 도메인 Kafka 오분류 없음
거버넌스 등록미이행(p2 관찰)DomainClassification.core(SupportToCoreDependencyRulesTest.kt:18)에 community 미등록 — FR-8 위반. R1/R2 컨벤션은 자동 커버하나 분류 상수·context-map 갱신 누락(§10 즉시착수 2)

이벤트 아키텍처 정합 (Layer 1/2 판단)

항목상태
Layer 2 (Kafka, 무관 도메인)정합 — KafkaDomainEventPublisher(발행)·NotificationEventWorker(소비, presentation). payment/booking/ticketing → notification 비동기 단방향
Layer 1 (Spring ApplicationEvent, 같은 컨텍스트)정합 — SpringDomainEventPublisherDomainEventPublisher interface 뒤로 은닉. RoutingDomainEventPublishertopic 유무로 Layer 1/2 분기
발행자-구독자 무결합정합 — 서비스가 DomainEventPublisher interface로만 발행, KafkaTemplate·ApplicationEventPublisher 직접 주입 없음

컨벤션 조사 결론

심각(p0~p2) 위반 0건. 발견은 전부 p3 수준 관찰 대상 2종:

  1. JwtIssuer/JwtTokenProviderInstant — 외부 토큰 의미론이라 허용 판단 (도메인 시간 아님).
  2. 테스트 4건 JUnit @Test — ArchUnit 관용 3건 + 헬스 1건, 신규 비즈니스 로직 테스트 아님.

→ 코드베이스는 private-be-code-convention·private-be-architecture-rule실질 정합. 상기 p3 2종의 처리 판단은 /private-review로 넘깁니다(이 문서는 조사까지).


10. 지금 할 일 vs 나중에

즉시 착수 [정정 — community 본체는 이미 완료, 실제 남은 항목으로 재작성]

초판 1. community 컨텍스트 본체 설계완료 (eac1c925/a701840b/5ff07c7c). 아래는 실측으로 확인한 실제 잔여 항목입니다.

  1. docs/domain-context-map.md stale 정정 (최우선 — 이번 오판의 근본 원인)docs/domain-context-map.md:20이 여전히 community = “(VO만) … 커뮤니티 (구축 중)”, :225 “아직 VO만 존재하는 미완성 컨텍스트”로 기술. 이 문서가 SSOT 입력으로 쓰여 직전 진단(및 cold 재검증)을 오도했습니다. 완성 컨텍스트(Entity 2·Service·Repo·이벤트·워커)로 갱신해야 문서-코드 정합. (senior-be 또는 doc-sync)
  2. DomainClassification에 community 분류 등록 (FR-8 미이행 시정)SupportToCoreDependencyRulesTest.kt:18 core = listOf("booking","facility","goods","payment","ticketing","user","post","message")community 없음. FR-8(“신규 도메인 추가 시 이 목록에 분류를 반영”)을 지키지 못한 상태. R1(교차참조)·R2(레이어)는 slices().matching("com.sportsapp.domain.(*)..")·domain.. 컨벤션 기반이라 community를 이미 자동 강제(교차 import 실측 0건)하나, R3 분류·화이트리스트 상수에서 누락 → 지원 도메인이 community를 동기 의존해도 R3가 못 잡음. core 리스트에 community 추가. (private-review 또는 senior-be)
    • 부수: domain-context-map.md:203은 Community&Chat(message/community/post)을 Supporting으로 분류하나 DomainClassification.core는 message·post를 core로 둠 → 두 문서 분류 불일치도 함께 정합.
  3. 설계 원칙 문서화 (초판 3번 유지) — ① 상품 통합 aggregate 금지(결제 OrderType이 통합축) ② 주문 통합 쓰기 aggregate 금지 ③ 모집은 post 아닌 recruitment 소유 ④ 취소 정책은 전략 객체(facility 소유), refundAmount 외부주입 지양 ⑤ b2c/b2b는 롤+Partner 유지 ⑥ 모임·커뮤니티는 community 단일 컨텍스트 + Visibility 겸용 유지(조기 2분할 금지, 이미 구현됨).
  4. 재발 방지 — 진단 파이프라인에 트리 신선도 게이트 선행 — 이번 오판은 cold 재검증(§1-1)조차 같은 stale 트리를 봐서 걸러내지 못함. 도메인 경계 진단 시작 전 git rev-list --left-right --count HEAD...origin/dev == 0/0 확인을 의무 선행 단계로 고정(이 재진단 문서 상단 게이트가 그 예시).

트리거 도달 시 (지금은 미착수 — 오버엔지니어링 경계)

  • community 콘텐츠·예약 연동 — 모임 게시글(community↔post ID참조)·소모임 예약(community↔booking)·종목별 게시글. community 본체는 있으나 이 3개 연동이 부분일치의 잔여 갭 (모임/커뮤니티 게시판·예약 FR 확정 시)
  • recruitment 컨텍스트 (모집 FR 확정 시) + CancellationPolicy 전략 (취소 수수료 스펙 시). recruitment는 community를 ID 참조
  • 시설상품 program + 시설 운영시간/자동 슬롯/휴무 (시설 예약 고도화 시)
  • 통합 catalog/order-query 읽기 파사드 (교차 조회 화면 스펙 시)
  • ticketing/payment 물리 분리 (측정된 피크 부하 임계 초과 시에만)
  • CommunityRole 중간 계층(MANAGER) — 사용자 정의 “중간 계층 확장 가능” 실제 위임 요구 발생 시 enum 확장(저비용)

명시적 미채택 (지금 규모에 과함)

  • 단일 Product 통합 aggregate / 단일 Order 통합 aggregate → 라이프사이클 이질, God-aggregate 위험
  • 모임·커뮤니티 2개 컨텍스트 분리 → Visibility로 겸용 가능
  • CommunityRole 중간 계층(MANAGER) 선제 도입 → YAGNI, enum 확장은 저비용이라 필요 시
  • b2c/b2b 계정 타입 1급 분리 → 유저 다중 역할성 훼손

Document History

날짜변경 내용
2026-07-06최초 작성 — 사용자 정의 vs 코드 도메인 갭 매트릭스, 상품·주문 통합 판정, 모임/모집 신규 컨텍스트 제안, 취소정책 확장 원칙, 장기 진화 6단계
2026-07-06보완 — §2 트래픽↔구조 3단계 verdict(컨텍스트별), §3 인프라 패턴 판정(캐싱·큐 도입됨/LB·게이트웨이·BFF·MSA 불필요+트리거), §6 사용자 정의 도메인 앵커 재정렬(매핑표+TO-BE 맵), §7 진화 계획 사용자 도메인 앵커+단계 다이어그램, §9 코드 컨벤션 grep 실측(심각 위반 0, p3 2종)
2026-07-06보완 2 — §8 이벤트 아키텍처 적합성 진단 신설(토폴로지 실측·흐름 다이어그램, 4항목 설계 판정, payment↔order 전환 트리거, 백본 확장 여지 §7 연결). 후속 섹션 재번호(코드 컨벤션 §9, 지금 할 일 §10)
2026-07-06§1 매트릭스 전면 재작성 — 사용자 확정 계층(상품 ⊃ 중고·브랜드·한정판·티켓·시설상품)을 SSOT로, “개념 존재”가 아닌 “경계·계층 일치”로 재판정. 티켓 일치경계불일치(ticketing↔goods import 0건), “상품 상위 개념” 누락 별도 행 추가, 각 행 근거 2종(존재 위치+경계 판단) 파일:라인 부착. 범례에 경계불일치 추가, 재집계(일치4/부분6/경계불일치1/누락6/코드전용1)
2026-07-06§1-1 Cold 재검증 추가 — 답 미노출 독립 스폰이 11개 load-bearing 판정을 코드로 재확인. 티켓 경계불일치·한정판 일치·상품상위/시설상품/시설운영시간/모집/취소정책/모임 누락·OrderType 3분기 전부 재현. “존재=일치” 회귀 없음 검증
2026-07-06stale 트리 재진단·정정 — 초판·1차 cold 재검증이 origin/dev보다 뒤처진 stale 로컬 트리에서 수행돼 community를 “VO 4개 orphan/미구축”으로 오판정한 것을 정정. 트리 신선도 게이트 선행 실행(HEAD=5ff07c7c == origin/dev, rev-list 0/0, backend clean). 실측 재확인: community는 완성 컨텍스트(Entity 2·DomainService 12메서드·UseCase 10·Repository 3·이벤트 3·예외 6·VO 4·테스트 22, CommunityChatIntegrationEventWorker Layer 1 채팅 연동). 정정: §정정 고지 배너 신설, §AS-IS community 행·실측 3번, §1 매트릭스 row 14(모임 누락→부분일치)·15·17, 재집계(부분일치 6→7·누락 6→5), §1-1 모임 행 판정 철회, §5·§7 P1(완료)·§9(community 컨벤션 정합 표 추가)·§10(즉시착수 재작성: domain-context-map.md stale 정정·DomainClassification.core 등록·신선도 게이트 의무화). community 외 도메인(티켓/상품상위/모집/시설운영시간/취소정책 등)은 fresh tree에서 전부 재현돼 판정 유지