백엔드 MSA 분리 판단 아키텍처 진단 문서

Background / 진단 범위

  • 요구사항 원문: “backend를 msa로 분리하려고 하는데, 도메인 바운디드 컨텍스트가 잘 나뉘어져 있는지 아키텍처를 어떻게 구성할 것인지부터 시작해줘”
  • 진단 목적: MSA 분리가 지금 옳은 결정인가를 코드 실측으로 판정하고, 옳지 않다면 무엇이 옳은지 후보군으로 제시한다. MSA 결론을 미리 정해두고 정당화하지 않는다.
  • 대상 트리: /Users/biuea/sports-app-arch-msa (읽기 전용 worktree)
  • 참고 선행 산출물:
    • 아키텍트/20260708-전체시스템-architecture.md
    • 아키텍트/20260706-사용자정의-도메인경계-갭분석-architecture.md
    • 도메인 경계 재설계/{PRD,TDD}.md + ADR/ 5건 (ADR-001 5계층 분류 / ADR-002 관계·의존 방향 / ADR-003 제공 UseCase 경계 / ADR-004 operator·mcp 격리 / ADR-005 ArchUnit fitness function)
    • 레포 docs/domain-context-map.md, requirement.md, domain-reference.md

트리 신선도 게이트 (진단 기준 커밋)

항목
HEADf18434c3dfda050f3a4aae2513f11425ad853ce4
origin/main...HEAD (ahead/behind)0 / 0
워킹트리clean
origin/main...origin/dev159 / 0 — dev는 main에 완전히 포함되며 159커밋 뒤. dev 기준 판단 금지 확인

과거 stale 트리 기반 orphan 오판 이력이 있어 게이트를 진단 앞단에 두었다.

⚠️ 브리핑 사실 정정 2건 (실측 결과)

브리핑 내용실측 결과근거
”layer→context→sub 패키지 재배치는 dev에만 머지됐고 main에는 미반영으로 보인다 → 설계 의도와 트렁크 현실의 괴리”오류. main에 반영돼 있다. git merge-base --is-ancestor e74c8a3d9 f18434c3 = YES (PR #193 PKG-2가 main 조상). 실제 트리도 application/ticketing/{dto,usecase}/처럼 서브패키지가 적용돼 있다b2c6e8f41 diff = application/ticketing/{ => dto}/CreateMyEventCommand.kt
”패키지는 layer-first”맞다. 단 PKG-2는 layer-first를 바꾸는 작업이 아니었다. PKG-2는 layer/context/*.ktlayer/context/{dto,usecase,...}/*.kt컨텍스트 하위에 sub 그룹핑을 추가한 것이고, 최상위 layer-first는 처음부터 의도된 유지 대상이었다com/sportsapp/{application,domain,infrastructure,presentation} 최상위 4개

“설계 의도와 트렁크 현실의 괴리”는 존재하지 않는다. 패키지 구조는 설계대로다. 실제 괴리는 다른 곳(문서 stale·FR-8 분류 누락)에 있고 §7에 기록했다.


1. AS-IS 구조 조사

1-1. 배포 형태·모듈 (실측)

항목실측값근거
Gradle 모듈단일 모듈 (include 선언 0건)backend/settings.gradle.kts — rootProject만
배포 단위단일 컨테이너 backend (API + Kafka Consumer + 스케줄러 + WebSocket 통합)docker-compose.yml·.dev.yml·.prod.yml 모두 backend 서비스 1개
수평 확장 자산nginx LB + backend replicadocker-compose.lb.yml (backend + nginx-lb)
패키징 전략layer-first → context → sub (com/sportsapp/{layer}/{context}/{dto,usecase,...})최상위 4 layer, PKG-2 적용 완료
main Kotlin 파일1,280개find src/main/kotlin -name "*.kt"
test Kotlin 파일737개find src/test -name "*.kt"
REST Controller52개*ApiController.kt
EventWorker16개presentation/**/*EventWorker.kt
Flyway 마이그레이션59개 / 테이블 45개src/main/resources/db/migration/

폴리글랏 영속화: MySQL 8.0(주 관계형) / MongoDB 7.0(facility 문서) / Redis 7(락·재고 게이트·좌석 락·대기열·JWT 블랙리스트, 13개 컴포넌트가 사용) / Kafka 3.7 KRaft 단일 브로커 / MinIO(S3 호환).

관측 스택 보유 (docker-compose.observability.yml): otel-collector · prometheus · tempo(분산 추적) · loki · promtail · grafana. → MSA 선행 인프라 중 분산 추적은 이미 있다.

1-2. 컨텍스트 인벤토리 + 코드 규모 (4개 레이어 합산 실측)

domain 패키지 기준 20개 컨텍스트 + common 공유 커널, 그 외 application 전용 4개(catalog·order·dashboard·image).

컨텍스트파일라인ADR-001 분류비고
goods1504,873코어Product·Cart·GoodsOrder·LimitedDrop·Stock. 최대
mcp1033,857서브시스템AI 에이전트 운영
message1023,087코어Room·Message, STOMP 실시간
facility1003,007코어Facility(Mongo)·Program·OperatingHours·Holiday
booking912,668코어Slot·Booking
ticketing872,476코어Event·Seat·Ticket·TicketOrder
user621,268코어User·Role·RBAC
recruitment611,841코어Recruitment·Application (신규)
notification591,573지원
featureflag581,815미분류
community541,813코어Community·CommunityMember
partner471,421미분류B2B API 신원
virtualqueue371,615미분류가상 대기열 (신규)
post371,157코어
payment371,419코어공용 컨텍스트
alerting361,262지원
common31656공유 커널
operator16487지원
airquality14616미분류
weather10372지원
featuredemo7148미분류
dashboard / catalog / order / image7 / 7 / 6 / 5267 / 353 / 263 / 93조회·유틸application 전용, domain 레이어 없음

규모 판정: 최대 컨텍스트(goods)가 4,873라인이다. MSA 서비스 1개의 통상 하한(수만 라인)에 한참 못 미친다 — 20개로 쪼개면 평균 1,600라인짜리 서비스 20개가 된다.

1-3. 선행 문서 대비 해소된 갭 (20260706·20260708 → 현재)

이전 진단의 주요 지적이 상당수 해소됐다. 판정을 갱신한다.

이전 지적현재 상태근거
상품 ⊃ 중고/브랜드 구분 불가해소SellerType{B2C,B2B} 판별 필드 도입, 인증 채널로 등록 시 1회 자동 결정domain/goods/vo/SellerType.kt, domain/goods/entity/Product.kt:52
시설상품(PT·클래스) 누락해소Program aggregate 신설domain/facility/entity/Program.kt:23
모집(recruitment) 완전 누락해소 — 독립 코어 컨텍스트 (61파일)domain/recruitment/
시설 운영시간·휴무 누락해소OperatingHours·Holiday VOdomain/facility/vo/{OperatingHours,Holiday}.kt
OrderType가 payment 소유(역방향)해소domain.common.order로 공유 커널 승격, 4값(BOOKING/TICKETING/GOODS/RECRUITMENT)domain/common/order/OrderType.kt
상품·주문 상위 개념 부재부분 해소catalog·order 읽기 조합 파사드 도입(쓰기 상위 aggregate는 의도적 미도입)application/catalog/CatalogCompositionService.kt, application/order/OrderCompositionService.kt
DomainClassification.core에 community 미등록해소 — community·recruitment 등록됨SupportToCoreDependencyRulesTest.kt:19
마케팅 폭주 흡수 계층 부재해소virtualqueue 컨텍스트 신설(37파일)domain/virtualqueue/

1-4. AS-IS 컴포넌트 다이어그램

flowchart LR
    subgraph Ingress["인그레스 (단일 배포단위)"]
        Nginx[nginx LB]
        VQ[virtualqueue 입장토큰]
        LS[LoadSheddingFilter]
    end
    subgraph App["Spring 모놀리스 backend"]
        Ctrl[52 Controller]
        UC[UseCase 계층]
        Facade[catalog·order 조합파사드]
        DS[20 컨텍스트 DomainService]
    end
    subgraph Stores["단일 공유 저장소"]
        MySQL[(MySQL 45테이블)]
        Mongo[(MongoDB facility)]
        Redis[(Redis 락·큐)]
    end
    subgraph Bus["이벤트 백본"]
        Router[RoutingDomainEventPublisher]
        Kafka[Kafka 3토픽]
    end
    Nginx --> VQ
    VQ --> LS
    LS --> Ctrl
    Ctrl --> UC
    Ctrl --> Facade
    Facade --> DS
    UC --> DS
    DS --> MySQL
    DS --> Mongo
    DS --> Redis
    DS --> Router
    Router --> Kafka
    Kafka --> UC

1-5. 병목·안티패턴 (파일 근거)

#항목판정근거
B1단일 배포 단위에 20 컨텍스트 + 52 컨트롤러 — b2c/마케팅/b2b/운영(mcp)이 같은 힙·스레드풀·커넥션풀 공유임계점 근접SportsApplication.kt 단일 엔트리, compose backend 1서비스
B2infrastructure Gateway 구현이 타 컨텍스트 Repository를 직접 주입 — 데이터 레벨 결합 9건/5쌍MSA 하드 블로커§2-2 표 (FacilityScheduleGatewayImpl.kt:6-9 등)
B3domain 엔티티가 곧 JPA 엔티티 — domain 42파일이 jakarta.persistence import. 스키마 변경이 도메인 모델에 직결분리 시 마찰domain/goods/entity/Product.kt @Entity, grep -rl "import jakarta.persistence" domain/ = 42
B4공유 커널이 물리 테이블을 소유domain/common/Permission.kt@Table(name="permissions"), user의 role_permissions가 참조DB 분리 시 충돌점domain/common/Permission.kt:11
B5FR-8 분류 누락 5건 — featureflag·partner·virtualqueue·airquality·featuredemoDomainClassification 어디에도 없음 → R3 규칙이 이들을 못 잡음거버넌스 구멍SupportToCoreDependencyRulesTest.kt:19-21 (core 10 + support 4 + subsystem 1 = 15 / 실제 20)
B6docs/domain-context-map.md stale — virtualqueue 0회·catalog 0회 언급, recruitment↔payment/community 연동을 “BE-55·BE-60 예정”으로 기술하나 코드는 배선 완료문서-코드 괴리맵 최종 수정 1fbf8b4f6(7/7) 이후 virtualqueue·catalog·order 유입

순환 의존 0건, 컨텍스트 교차 JOIN 0건, 네이티브 SQL 0건(유일 2건은 Spring Batch 메타데이터 초기화). 구조 위생은 매우 좋다.


2. 컨텍스트 간 결합 실측표

2-1. 결합 축별 총량

건수판정
① domain 레이어 교차 import (ArchUnit R1)0건정합 — 도메인 모델은 완전 격리
② application 레이어 교차 참조22쌍 / 48 import설계상 허용(ADR-002 rule #2)
③ infrastructure 교차 참조 (타 컨텍스트 Repository 직접 주입)5쌍 / 9 importMSA 블로커
④ 공유 테이블 / 교차 JOIN0건 (교차 JOIN), 단 공유 커널 테이블 1건(permissions)대체로 정합
⑤ Kafka 이벤트3토픽 / 7구독 그룹정합
⑥ 공유 커널 의존 (domain.common)전 컨텍스트ErrorStatus/BusinessException 119회 등

2-2. ③ infrastructure 데이터 레벨 결합 — 전수 (MSA 하드 블로커)

Gateway interface는 소비자 도메인에 정의(ACL 정합)되나, 구현체가 공급자의 Repository를 직접 주입해 남의 테이블을 읽는다. import는 domain interface를 향하므로 R1은 통과하지만, 스키마 결합은 그대로다.

소비자 → 공급자파일:라인주입 대상분리 시 필요 조치
booking → facilityinfrastructure/booking/gateway/FacilityOwnershipGatewayImpl.kt:6FacilityRepository원격 조회 or 소유권 읽기모델
booking → facilityinfrastructure/booking/gateway/FacilityScheduleGatewayImpl.kt:6,7,8,9FacilityRepository + Facility + OperatingHours + TimeRange원격 조회 or 스케줄 읽기모델 복제
community → bookinginfrastructure/community/gateway/SlotInfoGatewayImpl.kt:3SlotRepository원격 조회
facility → bookinginfrastructure/facility/gateway/SlotQueryGatewayImpl.kt:3SlotRepository원격 조회
message → goodsinfrastructure/message/gateway/GoodsProductGatewayImpl.kt:4ProductRepository원격 조회
notification → userinfrastructure/notification/gateway/RecipientContactResolver.kt:3user 도메인 타입이벤트 payload에 연락처 동봉 or 원격 조회

이 6개 파일이 MSA 분리 시 가장 먼저 깨진다. Gradle 멀티모듈로 쪼개는 순간 컴파일 에러가 난다(그래서 멀티모듈이 유용한 진단 도구다).

2-3. ② application 레이어 교차 참조 — 쌍별 (import 건수)

소비자 → 공급자건수성격
community → message6채팅방 조회(RoomContextQueryService)
booking → payment6결제 개시(PaymentDomainService·PgInitiateCommand)
post → community5멤버십 인가(requireActiveMember)
goods → payment4결제 개시
dashboard → ticketing / goods / booking / user / facility4/3/3/1/1읽기 조합 (R3 화이트리스트)
catalog → goods / ticketing / recruitment / facility4/2/2/2읽기 조합 파사드
ticketing → payment3결제 개시
recruitment → payment3결제 개시
order → ticketing / recruitment / goods / booking2/2/2/2읽기 조합 파사드
recruitment → community2커뮤니티 조회
facility → booking2Program 회차 = Slot 생성
partner → user1연동 User 프로비저닝 (R3 화이트리스트)

주목: catalog·order 파사드는 4개 DomainService를 병렬 fan-out + 도메인당 300ms 타임아웃 + 실패 도메인 부분 저하(failedDomains) 로 조합한다 — 이미 MSA의 API Composition 패턴 형태다.

근거: application/catalog/CatalogCompositionService.kt:28(DOMAIN_TIMEOUT_MILLIS = 300L), application/order/OrderCompositionService.kt:26

2-4. ⑤ Kafka 이벤트 토폴로지 (Layer 2)

토픽발행구독 groupId근거
event.payment.payment.v1paymentbooking-payment / goods-payment / ticketing-payment / recruitment-payment / notification-payment (5 팬아웃)presentation/{booking,goods,ticketing,recruitment}/worker/*PaymentEventWorker.kt:21-22, notification/worker/NotificationEventWorker.kt:27-28
event.booking.booking.v1bookingnotification-bookingNotificationEventWorker.kt:44-45
event.ticketing.ticket.v1ticketingnotification-ticketingNotificationEventWorker.kt:60-61

공용 컨텍스트 역참조 0건 — payment는 발행만 하고 주문 컨텍스트를 모른다. when(orderType) 동기 디스패치 허브 없음. private-be-architecture-rule의 핵심 규칙을 준수한다.

2-5. 미확인 항목 (추측하지 않음)

항목상태
실측 트래픽(RPS·P95·동시접속)미확인requirement.md는 목표치만 제시, 측정 아티팩트 없음
컨텍스트별 실제 부하 분포미확인docker-compose.sim.yml(k6) 자산은 있으나 결과 미보유
컨텍스트별 DB 테이블 크기·증가율미확인

3. 바운디드 컨텍스트 적합성 판정

3-1. 컨텍스트별 판정

판정 기준: 응집도(자기 데이터·규칙을 스스로 소유) / 소유 데이터(테이블 독점) / 독립 배포 가능성(교차 데이터 접근 없음).

컨텍스트응집도소유 데이터독립 배포판정
payment높음 — 발행만, 주문 타입 모름payments 독점가능PASS (모범)
virtualqueue높음 — EntryTokenGuard(common)로만 노출, 역참조 0Redis 전용가능PASS (모범)
user높음users·roles·user_roles·role_permissions조건부 — permissions가 공유 커널 소유(B4)PASS (조건부)
goods높음products·carts·cart_items·goods_orders·goods_order_items·stocks·limited_drops조건부 — message가 ProductRepository 역방향 접근PASS (조건부)
ticketing높음events·seats·tickets·ticket_orders가능PASS
recruitment높음 — 자체 취소수수료 정책 소유recruitments·applications가능PASS
community높음communities·community_members·community_bookings가능PASS
message높음rooms·messages·room_participants·room_invitations조건부 — goods 테이블 직접 읽기PASS (조건부)
post중간 — 인가를 community에 의존posts·comments조건부PASS (조건부)
mcp / alerting / notification / operator / featureflag높음 — 코어 역참조 0각자 독점가능PASS
facility ↔ booking낮음 — 양방향 데이터 접근facility=Mongo+regions, booking=slots·bookings불가재경계 필요
partner ↔ user낮음 — 단일 트랜잭션 쓰기 결합partner 3테이블조건부재경계 검토
catalog / order / dashboard / image해당 없음 — Aggregate 없는 읽기 조합없음가능(무상태)PASS (파사드)

3-2. 재경계 필요 — facility ↔ booking (유일한 진짜 문제)

양방향 데이터 접근이 존재한다:

  • facility → booking: SlotQueryGatewayImpl.kt:3SlotRepository 주입
  • booking → facility: FacilityScheduleGatewayImpl.kt:6-9FacilityRepository + Facility 엔티티 + OperatingHours VO 주입
  • facility → booking (application): CreateProgramSessionUseCase@Transactional로 Program 읽기 + Slot 쓰기

라이프사이클도 얽혀 있다 — 시설의 운영시간·휴무가 슬롯 생성 규칙을 결정하고, Program(시설상품) 회차가 곧 Slot이다.

현재 경계제안 경계근거
facility(시설·Program·운영시간, Mongo) / booking(Slot·Booking, MySQL) 별도하나의 배포 단위 facility-booking으로 묶는다 (내부 모듈은 유지)변경 주기 동일(시설 운영 정책 변경 → 슬롯 규칙 변경), 양방향 데이터 접근, 단일 트랜잭션 쓰기(CreateProgramSessionUseCase)
domain/common/Permission.ktpermissions 테이블 소유user로 이관 — 공유 커널은 물리 테이블을 소유하지 않는다role_permissions(user 소유)가 유일 소비자. 공유 커널은 계약만
featureflag·partner·virtualqueue·airquality·featuredemo 미분류ADR-001 5계층에 편입 (제안: featureflag·virtualqueue=플랫폼/지원, partner=지원, airquality·featuredemo=지원)FR-8 절차 미이행 → R3가 이들의 코어 역참조를 못 잡는다

3-3. 분리 시 트랜잭션이 쪼개지는 지점 (전수)

#지점현재분리 시난이도
T1partner/usecase/CreatePartnerUseCase.kt@Transactional 안에서 user 쓰기(register + assignRole×2) + partner 쓰기(createPartner)2컨텍스트 쓰기 → SAGA 또는 보상 트랜잭션 필수높음
T2facility/usecase/CreateProgramSessionUseCase.kt@Transactional 안에서 facility 읽기 + booking 쓰기원격 읽기 + 로컬 쓰기 (읽기 실패 시 중단)중간
T3post/usecase/CreateCommunityPostUseCase.kt@Transactional 안에서 community 인가 읽기 + post 쓰기원격 인가 조회 + 로컬 쓰기낮음
T4post/usecase/AddCommentUseCase.kt:22동일 (requireActiveMember 인가 읽기)동일낮음
T5결제 개시 4종 — CreateBookingUseCase · CreateGoodsOrderUseCase · PurchaseTicketsUseCase · ApplyRecruitmentUseCase이미 클래스 레벨 @Transactional 없음 (PG 네트워크 호출을 트랜잭션에 감싸지 않으려는 의도적 설계)추가 작업 없음 — 이미 분리 완료없음
T6결제 확정 (payment → 주문 4종)이미 Kafka 이벤트 + 멱등 구독추가 작업 없음 — 이미 최종일관성없음
T7Gateway 6개 구현 (§2-2)로컬 Repository 호출원격 호출 or 읽기모델 복제중간

핵심: 돈이 걸린 경로(결제 개시·확정)는 이미 트랜잭션이 쪼개져 있고 이벤트 기반 최종일관성으로 동작한다(T5·T6). MSA 전환에서 가장 어려운 부분이 이미 끝나 있다. 남은 진짜 SAGA 대상은 T1 단 1건이다.

3-4. 사용자 정의 경계 정합 매트릭스 (존재 ≠ 경계정합)

requirement.md·domain-reference.md가 정의한 계층(상품 ⊃ 중고·브랜드·티켓·한정판·시설상품, 주문 = 상위 단일 개념)이 SSOT다. “그 이름의 패키지가 존재한다”로 판정하지 않고, 소유 관계(테이블 소유·동기 호출 방향·이벤트 흐름) 를 대조한다.

사용자 정의① 존재 위치 (파일:라인)② 경계 판단 근거판정
상품 (상위 개념)application/catalog/dto/CatalogItem.kt:13 + CatalogItemType(PRODUCT/LIMITED_DROP/TICKET/PROGRAM/RECRUITMENT)상위 개념이 application 읽기 파사드로만 존재. grep -rn "SellableItem|abstract class Product" domain/ = 0 → 쓰기·소유 경계로서의 상위 타입은 여전히 없다. 5개 하위가 3개 독립 컨텍스트(goods·ticketing·facility·recruitment)에 분산 소유부분일치 (읽기만 통합)
상품 ⊃ 중고(b2c)domain/goods/vo/SellerType.kt(B2C), domain/goods/entity/Product.kt:52goods(=상품 그룹핑) 내부 배치 + 판별 필드 존재. 이전 “구분 불가” 해소일치
상품 ⊃ 브랜드(b2b)동일 SellerType.B2B, Product.kt:52동일. 인증 채널로 등록 시 자동 결정, 이후 불변일치
상품 ⊃ 한정판domain/goods/entity/LimitedDrop.kt (productId 참조)goods 내부 회차 aggregate. 사용자 계층과 동일 그룹핑일치
상품 ⊃ 티켓domain/ticketing/entity/{Event,Ticket,Seat,TicketOrder}.kt독립 최상위 패키지grep "import com.sportsapp.domain.goods" domain/ticketing/ = 0, 역방향 = 0. 자체 테이블 4개(events·seats·tickets·ticket_orders) 독점 소유. 사용자 계층은 상품⊃티켓인데 코드는 goods와 형제 독립 컨텍스트경계 불일치 (계층 이탈)
상품 ⊃ 시설상품(PT·클래스)domain/facility/entity/Program.kt:23 (facilityId·price·capacity)신설됐으나 facility 산하에 배치. goods(=상품) 그룹핑 밖. 회차는 booking의 Slot이 소유 → 상품 하위가 아니라 facility 하위경계 불일치 (계층 이탈)
주문 (상위 개념)application/order/OrderCompositionService.kt:34 + domain/common/order/OrderType.kt(4값)OrderType가 공유 커널로 승격돼 통합축은 확보. 그러나 쓰기 주문 aggregate는 GoodsOrder·TicketOrder·Booking·Application 4분할 유지. 상위는 읽기 조합 파사드뿐부분일치 (통합축 확보, 쓰기 분산)
결제domain/payment/entity/Payment.kt, event.payment.payment.v1 발행단일 컨텍스트 1:1, 주문 컨텍스트 역참조 0건일치
운동시설domain/facility/entity/Facility.kt + OperatingHours·Holiday등록·소유자·운영시간을 facility가 소유일치
시설 타임슬롯 예약domain/booking/entity/{Slot,Booking}.kt예약 라이프사이클을 booking이 소유. 단 facility와 양방향 데이터 접근(§3-2)부분일치 (경계 누수)
모임 (모임장·유저 계층)domain/community/entity/{Community,CommunityMember}.kt, CommunityRole(HOST/MEMBER), Visibility.PRIVATE멤버십·계층·전용 채팅 존재. 명칭만 사용자 “모임”≠코드 community부분일치 (명칭 불일치)
커뮤니티 (종목별 게시·모집)community(PUBLIC·SportCategory) + post + recruitment3개 컨텍스트에 분산되나 각 연동이 배선 완료(post→community 인가, recruitment→community 참조)부분일치 (컨텍스트 분산)
유저 ⊃ b2c/b2bdomain/user/entity/User.kt + UserRoleName + domain/partner/entity/Partner.kt계정 타입이 아닌 RBAC 롤 + Partner 신원으로 표현. 한 유저의 다중 역할을 지원하므로 의도적 설계부분일치 (의도적)
채팅domain/message/ + RoomContextType(COMMUNITY/GOODS_PRODUCT)범용 컨텍스트 방 메커니즘을 community·goods가 재사용일치

집계: 일치 6 / 부분일치 6 / 경계 불일치 2 (티켓·시설상품).

해석 — 경계 불일치 2건은 “고쳐야 할 결함”이 아니다. 20260706 §4 문제A에서 이미 판정했듯, 상품 5종은 라이프사이클이 이질적이다(중고=단건 C2C / 브랜드=SKU 재고 / 티켓=좌석재고+공연일 / 한정판=시간게이트 / 시설상품=슬롯·정원). 단일 aggregate로 통합하면 God-aggregate가 된다. 사용자 멘탈 모델의 “상품”은 읽기(카탈로그) 차원의 개념이고, 코드는 이를 catalog 파사드로 정확히 충족한다. 이 불일치는 유지가 옳고, 문서에 “의도된 불일치”로 못박는 것이 조치다.


4. MSA 분리 후보군 비교

4-0. 운영 현실 전제 (비용 판단의 기준) — 확정 제약 반영

항목
운영 인원1인
배포 환경로컬 docker compose 단일 호스트 (확정 제약 — k8s·클라우드 유료 인프라 도입 없음)
호스트 스펙 (실측)10코어 / Docker VM 가용 메모리 15.6GiB
현재 컨테이너 수13개 (backend + MySQL + Mongo + Redis + Kafka + MinIO + mock 4종 + mailhog 등)
JVM 1개 실사용 메모리 (실측)~950MB (Xmx 768m, mem_limit 1.8g — 2.56g에서 OOM(137) 관측 후 하향)
관측 스택otel + tempo(추적) + prometheus + loki + grafana — 보유
게이트웨이nginx LB만 (docker-compose.lb.yml) — 인증·라우팅은 앱 내 Spring Security
서비스 디스커버리없음 (compose DNS로 대체 가능)
실측 트래픽있음상시 트래픽 시뮬레이터/실측-리포트.md (2026-07-06). §7-1 참조

근거: 상시 트래픽 시뮬레이터/실측-리포트.md “측정 환경”·“부록”·“부록 2 메모리 안정화”

4-0-1. 단일 호스트 제약이 만드는 물리적 상한 (모든 후보에 선행 적용)

확정 제약(단일 호스트) 아래에서 분리로 얻을 수 있는 것과 없는 것이 물리적으로 갈린다.

자원분리해도 공유되는가실측 근거결론
호스트 CPU 총량공유 (10코어 고정)4 replica가 피크에 합계 ~10.7코어 점유 = 이미 포화독립 스케일아웃 실효 없음 — 서비스를 쪼개도 CPU 총량은 그대로. JVM 개수만 늘어 오버헤드 증가
호스트 메모리공유 (15.6GiB 고정)JVM 1개 ~950MB. 데이터스토어·mock 컨테이너가 별도 소비JVM 실용 상한 8~10개 — 현재 4 replica 사용 중이므로 여유 슬롯 4~6개. 20 컨텍스트 분리는 19GB 필요로 물리적 불가
MySQL 인스턴스공유 (별도 스키마로만 분리 가능)쓰기 병목이 HikariCP 풀 고갈(waiting=372)스키마를 나눠도 같은 mysqld 프로세스·같은 커넥션 상한 — 격리 효과 부분적
디스크 I/O · Docker 데몬 · 호스트 커널공유호스트 장애·디스크 포화는 분리로 격리 불가
JVM 힙 / 스레드풀 / 배포 재기동격리됨분리로 실제 얻는 것은 이 3가지뿐

핵심 귀결: 실측된 병목 2개(① 앱 티어 CPU 포화 ② HikariCP 커넥션 풀 고갈)는 둘 다 단일 호스트에서 서비스를 쪼개도 해결되지 않는 종류다. ①은 CPU 총량이 고정이고, ②는 같은 MySQL을 공유하기 때문이다. 따라서 단일 호스트에서 MSA가 주는 이점은 “JVM 힙·스레드풀·배포 재기동 격리” 3가지로 제한된다.

데이터 분리의 실익과 한계 (같은 호스트 전제)

방식실익한계
같은 MySQL 인스턴스 내 별도 스키마소유권 명확화, 교차 JOIN 물리적 차단, 미래 이전 준비격리 없음 — 같은 mysqld의 CPU·버퍼풀·커넥션 상한 공유. 한 스키마의 슬로우 쿼리가 전체를 죽인다
별도 MySQL 컨테이너커넥션 풀·버퍼풀 격리(실측 병목 ②에 유효)컨테이너당 메모리 +1GB급, 호스트 디스크·CPU는 여전히 공유. 15.6GiB 예산에서 2~3개가 상한
별도 호스트진짜 격리확정 제약으로 범위 밖

→ 데이터 분리는 “소유권 명확화”에는 유효하지만 “장애 격리”에는 단일 호스트에서 부분적으로만 유효하다. 별도 MySQL 컨테이너는 실측 병목 ②(풀 고갈)에 실효가 있으나 2~3개가 메모리 상한이다.

4-1. 후보 비교

ⓐ 현행 유지 + 패키지·거버넌스 정합만 회복

  • 내용: 단일 모듈 유지. §2-2 데이터 결합 6파일 정리, FR-8 미분류 5건 편입, permissions 이관, context-map 갱신.
  • 얻는 것: 비용 거의 0(추정 2~3일). 경계 위생 회복. ArchUnit R3가 20개 컨텍스트 전부 커버.
  • 잃는 것: 컴파일 타임 경계 강제는 여전히 없음(ArchUnit 런타임 테스트 의존). 미래 추출 시 같은 조사를 반복.
  • 필요 인프라: 없음.
  • 비용: 2~3일.

ⓑ Gradle 멀티모듈 모듈러 모놀리스 ★ 채택

  • 내용: 단일 배포 유지, 빌드를 모듈로 분할. 제안 모듈: common(공유 커널) / payment / goods / ticketing / facility-booking(§3-2 병합) / community-post-message / recruitment / user-partner / platform(virtualqueue·featureflag·notification·alerting·operator·mcp) / composition(catalog·order·dashboard) / bootstrap(앱 조립). 모듈 간 의존은 domain interface + 이벤트만 허용.
  • 얻는 것:
    • 컴파일 타임 경계 강제 — §2-2의 6개 파일이 즉시 컴파일 에러로 드러난다. ArchUnit보다 강한 보증.
    • 모듈별 명시적 API 표면 확정 → 추출이 기계적 작업이 됨.
    • 737개 테스트의 증분·병렬 빌드 개선.
    • 런타임 비용 0 — 컨테이너 여전히 1개.
    • 롤백 쉬움 — 모듈 병합은 되돌리기 간단, 데이터·배포 변경 없음.
  • 잃는 것: 빌드 스크립트 복잡도 증가. domain/commonPermission 엔티티(B4)·EntryTokenGuard 등 공유 커널 정리가 선행돼야 함. 자원 격리·독립 배포는 여전히 없음.
  • 필요 인프라: 없음.
  • 비용: 1~2주 (선행 결합 해소 포함).

ⓒ 선택적 서비스 추출 (strangler)

  • 내용: ⓑ 완료 후 1~2개만 별도 배포. 추출 1순위 = virtualqueue(§4-2), 2순위 = mcp.
  • 얻는 것: 마케팅 폭주 흡수 계층을 앱 본체와 자원 격리. mcp(운영 서브시스템)를 코어 라이프사이클에서 분리.
  • 잃는 것: 컨테이너 +12, 원격 호출 지연·부분 실패 처리, 배포 파이프라인 23개.
  • 필요 인프라: API 게이트웨이(nginx 라우팅으로 대체 가능), 분산 추적(보유), 서비스 디스커버리(compose DNS로 대체 가능), 데이터 분리(virtualqueue는 Redis 전용이라 불필요).
  • 비용: virtualqueue 추출 3~5일(경계가 이미 EntryTokenGuard 하나) / mcp 추출 1주.

ⓓ 전면 MSA (20 컨텍스트 분리) ✗ 미채택

  • 내용: 컨텍스트별 독립 서비스 + DB 분리 + 게이트웨이 + 디스커버리 + SAGA.
  • 얻는 것: 이론상 독립 배포·자원 격리·기술 스택 자유.
  • 잃는 것 (수치):
    • 컨테이너 13 → 30+, 단일 호스트에서 JVM 20개 × 512MB = +10GB RAM.
    • 인프라 비용 3.75~6배 (벤치마크 §5).
    • MSA 이득은 개발자 10~15명 초과 팀에서 발현 — 현재 1인.
    • 평균 서비스 규모 1,600라인 — 서비스가 아니라 함수 크기.
    • MySQL 45테이블을 20개 스키마로 분할 → 듀얼라이트·백필·검증 사이클 20회.
    • DORA: MSA 팀 90%가 결국 배치 동시 배포 → 분산 모놀리스(최악 조합) 위험.
  • 필요 인프라: 게이트웨이 + 디스커버리 + 서비스 메시 + 컨피그 서버 + SAGA 오케스트레이터 + 스키마 20벌.
  • 비용: 3~6개월 + 상시 운영 부담.
  • 미채택 사유: 측정된 부하가 0인 상태에서 3.75~6배 비용과 SAGA 복잡도를 지불하는 것은 명백한 오버엔지니어링이다. 현재 병목은 “도메인이 안 나뉘어서”가 아니다 — 도메인은 이미 잘 나뉘어 있다(R1 0건, 교차 JOIN 0건). 분리해서 얻을 것이 없다.

4-2. 비교표

기준ⓐ 현행+정합ⓑ 멀티모듈ⓒ 선택 추출ⓓ 전면 MSA
처리량 개선없음없음격리된 1~2개만이론상 최대
지연 영향없음없음+원격 홉 1+원격 홉 다수
운영 복잡도현행현행(런타임 동일)매우 높음
구현 비용2~3일1~2주+3~5일/서비스3~6개월
롤백 난이도매우 쉬움쉬움(모듈 병합)매우 어려움
추가 컨테이너00+1~2+20
경계 강제력ArchUnit(런타임)컴파일 타임컴파일+네트워크네트워크
1인 운영 적합성
판정부분 채택(ⓑ의 선행 단계)채택트리거 도달 시미채택

4-2-1. 분리 동기 3개 × 후보 충족도 매트릭스 (사용자 확정 가중치)

사용자가 동기 3개를 모두 1순위로 확정했다. 세 동기는 최적 해가 각각 다르므로 따로 평가한다 — “MSA 하나로 셋 다 해결”은 성립하지 않는다.

충족도: ◎ 충분 / ○ 부분 / △ 미미 / ✗ 못 함 (또는 악화)

동기실측된 문제ⓐ 현행+정합ⓑ 멀티모듈ⓒ 선택 추출ⓓ 전면 MSA분리 없이 얻는 대안
① 트래픽·확장성앱 티어 CPU 바운드. 4 replica 합 ~10.7코어/10코어 호스트. 0에러 상한 ~500 rps (목표 20,000 TPS의 2.5%)○ (virtualqueue만)✗ 악화 — JVM 개수↑로 동일 CPU에서 오버헤드만 증가◎ replica 증설 + HikariCP 풀 튜닝 — 실측상 3→4 replica + pool 10→40으로 knee가 60 → 250300 rps (45배)
② 배포 독립성·개발 속도컴파일 2m 30s + 단위 테스트 3m 49s = 최소 6분 20초 전체 검증 (통합·E2E·detekt 제외) — 증분 빌드로 변경 모듈만 재컴파일·재테스트○ (단 배포 파이프라인 20개 관리)○ 테스트 태그 분리·Gradle 캐시 (모듈 경계 없이는 효과 제한)
③ 장애 격리전역 세마포어 200을 한 컨텍스트가 독점 가능. 서킷브레이커 없음(spring-retry만). HikariCP 풀 전 컨텍스트 공유○ (경계는 굳지만 런타임 동일)○ (JVM·스레드풀만, CPU·DB는 공유)○ (단일 호스트라 CPU·디스크·호스트는 여전히 공유)◎ 컨텍스트별 벌크헤드 + 서킷브레이커 + 타임아웃 — §4-2-2

4-2-2. 동기별 최소 비용 해법 (분리와 무관하게 각각 최적)

동기최소 비용 해법실측 근거예상 비용분리 대비
replica 증설 + HikariCP maximum-pool-size 상향(10→40) + connection-timeout 하향(30s→5s)실측 리포트 부록: 이 조합만으로 조회 knee 60 → 250~300 rps, 0에러 상한 ≥500 rps설정 변경 수시간MSA로는 얻을 수 없음 (CPU 총량 고정)
①-예외virtualqueue 추출대기열은 유입을 차단하는 것이 목적이라 CPU를 거의 안 쓴다. Redis 전용이라 DB 분리 불필요. 백엔드가 죽어도 대기실은 살아 있어야 의미가 있음3~5일분리가 실제로 유효한 유일 케이스
Gradle 멀티모듈 (ⓑ)컴파일 2m30s·단위테스트 3m49s가 컨텍스트 무관하게 전량 재실행. 모듈 경계가 있어야 Gradle이 변경분만 재빌드1~2주서비스 분리 없이 동일 효과
① 컨텍스트별 벌크헤드(전역 Semaphore 200 → 컨텍스트별 permit 분할) ② resilience4j 서킷브레이커 도입(현재 미보유) ③ 외부 호출 타임아웃 일괄 적용HikariCP 풀을 읽기/쓰기로 분리현재 LoadSheddingFilter전역 세마포어 1개라 한 컨텍스트가 200 permit을 전부 소진 가능. catalog·order 파사드의 도메인당 300ms 타임아웃 + failedDomains 부분 저하가 이미 올바른 패턴 — 이를 전 경로로 확대3~5일단일 호스트에서는 분리보다 격리 효과가 큼 (CPU·DB 공유는 분리로 못 푸는데, 벌크헤드는 그 공유 자원의 배분을 통제)

③에 대한 결정적 관찰: 단일 호스트에서 분리가 격리하는 것은 JVM 힙·스레드풀·배포 재기동뿐이다. 그런데 실측 붕괴 모드(F2: 과부하 시 60초 큐잉 → 재기동 전까지 영구 열화)는 스레드풀·커넥션 풀 고갈이 원인이고, 이것은 벌크헤드로 직접 해결된다. 실제로 F2는 이미 LoadSheddingFilter로 수정돼 있고, 남은 것은 그것을 전역 1개에서 컨텍스트별로 쪼개는 것이다 — 서비스 분리보다 훨씬 싸고 효과가 직접적이다.

4-3. 채택 결론

⚠️ 전제 변경으로 개정됨 (2026-07-28) — 사용자가 비용을 인지한 상태에서 물리 분리(4~5 서비스, DB 2단계)를 확정했다. 아래 채택 결론(모듈러 모놀리스·단일 배포 유지)은 폐기한다. 실행설계는 20260728-msa-물리분리-실행설계.md 참조. 본 문서 §1~§3의 AS-IS 실측 데이터는 유효하며 그 설계의 근거로 계속 인용된다.

채택안은 단일 후보가 아니라 동기별 조합이다. 세 동기의 최적 해가 다르기 때문이다(§4-2-1).

우선순위조치대응 동기근거
1HikariCP 풀 튜닝 + replica 증설 (설정 변경)① 트래픽실측상 knee 60→250300 rps(45배). MSA로는 얻을 수 없는 개선
2컨텍스트별 벌크헤드 + 서킷브레이커 + 타임아웃③ 장애 격리실측 붕괴 모드(F2)의 직접 해법. 단일 호스트에서 분리보다 효과가 큼
3ⓑ Gradle 멀티모듈 (ⓐ를 0단계로 흡수)② 배포 속도컴파일 2m30s + 단위테스트 3m49s를 변경 모듈로 한정
4ⓒ virtualqueue 추출 (트리거 도달 시)①-예외분리가 실제로 유효한 유일 케이스
ⓓ 전면 MSA — 미채택단일 호스트에서 동기 3개 중 어느 것도 충족 못 함

ⓓ 미채택 근거를 동기별로 명시한다 (확정 제약 반영):

  1. ① 트래픽에 대해 — MSA는 도움이 안 되는 정도가 아니라 악화시킨다. 실측 병목은 앱 티어 CPU이고 호스트는 10코어 고정이다. 4 replica가 이미 ~10.7코어를 점유한다. 서비스를 20개로 쪼개면 같은 CPU 위에서 JVM 20개의 기본 오버헤드(힙·메타스페이스·GC 스레드·서블릿 스레드풀)가 중복돼 총 처리량이 떨어진다. 메모리는 20 × 950MB = 19GB > 15.6GiB애초에 기동 불가다.
  2. ② 배포 속도에 대해 — 멀티모듈이 같은 효과를 더 싸게 준다. 배포 파이프라인 20개·이미지 20개를 1인이 관리하는 비용이 증분 빌드 이득을 상쇄한다. 게다가 현재 배포는 docker build ... bootJar -x test배포 자체는 이미 테스트를 돌리지 않는다 — 느린 것은 배포가 아니라 로컬 검증 루프이고, 그건 모듈 경계로 해결된다.
  3. ③ 장애 격리에 대해 — 단일 호스트에서는 격리 범위가 3가지로 제한된다. 분리로 얻는 것은 JVM 힙·스레드풀·배포 재기동뿐이고, 실측 병목인 CPU 포화와 MySQL 커넥션 고갈은 여전히 공유된다. 반면 벌크헤드는 그 공유 자원의 배분 자체를 통제하므로 단일 호스트에서 더 직접적이다.
  4. 경계는 이미 잘 나뉘어 있다. domain 교차 import 0건, 교차 JOIN 0건, 공용 컨텍스트 역참조 0건, 결제 경로는 이미 이벤트 기반 최종일관성. MSA로 고칠 경계 문제가 없다 — 실제 결함은 infrastructure 6개 파일의 데이터 결합뿐이고 이건 분리 없이 고친다.
  5. ⓑ는 MSA를 포기하는 선택이 아니라 준비하는 선택이다. 제약(단일 호스트)이 풀리거나 트리거가 도달했을 때 추출이 기계적 작업이 된다. 반대로 지금 ⓓ로 가면 되돌릴 수 없다.

단계적 채택 임계 조건 (측정 가능, OR 조건):

다음 단계트리거 (전부 측정치 기준)
1·2 → 3 (멀티모듈)즉시 착수 가능 (트리거 불요 — 런타임 무변경)
3 → 4 (virtualqueue 추출)① 마케팅 스파이크 시 대기열 처리가 backend CPU 포화에 동반 열화하는 것이 관측, 또는 ② 대기열 입장 처리 P95 >500ms
4 → 코어 추출① HikariCP 풀 튜닝·replica 증설·벌크헤드를 전부 적용한 뒤에도 p95 SLA 미달이 지표로 증명, 그리고 ② 여유 JVM 슬롯(현재 4~6개) 내에서 가능한 범위일 것
→ ⓓ 전면 MSA단일 호스트 제약이 해제(다중 호스트·클라우드)되고 개발 인원 5명 초과. 제약이 유지되는 한 영구 미채택

5. 동일 제품군·경쟁사 아키텍처 벤치마킹

#사례해결 방식참고 / 미참고
1Shopify (Core 모놀리스)커머스 전체를 단일 Rails 앱 “Shopify Core”로 유지하되, 내부를 비즈니스 도메인(billing·orders)별 컴포넌트로 분할. MSA를 검토했으나 “자체 문제를 새로 만든다”고 판단해 모듈러 모놀리스 채택. 경계 강제를 위해 자체 도구(Packwerk) 를 만듦 — 단일 테스트·배포 파이프라인 유지가 핵심 이득참고 — ⓑ의 직접 근거. 우리 ArchUnit R1~R4가 Packwerk와 같은 역할이고(ADR-005도 이 벤치마크를 인용), 여기에 Gradle 모듈로 컴파일 타임 강제를 더하는 것이 다음 수순
2오늘의집 (bucketplace) MSA Phase 1모놀리스 → MSA 전환 시 Branch By Abstraction 사용: Aggregator 레이어로 레거시 API 호환 유지 → 신규 서비스 병행 개발 → 트래픽 점진 이동. 트래픽 섀도잉으로 레거시/신규 응답을 Kafka로 비교 검증. 인프라: Spring Cloud Gateway + gRPC/Protobuf + 분산추적. Phase 1에 3개월 소요부분 참고 — Aggregator 패턴은 우리 catalog·order 조합 파사드가 이미 동일 역할. 섀도잉 검증은 ⓒ 추출 시 채택. 미참고: gRPC·Spring Cloud Gateway는 1인·단일호스트에 과함
3모듈러 모놀리스 전환 통계 (2026)MSA 조기 도입 시 인프라 지출 30~50% 증가 + DevOps 엔지니어 1~2명 추가 필요. 동일 기능 범위 기준 MSA 인프라 비용은 모놀리스 대비 3.75~6배. MSA 이득은 개발자 10~15명 초과 팀에서만 발현. DORA: MSA 팀 90%가 서비스를 배치 동시 배포 → 분산 모놀리스참고 — ⓓ 미채택의 정량 근거. 1인 운영 + 실측 트래픽 0에서 3.75~6배 지출은 정당화 불가
4Spring Modulith / microservices-ready modulith바운디드 컨텍스트 단위로 모듈화하면 “나중에 추출하기 쉬움”. 추출 대비 통신 규칙: 도메인 이벤트 우선, 동기 호출이 필요하면 조회(query)로만 제한하고 모듈 경계의 명시적 계약을 경유 — 크로스 모듈 상태 변경 금지참고 — 우리 현황과 정확히 일치. 이벤트(Kafka 3토픽) + 동기는 조회 위주. 위반은 §3-3 T1(CreatePartnerUseCase의 크로스 컨텍스트 쓰기) 1건뿐 → ⓑ 진행 시 최우선 정리 대상

Sources:


6. 장기 도메인 진화 계획

각 단계는 독립 배포 가능하고 롤백 지점을 가진다.

6-1. 단계별 계획

단계내용전제 조건완료 판정 기준롤백 지점
0단계 — 결합 해소·거버넌스 정합 (즉시)① §2-2 Gateway 6파일에서 타 컨텍스트 Repository 주입 제거 → 공급자 DomainService 경유로 전환 ② domain/common/Permissionuser로 이관 ③ FR-8 미분류 5건(featureflag·partner·virtualqueue·airquality·featuredemo)을 DomainClassification에 편입 ④ docs/domain-context-map.md 갱신(virtualqueue·catalog·order 반영)없음ArchUnit 전체 GREEN + grep으로 infrastructure 교차 Repository 주입 0건 + DomainClassification 합계 = domain 패키지 수(20)코드 되돌리기 (스키마 무변경)
1단계 — Gradle 멀티모듈화11개 모듈로 분할(§4-1 ⓑ). 모듈 간 의존은 domain interface + 이벤트만. bootstrap 모듈이 앱 조립0단계 완료 (미완 시 컴파일 실패)./gradlew build GREEN + 모듈 의존 그래프에 순환 0 + 컨테이너 수 13 유지(런타임 무변경)settings.gradle.kts include 제거로 단일 모듈 복귀
2단계 — facility·booking 경계 재정리두 컨텍스트를 하나의 모듈 facility-booking으로 병합(내부 패키지는 유지). 양방향 Gateway 2개 제거, CreateProgramSessionUseCase 트랜잭션을 모듈 내부로1단계 완료모듈 간 facility↔booking 참조 0건 + 기존 예약 시나리오 테스트 GREEN모듈 재분할 (코드 이동만)
3단계 — virtualqueue 서비스 추출 (트리거 도달 시만)별도 컨테이너로 분리. 데이터 분리 불필요(Redis 전용). 경계는 EntryTokenGuard(HMAC 무상태 검증) 하나라 백엔드는 원격 호출 없이 토큰만 검증ⓒ 트리거 충족(§4-3) + 2단계 완료대기열 E2E 시나리오 GREEN + 백엔드↔대기열 동기 호출 0건(HMAC 무상태) + 마케팅 부하 시 backend CPU 하락 관측대기열 컨테이너 중지 + 피처 플래그 OFF (기존 플래그 virtualqueue.enabled 활용)
4단계 — mcp 서브시스템 추출 (트리거 도달 시만)운영 서브시스템을 코어와 분리. mcp는 코어를 동기 호출하지 않으므로(ADR-004·R3) 경계가 이미 깨끗함3단계 안정화mcp 관련 시나리오 GREEN + 코어 배포가 mcp 배포와 독립mcp 컨테이너를 backend에 재통합
5단계 — 코어 컨텍스트 추출 + DB 분리 (강한 트리거만)goods 또는 ticketing 1개만 추출. 데이터 분리 5단계 필수(아래 6-2)ⓓ 트리거 충족(개발 5명 초과 + 부하 측정)섀도 트래픽 응답 일치 + 독립 배포 2주 무사고스키마는 expand-contract라 코드 롤백만으로 복귀

1~4단계는 데이터 마이그레이션이 없다 — 스키마 무변경이라 롤백이 전부 코드 되돌리기다. 데이터 분리는 5단계에서만 발생한다.

6-2. 5단계 데이터 분리 절차 (Flyway 인라인 백필 DML 금지)

goods를 예로, products·stocks·goods_orders 등 7테이블을 별도 스키마로 분리할 때:

단계내용롤백 지점
1. 듀얼라이트신규 스키마에 테이블을 nullable로 생성(DDL, Flyway). 코드가 기존·신규 양쪽에 동시 기록신규 경로를 아무도 읽지 않으므로 코드 되돌리기
2. 배치 백필Spring Batch 청크(PK 범위) 단위로 기존 데이터 이관. 청크마다 커밋, rate limit, 멱등(재실행 가능). Flyway UPDATE/INSERT SELECT 금지 — 테이블 전체 락으로 배포가 곧 장애배치 중단 (신규 스키마는 미사용)
3. 데이터 검증검증 배치로 “누락 0건 · 기존≠신규 0건” 판정. 결과를 아티팩트로 남김동일
4. 기능 배포신규 스키마를 읽는 경로 배포 (피처 플래그 게이트)플래그 OFF
5. 배포 후 검증신규 경로 지표·샘플 확인 → 이상 시 플래그 OFF / 이전 태그 재기동. 안정화 후 구 테이블 제거(별도 마이그레이션)플래그 OFF

6-3. 진화 단계 다이어그램

flowchart LR
    subgraph S0["0단계 결합해소"]
        Fix[Gateway 6파일 정리]
        Gov[FR-8 분류·문서]
    end
    subgraph S1["1단계 멀티모듈"]
        Mod[11 Gradle 모듈]
    end
    subgraph S2["2단계 경계정리"]
        FB[facility-booking 병합]
    end
    subgraph S34["3~4단계 선택추출"]
        VQ[virtualqueue 서비스]
        MCP[mcp 서비스]
    end
    subgraph S5["5단계 코어분리 (강트리거)"]
        Core[goods 또는 ticketing]
        DB[(전용 스키마)]
    end
    Fix --> Gov
    Gov --> Mod
    Mod --> FB
    FB -.->|부하 측정 트리거| VQ
    VQ -.-> MCP
    MCP -.->|인원 5명 초과| Core
    Core --> DB

점선 = 측정된 트리거 도달 시에만 진행. 실선 = 즉시 착수 경로.


7. 부속 진단 (역할 명세 필수 항목)

7-1. 트래픽 ↔ 구조 적합성

트래픽 지점전제 (목표치, requirement.md:16,19)판정근거
b2c 상시 조회3,000 TPS (목표)지금은 충분nginx LB + backend replica(docker-compose.lb.yml), Redis 캐시 13컴포넌트
b2b 파트너100 TPS (목표)지금은 충분쓰기 위주 저부하
마케팅 폭주20,000 TPS (목표)임계점 근접 → 대응 완료virtualqueue 신설(37파일)로 입장 제어 도입. 이전 진단의 “흡수 계층 부재” 해소. 단 효과 측정치 없음
단일 MySQL 쓰기마케팅 시 주문 확정지금은 충분결제 확정이 이미 Kafka 비동기(§2-4), 대기열이 유입 총량 제한
모든 지점 공통측정 부재가 최대 리스크docker-compose.sim.yml(k6) 자산 보유하나 결과 아티팩트 없음 → §8 Open Question

“과잉” 판정 없음, “이미 초과” 판정 없음. 구조는 목표 트래픽에 대해 대체로 적합하며, MSA로 얻을 트래픽 이득이 현재 근거 위에 존재하지 않는다.

⚠️ 위 표는 초판(실측 미발견 상태) 판정이다. 아래 §7-1-0에서 실측 데이터로 전면 재판정했다.

7-1-0. 트래픽 실측 재판정 (상시 트래픽 시뮬레이터/실측-리포트.md, 2026-07-06)

정정: 초판은 “측정값 0”으로 기술했으나 실측 리포트가 존재한다. 아래가 확정 판정이다.

실측 결과 (조회 경로 GET /facilities, warm, constant-arrival-rate)

구성목표 rps달성 rpsp95에러율판정
3 replica / pool 106059.9459ms0%지속 가능(knee)
3 replica / pool 1010099.41.65s0%지연 열화
3 replica / pool 10200+붕괴 ~96/s60s(timeout)47~100%붕괴
4 replica / pool 40200199.7122ms0%여유
4 replica / pool 40300298.5922ms0%knee 부근
4 replica / pool 40500498.41.03s0%0에러 상한

실측 병목 2건 — 둘 다 분리로 해결되지 않는다

병목실측 근거단일 호스트에서 분리로 해결?
① 앱 티어 CPU 바운드backend 3 replica 피크 CPU 254~271%씩(합 ~7.8코어), 4 replica는 합 ~10.7코어 / 10코어 호스트. 반면 mysql 2963% · redis 819%로 데이터스토어는 여유✗ 아니오 — CPU 총량 고정. 분리하면 JVM 오버헤드만 증가
② HikariCP 커넥션 풀 고갈(쓰기)HikariPool-1 - Connection is not available, request timed out after 30018ms (total=10, active=9, idle=1, waiting=372). 400 rps 쓰기에서 5xx 3,972건✗ 아니오 — 같은 MySQL 공유. 설정 튜닝(10→40)으로 이미 해결

확정 판정

트래픽 지점목표 (requirement.md:16,19)실측 대비판정
b2c 상시 조회3,000 TPS0에러 상한 ~500 rps = 목표의 17%이미 초과 (목표 기준) — 단일 호스트 물리 한계. 설계 문서도 “단일 로컬 머신에서는 물리적 미달 예상”으로 명시
b2b 파트너100 TPS실측 상한 내지금은 충분
마케팅 폭주20,000 TPS실측 상한의 2.5%이미 초과 (목표 기준) — 단 virtualqueue가 유입 자체를 차단하는 설계라 앱이 20,000 TPS를 받을 필요가 없다
오버셀 정확성0건4,206 요청 중 정확히 2,000건 입장, 오버셀 0건정합 (실측 PASS)

결론 3가지:

  1. 구조가 아니라 하드웨어가 상한이다. 데이터스토어는 여유인데 앱 JVM이 CPU를 다 쓴다. 실측 리포트 권장 조치 4번도 “3000/20000 TPS는 수평 확장 전제 — 앱 티어 CPU 바운드, 단일 머신 물리 한계 확인”.
  2. 가장 큰 개선은 이미 검증된 설정 튜닝이다 — 3 replica/pool 10 → 4 replica/pool 40으로 knee 60 → 250300 rps(45배). MSA 분리로는 이만한 개선을 얻을 수 없다.
  3. 잔여 정합성 결함 2건이 미해결이다F1 예약-주문 불일치(Redis 예약 2,000 vs DB 주문 1,357 = 643 슬롯 누수, under-sell), F3 “실패로 보이는 성공”(DB 커밋 1,357 vs 클라이언트 202 관측 228). MSA 분리보다 우선순위가 높다 → §8 OQ-7.

7-1-1. 장애 격리 현황 (분리 없이 얻는 대안의 AS-IS)

장치현황파일 근거
로드 셰딩 (전역 벌크헤드)보유Semaphore(200) 초과 시 즉시 503 + Retry-After. 인증 필터 앞단 등록, /actuator/**·/healthz 제외, enabled 플래그로 즉시 OFFinfrastructure/loadshedding/LoadSheddingFilter.kt전역 1개 — 한 컨텍스트가 200 permit을 전부 소진 가능 → 컨텍스트별 분할 필요
조합 파사드 타임아웃 + 부분 저하보유 (모범) — 도메인당 300ms 타임아웃, 실패 도메인은 제외하고 failedDomains에 기록CatalogCompositionService.kt:28, OrderCompositionService.kt:26catalog·order 2곳에만 적용 → 전 경로 확대 필요
재시도부분 — spring-retrybuild.gradle.kts:54
서킷브레이커없음grep resilience4j build.gradle.kts = 0건도입 필요
커넥션 풀 격리없음 — 단일 HikariCP 풀을 전 컨텍스트 공유, maximum-pool-size 미설정(Spring 기본 10)application.yml상향 + 읽기/쓰기 분리 필요
Redis fail-open보유 — Redis 다운 시 대기열 통과(정합성은 MySQL 보장)가상대기열 TDD “핵심 실패 경로”

→ **장애 격리는 “없다”가 아니라 “전역으로만 있고 컨텍스트별로 없다”**가 정확한 진단이다. 실측 붕괴 모드 F2는 이미 LoadSheddingFilter로 막혔고, 남은 작업은 그것을 컨텍스트별로 쪼개고 서킷브레이커를 더하는 것이다.

7-1-2. 배포·개발 속도 실측 (동기 ②의 근거) — 본 진단에서 직접 측정

커밋 f18434c3, 호스트 동일.

측정 항목명령소요exit
전체 컴파일 (clean 후)./gradlew cleancompileKotlin compileTestKotlin2m 30s (150.8s)0 (BUILD SUCCESSFUL)
단위 테스트 (domain + application, 컨테이너 없음)./gradlew test --tests "com.sportsapp.domain.*" --tests "com.sportsapp.application.*"3m 49s (230.2s)0 (BUILD SUCCESSFUL)
합계 (최소 검증 루프)≈6분 20초

위 합계는 하한이다 — 제외: Testcontainers 통합 40파일, scenario E2E 89파일, @SpringBootTest 풀부팅 18파일, detekt, harnessCheck.

테스트 유형파일 수분리 시 영향
전체 테스트 파일737
순수 단위 (MockK)323모듈 경계로 즉시 분할 가능
Testcontainers 사용40컨테이너 기동 비용이 지배적
@SpringBootTest 풀부팅18분리 후에도 통합 검증엔 전체 부팅 필요
scenario E2E89분리해도 E2E는 전체 대상

핵심: 한 컨텍스트만 고쳐도 main 1,280 + test 737 파일 전체가 재컴파일·재테스트 대상이다. 이것이 동기 ②의 실체이고, Gradle 멀티모듈(ⓑ)이 정확히 이 문제를 서비스 분리 없이 푼다. 참고로 배포 자체는 이미 테스트를 돌리지 않는다(backend/Dockerfile:18bootJar -x test -x detekt -x harnessCheck) — 느린 것은 배포가 아니라 로컬 검증 루프다.

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

관점판정근거
① 백본 두께 (알림 전용 쏠림?)양호 — 이전 대비 개선event.payment.payment.v1 구독자가 주문 4컨텍스트 + notification = 5 groupId 팬아웃. 20260706 시점의 “notification 100% 쏠림”은 해소됨
② 공용 컨텍스트 역참조우수 — 규칙 준수payment는 발행만. OrderConfirmationGatewaywhen(orderType) 허브 0건. 각 주문 컨텍스트가 자기 *PaymentEventWorker로 확정
③ Layer 1/2 판단적절결제 확정(유실 불가·내구성)=Kafka, 채팅방 provision·환불(같은 프로세스 파생)=Spring @TransactionalEventListener(AFTER_COMMIT). 오분류 0건
④ 멱등·순서·버저닝양호eventId 멱등, aggregateId 파티션 키, .v1 접미사, sealed payload + @JsonTypeInfo(property="eventType")
⑤ 토픽 네이밍정합3토픽 전부 event.{domain}.{sub-domain}.v{N}

동기 결합 전환 후보: §2-2의 6개 Gateway. 단 대부분 읽기성 즉시 조회(예약 시 시설 스케줄 확인)라 이벤트화가 부적합하다. 지금 동기가 옳다 — 조기 이벤트화는 오버엔지니어링. 전환 트리거는 §6 3~5단계(배포 단위 분리) 도달 시이며, 그때 조회는 원격 API 또는 읽기모델 복제로, 쓰기는 이벤트로 전환한다.

예외 1건: partner → user 크로스 쓰기(§3-3 T1)는 읽기가 아니라 쓰기 오케스트레이션이다. Modulith 벤치마크(§5 #4)의 “크로스 모듈 상태 변경 금지” 위반에 해당하므로, ⓑ 진행 시 이벤트 기반(user 생성 → partner 구독) 또는 보상 트랜잭션으로 정리한다.

7-3. 코드 컨벤션 정합 조사 (grep 실측, 커밋 f18434c3)

정합 (위반 0건)

규칙 ID결과
no-jpa-query (@Query)0건 — QueryDSL CustomRepositoryImpl 사용
no-consumer-record0건 — EventWorker가 DTO 직접 매핑
no-crosscontext-raw-read (네이티브 SQL 교차 조회)0건 — JdbcTemplate 2건은 Spring Batch 메타데이터 초기화(BatchMetadataSchemaInitializer.kt:32)
공용 컨텍스트 역참조 (when(orderType) 허브)0건
레이어 의존 방향 (R2) / domain 교차 참조 (R1)0건 — ArchUnit 7개 스펙이 강제

불일치 (파일:라인 + 룰ID) — 조사만 하고 수정하지 않는다.

룰 ID파일:라인내용심각도조치 위임
no-business-flow-in-infra (변형)infrastructure/booking/gateway/FacilityScheduleGatewayImpl.kt:6-9 외 5파일 (§2-2 전수)infrastructure 어댑터가 타 컨텍스트 Repository를 직접 주입해 남의 테이블을 읽음. import는 domain interface라 R1은 통과하나 스키마 결합은 잔존 — no-crosscontext-raw-read가 막으려는 것과 동일한 실질(수단만 JdbcTemplate → Repository)p1§6 0단계 / private-senior-be
no-bean-config-wiring 회피 / no-conditional-on-propertyapplication/facility/usecase/CreateProgramSessionUseCase.kt 외 20여건@Profile("!test-jpa")로 빈 등록 토글. 의도는 기능 토글이 아니라 JPA 슬라이스 테스트 컨텍스트 제외라 규칙 취지(무중단 롤백 불가)와는 무관하나, 규칙 문언상 @Profile 금지 대상p1(문맥 완화)/private-review
domain 순수성 (JPA 침투)domain/** 42파일이 import jakarta.persistence (예: domain/goods/entity/Product.kt @Entity)domain 엔티티가 곧 JPA 엔티티. 컨벤션의 “Domain Entity와 JPA Entity 분리 시” 규칙을 적용하지 않은 상태 — 의도적 선택으로 보이나 §6 5단계 DB 분리 시 마찰 요인p2(관찰)결정 필요 → §8 OQ-4
공유 커널 순수성 (R4 변형)domain/common/Permission.kt:11 @Table(name = "permissions")공유 커널이 물리 테이블을 소유. R4(common이 도메인을 import하지 않음)는 통과하나, 공유 커널이 스키마를 갖는 것은 모듈·서비스 분리 시 소유권 충돌p2§6 0단계
FR-8 갱신 절차 미이행SupportToCoreDependencyRulesTest.kt:19-21DomainClassification 합계 15 vs 실제 domain 패키지 20 — featureflag·partner·virtualqueue·airquality·featuredemo 미분류. R3가 이들의 코어 역참조를 검사하지 못함p2§6 0단계
문서-코드 정합docs/domain-context-map.mdvirtualqueue 0회·catalog/order 0회 언급. recruitment↔payment/community 연동을 “BE-55·BE-60 예정”으로 기술하나 코드는 배선 완료(ApplyRecruitmentUseCase, RecruitmentPaymentEventWorker)p3doc-sync

아키텍트는 조사·보고까지가 범위다. 위 항목의 코드 수정은 /private-review 또는 private-*-implementer로 위임한다.

7-4. TO-BE 컴포넌트 다이어그램 (ⓑ 채택안 반영)

flowchart LR
    subgraph Boot["bootstrap 모듈 (단일 배포)"]
        App[SportsApplication]
    end
    subgraph Comp["composition"]
        Cat[catalog·order·dashboard]
    end
    subgraph Core["코어 모듈"]
        Pay[payment]
        Goods[goods]
        Tick[ticketing]
        FB[facility-booking]
        Comm[community-post-message]
        Recr[recruitment]
        UsrP[user-partner]
    end
    subgraph Plat["platform"]
        VQ[virtualqueue·featureflag·notification·alerting·mcp]
    end
    subgraph SK["common 공유커널"]
        CK[OrderType·DomainEvent·계약만]
    end
    App --> Comp
    App --> Core
    App --> Plat
    Comp -->|읽기 조합| Core
    Core -->|이벤트만| Plat
    Core --> SK
    Plat --> SK

모듈 간 화살표는 domain interface + 이벤트로만 성립한다. permissions 테이블 이관 후 common은 물리 스키마를 갖지 않는다.


8. Open Questions (사용자 결정 필요)

#질문배경선택지
OQ-1MSA 분리의 목적이 무엇인가?해소 — 사용자가 동기 3개(트래픽·확장성 / 배포 독립성 / 장애 격리) 전부를 1순위로 확정. §4-2-1·§4-2-2에 반영종결
OQ-2부하 실측을 할 것인가?해소상시 트래픽 시뮬레이터/실측-리포트.md(2026-07-06)에 실측 존재. §7-1-0에 반영종결
OQ-2′실측 리포트가 2026-07-06 기준이다. virtualqueue(7/11 도입) 이후 재측정할 것인가?실측 시점 이후 virtualqueue·recruitment·catalog/order가 유입됐다. 특히 virtualqueue 효과(다운스트림 도달 TPS 감소율)는 PRD가 실측을 요구하지만 아직 측정 안 됨. §4-3 트리거가 이 측정에 의존재측정 후 진행 / 기존 실측으로 진행
OQ-7실측 결함 F1·F3를 MSA 논의보다 먼저 처리할 것인가?F1: 643 슬롯 누수(Redis 예약 2,000 vs DB 주문 1,357 — under-sell), F3: “실패로 보이는 성공”(DB 커밋 1,357 vs 클라이언트 성공 228). 둘 다 정합성·정산 결함이고 아키텍처 변경과 무관하게 존재한다. 아키텍트 판단으로는 MSA·멀티모듈보다 우선순위가 높다선처리(권장) / 병행 / 후순위
OQ-8HikariCP 풀 튜닝을 코드베이스에 반영할 것인가?실측 부록에서 pool 10→40 + timeout 30s→5s로 knee가 4~5배 개선됐으나, 레포 application.yml에는 여전히 maximum-pool-size 설정이 없다(Spring 기본 10). 실측 튜닝이 git-ignored 오버레이에만 있고 본 코드에 미반영즉시 반영(권장) / 유지
OQ-3facility·booking을 하나의 모듈로 병합하는 데 동의하는가?§3-2 유일한 재경계 필요 지점. 양방향 데이터 접근 + 단일 트랜잭션 쓰기. 병합하면 경계 위반이 사라지지만 컨텍스트 개수는 줄어든다병합 / 양방향 Gateway를 단방향으로 정리해 분리 유지
OQ-4domain 엔티티 = JPA 엔티티 구조를 유지할 것인가?domain 42파일이 jakarta.persistence 의존(B3). 유지하면 §6 5단계 DB 분리 시 도메인 모델과 스키마가 함께 움직인다. 분리하면 POJO+매퍼 작성 비용이 1,280파일 규모에 발생유지(권장 — 5단계는 먼 미래) / 지금 분리
OQ-5상품·주문의 쓰기 상위 aggregate를 만들 것인가?§3-4에서 티켓·시설상품이 사용자 계층(상품 하위)과 불일치. 본 진단은 “라이프사이클 이질로 통합 미채택, 읽기 파사드로 충분”이 옳다고 판단하나, 이는 20260706 판정 계승이다현행 유지(권장) / 통합 aggregate 재검토
OQ-6virtualqueue 도입 효과가 측정됐는가?20260711에 21티켓으로 도입됐고 기본 OFF 상태다. 마케팅 폭주 대응의 핵심 자산인데 효과 측정치가 없다. §6 3단계 트리거가 이 측정에 의존한다부하 테스트로 ON/OFF 비교 / 현행 유지

지금 할 일 vs 나중에

동기 3개(트래픽·배포속도·장애격리)에 각각 최소 비용 해법을 매핑한 우선순위다.

우선순위 1 — 정합성 결함 선처리 (아키텍처 변경과 무관, OQ-7)

  1. F1 슬롯 누수 643건 — Redis 예약 후 DB 저장 실패 시 예약 보상(취소) 누락 → under-sell.
  2. F3 “실패로 보이는 성공” — DB 커밋 1,357 vs 클라이언트 성공 관측 228. 정산·UX 혼선.

우선순위 2 — 동기 ① 트래픽 (설정 변경, 수시간) ★ 비용 대비 효과 최대

  1. HikariCP maximum-pool-size 10→40 + connection-timeout 30s→5s를 application.yml에 반영 (OQ-8) — 실측 검증됨.
  2. backend replica 3→4 + JVM 힙 고정(Xmx 768m, mem_limit 1.8g) — 실측상 knee 60 → 250300 rps (45배). MSA로는 얻을 수 없는 개선이다.

우선순위 3 — 동기 ③ 장애 격리 (3~5일, 분리 없이)

  1. LoadSheddingFilter 전역 Semaphore(200) → 컨텍스트별 permit 분할 — 한 컨텍스트의 permit 독점 차단.
  2. resilience4j 서킷브레이커 도입 (현재 미보유) + 외부 호출 타임아웃 일괄 적용.
  3. catalog·order의 300ms 타임아웃 + failedDomains 부분 저하 패턴을 전 경로로 확대 — 이미 있는 모범 패턴의 확산.

우선순위 4 — 경계 위생 (§6 0단계, 2~3일)

  1. infrastructure 교차 Repository 주입 6파일 정리FacilityOwnershipGatewayImpl·FacilityScheduleGatewayImpl·SlotInfoGatewayImpl·SlotQueryGatewayImpl·GoodsProductGatewayImpl·RecipientContactResolver. 유일한 실질 경계 결함이자 멀티모듈화의 선행 조건.
  2. domain/common/Permissionuser 이관 / FR-8 분류 누락 5건 편입 / docs/domain-context-map.md 갱신 / CreatePartnerUseCase 크로스 컨텍스트 쓰기 정리(T1).

우선순위 5 — 동기 ② 배포 속도 (1~2주, 승인 후)

  1. Gradle 멀티모듈 11개 분할 — 컴파일 2m30s + 단위테스트 3m49s를 변경 모듈로 한정. 런타임 무변경, 컨테이너 13 유지.
  2. facility·booking 모듈 병합 (OQ-3 결정 후).

트리거 도달 시에만

  • virtualqueue 서비스 추출 — 분리가 실제로 유효한 유일 케이스(유입 차단이 목적, Redis 전용이라 DB 분리 불요). 트리거: 대기열 처리가 backend CPU 포화에 동반 열화 관측 또는 대기열 P95 >500ms
  • mcp 서비스 추출 — 위 안정화 후. 단 여유 JVM 슬롯(4~6개) 예산 내에서만
  • 코어 추출 — 우선순위 2·3을 전부 적용한 뒤에도 SLA 미달이 지표로 증명될 때

명시적 미채택 (단일 호스트 제약 하에서)

  • 전면 MSA(20 컨텍스트 분리) — 동기 3개 중 어느 것도 충족하지 못한다: ① 트래픽은 악화(CPU 총량 고정 + JVM 오버헤드 증가, 메모리 19GB > 15.6GiB로 기동 자체가 불가) ② 배포 속도는 멀티모듈이 더 싸게 해결 ③ 장애 격리는 CPU·MySQL·호스트가 여전히 공유라 효과가 JVM 힙·스레드풀·재기동 3가지로 제한. 단일 호스트 제약이 해제되기 전까지 영구 미채택.
  • API 게이트웨이·서비스 디스커버리·서비스 메시 선제 도입 — 단일 배포 단위에 홉만 추가
  • 컨텍스트별 별도 MySQL 컨테이너 다수 도입 — 메모리 예산상 2~3개가 상한. 소유권 명확화는 같은 인스턴스 내 별도 스키마로 충분
  • 상품·주문 통합 쓰기 aggregate — 라이프사이클 이질(God-aggregate 위험). 읽기 파사드로 충분
  • 조기 이벤트화 — 현재 동기 Gateway는 읽기성 즉시 조회라 동기가 옳음
  • domain/JPA 엔티티 분리 — 1,280파일 규모 비용 대비 이득이 먼 미래에만 발생

Document History

날짜변경 내용
2026-07-28개정 2 — 사용자 확정 제약 반영. 분리 동기 3개(트래픽·확장성 / 배포 독립성 / 장애 격리) 전부 1순위 + 단일 호스트 확정. 추가·정정: ① §4-0 운영 전제에 실측 호스트 스펙(10코어/15.6GiB, JVM 1개 950MB) ② §4-0-1 단일 호스트 물리적 상한 신설 — CPU 총량 고정·메모리 상한으로 JVM 실용 상한 810개(20 분리는 19GB로 기동 불가), 데이터 분리 실익/한계 구분 ③ §4-2-1 동기 3개 × 후보 충족도 매트릭스 + §4-2-2 동기별 최소 비용 해법 신설(분리 없이 얻는 대안 포함) ④ §4-3 채택 결론을 단일 후보→동기별 조합 우선순위로 재작성, ⓓ 미채택을 동기별로 논증 ⑤ §7-1-0 트래픽 실측 재판정 — 초판 “측정값 0” 정정, 실측-리포트.md 반영(3 replica knee 60 rps·붕괴 200+, 4 replica+pool40 knee 250~300·0에러 상한 500, 병목=앱 CPU 10.7코어/10코어 + HikariCP waiting=372, 오버셀 0건 PASS, 목표 20,000 TPS의 2.5%) ⑥ §7-1-1 장애 격리 AS-IS — LoadSheddingFilter 전역 세마포어·catalog/order 300ms 타임아웃 보유, 서킷브레이커 미보유§7-1-2 빌드·테스트 직접 측정 — 컴파일 2m30s·단위테스트 3m49s(둘 다 exit 0), 배포는 이미 -x test ⑧ Open Questions 갱신(OQ-1·OQ-2 종결, OQ-2′·OQ-7 F1/F3 결함 선처리·OQ-8 풀 튜닝 미반영 신설) ⑨ 실행 계획을 동기별 우선순위 5단계로 재작성
2026-07-28최초 작성 — MSA 분리 판단 진단. 트리 신선도 게이트(f18434c3, 0/0) 선행. 브리핑 사실 정정 2건(PKG-2가 main에 포함됨 — “설계 의도와 트렁크 현실의 괴리” 없음). AS-IS 20컨텍스트 규모 실측, 결합 6축 실측표(domain 0 / application 22쌍 / infrastructure 5쌍 = MSA 블로커 / 교차 JOIN 0 / Kafka 3토픽 5팬아웃), 컨텍스트별 PASS·재경계 판정(facility↔booking 유일 재경계), 트랜잭션 분할 7지점(결제 경로 T5·T6은 이미 분리 완료, 진짜 SAGA는 T1 1건), 사용자 정의 경계 매트릭스 재판정(일치6/부분6/경계불일치2, 선행 갭 8건 해소 확인), MSA 후보 4개 비교 → ⓑ Gradle 멀티모듈 채택 / ⓓ 전면 MSA 미채택, 벤치마킹 4건(Shopify·오늘의집·MSA 비용 통계·Spring Modulith), 진화 6단계 + 데이터 분리 5단계, Open Questions 6건