MSA 물리 분리 실행설계 (6 서비스 / DB 2단계)

Background / 전제

사용자가 물리적으로 분리된 서비스를 목적지로 확정했다. 멀티모듈은 목적지가 아니라 경유지다.

항목확정
목적지별도 배포 단위로 쪼개진 서비스 (모듈러 모놀리스는 1단계 경유지)
서비스 개수6개commerce / payment / facility-booking / social / platform / edge (OQ-8 결정: payment 단독 분리)
DB 분리2단계 완주 — 1단계 서버 분리(공유 DB + GRANT) → 2단계 DB 분리
운영 환경로컬 docker compose 단일 호스트, Docker VM 15.6 GiB 유지 (OQ-1: 상향 안 함)
동기트래픽·확장성 / 배포 독립성 / 장애 격리 (3개 모두 1순위)
비용 인지사용자가 인프라 비용·운영 부담 청구서를 인지하고 물리 분리를 선택함
  • 선행 진단서: 20260728-msa-분리-architecture.mdAS-IS 실측 근거는 그 문서가 SSOT다. 본 문서는 그 실측 위에 “어떻게 안전하게 물리 분리할 것인가”만 다룬다.
  • 본 문서는 분리 여부를 재논쟁하지 않는다. 단 물리적으로 불가능하거나 예산이 빡빡한 부분은 수치로 명시한다(§7).

트리 신선도 게이트 (설계 기준 커밋)

항목
HEAD89ae6c240314fb85b03067d78239c5b63fef99de
origin/main...HEAD0 / 0 (detached at origin/main)
워킹트리clean
개정 전 기준ed22bd15PH0-04 머지 반영으로 갱신

0단계 진행 현황 재실측 (89ae6c24 기준 — 잔존 건수 정정)

항목상태실측 근거
infrastructure 교차 Repository 주입 제거6/6 완료PH0-01~04. PH0-04가 RecipientContactResolver를 해소infrastructure/notification/gateway/RecipientContactGatewayImpl.kt:10 주석 “user 도메인의 UserRepository(=users 테이블)를 직접 읽지 않고, 공급자의 공개 행위 계약인…”. infrastructure/notification/gateway/ Repository 주입 0건 확인
7번째 결합 — 원 진단서 6파일 목록에 없던 건미해소infrastructure/security/McpTokenAuthenticationFilter.kt 생성자 3개 Repository 직접 주입: :23 McpTokenRepository, :24 McpTokenScopeRepository, :25 PermissionRepository. import는 :6·:7·:3. SecurityConfig.kt:36에 필터 체인 등록
컨텍스트 분류 20/20완료PH0-06 (b2372918)
permissions 이관 (PH0-05) / CreatePartner SAGA (PH0-08)PR 중미머지. 관련 잔재: infrastructure/user/mysql/PermissionRepositoryImpl.kt:4domain.common.PermissionRepository 구현

7번째 결합의 성격 — OQ-4 결정으로 해소되는 경로다

McpTokenAuthenticationFilter전 서비스 공통 인증 체인에 있으면서 platform 소유 테이블 3개(mcp_tokens·mcp_token_scopes·permissions)를 직접 읽는다. §2-2 D6·§6-3과 같은 문제다.

  • 서비스 분리 시 이 필터를 각 서비스에 그대로 두면 6개 서비스가 platform 테이블을 직접 읽어야 하고, 이는 §3-2 금지 ①(남의 테이블 SELECT)에 걸려 GRANT로 물리 차단된다 → 기동 즉시 권한 오류.
  • OQ-4 결정(edge 일괄 인증 방안 b)으로 해소된다 — 이 필터를 edge에만 두고, edge가 platform의 토큰 검증 결과를 내부 헤더로 전파한다. 그러면 platform 테이블을 읽는 주체가 edge 하나로 줄고, edge → platform 원격 검증으로 전환하면 DB 직접 읽기도 사라진다.
  • 1단계 작업 항목으로 등록한다(§11 W1-06).

1. 서비스 경계 확정

1-1. 묶는 기준 (우선순위 명시)

순위기준근거 데이터
1트랜잭션 경계 — 한 트랜잭션 안에서 함께 쓰는 컨텍스트는 쪼개지 않는다선행 진단 §3-3 T1~T7
2돈 경로 격리 (사용자 결정 — OQ-8)payment를 단독 서비스로
3결합 밀도 — application 교차 import가 많은 쌍은 같은 서비스로22쌍 / 62 import (§1-2 정정)
4부하 특성 — 스파이크형(마케팅)과 상시 조회형 분리실측: 앱 CPU 바운드, knee 250~300 rps
5변경 주기 — 독립 라이프사이클은 격리mcp·virtualqueue

확정 사항 (사용자 승인): ① facility + booking 병합 ② payment 단독 분리.

1-2. 결합 밀도 데이터 (설계 입력, 89ae6c24 기준 — 변동 없음)

소비자 → 공급자import소비자 → 공급자import
community → message6order → ticketing / recruitment / goods / booking2 / 2 / 2 / 2
booking → payment6facility → booking2
post → community5catalog → ticketing / recruitment / facility2 / 2 / 2
goods → payment4recruitment → community2
dashboard → ticketing4partner → user1
catalog → goods4dashboard → user / facility1 / 1
ticketing → payment3
recruitment → payment3
dashboard → goods / booking3 / 3

⚠️ 총계 정정 (개정 2): 초판·개정 1이 “22쌍 / 48 import”로 기술했으나 산술 오류였다. 89ae6c24에서 직접 카운트한 결과 총 62 import / 고유 22쌍이다(위 표의 건수 합 = 62). 쌍 목록·건수는 정확했고 합계만 틀렸다. 아래 모든 비율·차집합 계산을 62 기준으로 재산정했다.

핵심 관찰 2건:

  1. payment 인바운드 16건(booking 6 + goods 4 + ticketing 3 + recruitment 3), 아웃바운드 0건. payment는 순수 공급자다 — 단독 분리 시 원격 인바운드 16건, 원격 아웃바운드 0건이라 의존 방향이 단순하다.
  2. 파사드 3개가 교차 참조의 48%를 차지한다 — catalog 10 + order 8 + dashboard 12 = 30 / 62. 이들의 배치가 원격 호출 수를 지배한다.

1-3. 후보 비교 (개정 — B’ 채택)

전체 62 import 중 서비스 내부로 흡수되는 것과 경계를 넘는 것을 후보별로 산정했다.

후보구성내부화경계 넘는 import판정
A 부하격리 (5)commerce(goods·ticketing) / facility-booking / social / payment / platform(파사드·virtualqueue 포함)1745미채택 — virtualqueue가 platform에 묻혀 격리 무효
B 결합+엣지 (5)commerce(goods·ticketing·payment) / facility-booking / social / platform / edge(파사드 3 포함)2339미채택 — payment가 commerce에 동거해 돈 경로 격리 실패
B′ 개정 1 (6)아래 B″와 같으나 dashboard를 edge에 배치1646미채택 (개정 2)모듈 순환 발생(§1-3-1)
B″ = B′ + dashboard→platform (6)commerce(goods·ticketing) / payment / facility-booking / social / platform(+dashboard) / edge(virtualqueue·catalog·order·image)1745채택
C 최소분리 (4)core-commerce(거래 전부) / social / platform(파사드 포함) / edge(virtualqueue)3032미채택 — 최대 서비스가 전체 35%라 동기 ①·③ 무효화

1-3-1. B′ 폐기 사유 — 모듈 그래프 순환 (실측)

dashboardedge에 두면 platformedge 순환이 생긴다. 89ae6c24에서 확인했다.

방향근거 (파일:라인)
platform → edgepresentation/mcp/controller/McpOperatorProfileTools.kt:3application.dashboard.dto.DashboardSummaryResponse, :4application.dashboard.usecase.GetMyDashboardSummaryUseCase
edge → platformapplication/dashboard/usecase/GetMyDashboardSummaryUseCase.kt:9domain.user.service.UserDomainService (같은 파일이 presentation/dashboard에서도 domain.user 2건 추가 참조)

사용자 결정 — 방안 (a) 채택: dashboard 7파일을 platform에 배치한다.

근거내용
① 순환 해소mcp(platform)와 dashboard(platform)가 같은 서비스가 되고, dashboard → user도 platform 내부가 되어 양방향이 모두 내부화된다
② 이미 판정된 성격§4-1 C8이 dashboard를 “실시간성 요구 낮음 → TTL 캐싱”으로 판정했다. 실제로 GetMyDashboardSummaryUseCase.kt:10@Cacheable이 이미 부착돼 있다 — 핫 리드 fan-out(catalog·order)과 성격이 다르다
③ 대면 사용자dashboard는 operator 대면 운영 KPI다. platform(운영·플랫폼)의 관심사와 일치하고, mcp 운영 도구가 이미 dashboard를 소비한다

규모 영향 (실측):

서비스개정 전개정 후차이
edge62파일 / 2,591라인55파일 / 2,324라인−7 / −267
platform414파일 / 12,865라인421파일 / 13,132라인+7 / +267

dashboard = application/dashboard 5파일 170라인 + presentation/dashboard 2파일 97라인 = 7파일 / 267라인. domain·infrastructure 레이어 없음(Aggregate 없는 읽기 조합).

1-4. 채택 — 후보 B″ (6 서비스, dashboard는 platform)

서비스컨텍스트파일 / 라인 (실측)소유 테이블근거
commercegoods, ticketing237 / 7,36011마케팅 스파이크 2종(한정판·좌석)을 한 서비스에 격리
paymentpayment37 / 1,4191돈 경로 완전 격리 (사용자 결정). 인바운드 16 / 아웃바운드 0
facility-bookingfacility, booking191 / 5,6954 + Mongo 1병합 확정. 실측 조회 CPU 바운드 경로
socialcommunity, post, message, recruitment254 / 7,89811결합 밀도 최고 클러스터(6+5+2) 전부 내부화
platformuser, partner, notification, alerting, operator, featureflag, mcp, airquality, weather, featuredemo, dashboard421 / 13,13218신원·운영·플랫폼. partner→user(1)·dashboard→user(1) 내부화, mcp↔dashboard 순환 해소
edgevirtualqueue + catalog·order 파사드 + image + 게이트웨이 라우팅55 / 2,3240 (Redis만)무상태 엣지. 파사드는 이미 API Composition 패턴
합계1,195 / 37,82845

B″ 채택 근거:

  1. 사용자가 “돈 경로 완전 격리”를 결합 밀도보다 우선했다. B 대비 경계 넘는 import가 39 → 45(+6) 로 늘지만, 늘어난 것(goods→payment 4, ticketing→payment 3 = 7건 증가, dashboard→user 1건 내부화로 −1)은 §5에서 검증한 대로 트랜잭션 밖 호출이라 SAGA를 유발하지 않는다. 싼 6건을 지불해 payment를 격리하는 거래다. 1-1. dashboard를 platform에 두어 순환을 제거했다 (§1-3-1). 이 배치가 없으면 platform ↔ edge 순환으로 모듈 그래프가 위상정렬 불가해져 1단계 멀티모듈화 자체가 성립하지 않는다.
  2. payment 격리의 실질 이득: ① payment 장애가 조회·커뮤니티·시설을 죽이지 않음 ② PG 연동 변경이 commerce 배포와 무관 ③ payment만 별도 스케일·자원 상한 ④ 45테이블 중 payments 1개만 소유해 2단계 DB 분리가 가장 단순.
  3. edge 유지 근거: 남은 파사드 2개(catalog·order)는 이미 전용 스레드풀로 벌크헤드돼 있다 — CatalogAsyncConfig.kt:18-25(core 4 / max 8 / queue 50), OrderHistoryAsyncConfig.kt:20. virtualqueue와 동거해도 스레드 격리가 구현된 상태다 (OQ-10 동거 확정).
  4. 비용은 메모리다 — payment 분리로 JVM 1개(약 650 MiB/replica)를 1,419라인(전체 3.8%) 을 위해 지불한다. §7-4에서 예산 판정.

1-5. TO-BE 서비스 구성도

flowchart LR
    subgraph Edge["edge (무상태·DB권한 0)"]
        GW[nginx 라우팅 + 일괄 인증]
        VQ[virtualqueue]
        FC[catalog·order 파사드]
    end
    subgraph Svc["도메인 서비스 5"]
        CM[commerce<br/>goods·ticketing]
        PY[payment]
        FB[facility-booking]
        SO[social]
        PL[platform<br/>+dashboard]
    end
    subgraph Data["데이터 (1단계 공유+GRANT → 2단계 분리)"]
        MySQL[(MySQL 45테이블)]
        Mongo[(MongoDB)]
        Redis[(Redis)]
        Kafka[Kafka]
    end
    GW --> VQ
    GW --> FC
    FC -->|읽기 fan-out| CM
    FC -->|읽기 fan-out| FB
    FC -->|읽기 fan-out| SO
    CM -->|결제 개시| PY
    FB -->|결제 개시| PY
    SO -->|결제 개시| PY
    PY --> Kafka
    Kafka --> CM
    Kafka --> FB
    Kafka --> SO
    CM --> MySQL
    FB --> Mongo
    PL --> MySQL
    VQ --> Redis

2. 45테이블 소유권 배분표 (2단계 DB 분리 설계 입력)

2-1. 서비스별 소유 테이블 (payment 분리 반영)

서비스소유 테이블
commerceproducts stocks carts cart_items goods_orders goods_order_items limited_drops (goods 7) · events seats tickets ticket_orders (ticketing 4)11
paymentpayments1
facility-bookingprograms regions (facility 2) · slots bookings (booking 2) · MongoDB facilities 컬렉션 (domain/facility/entity/Facility.kt @Document)4 + Mongo 1
socialcommunities community_members community_bookings (3) · posts comments (2) · rooms messages room_participants room_invitations (4) · recruitments applications (2)11
platformusers roles user_roles role_permissions permissions(이관 후) (5) · partner partner_api_key partner_audit_log (3) · notifications push_tokens (2) · alerts (1) · operator_inbox_notifications (1) · feature_flags feature_flag_audit_logs (2) · mcp_tokens mcp_token_scopes mcp_audit_logs mcp_anomaly_events (4) · dashboard 0 (Aggregate·쓰기 없는 읽기 조합)18
edge없음 — Redis 전용 (queue:{type}:{id}:waiting·:heartbeat, 입장 토큰은 HMAC 무상태)0
합계45

dashboard 이동은 테이블 배분에 영향이 없다 — dashboard는 Aggregate Root·쓰기가 없는 읽기 조합 파사드라 소유 테이블이 0개다(ADR-001 “조회·유틸” 분류). edge → platform 이동으로 양쪽 소유 테이블 수가 모두 불변(edge 0, platform 18)이다.

근거: @Entity + @Table(name=...) 전수 매핑. 45 = Flyway CREATE TABLE 집계와 일치. payment는 payments 1테이블만 소유 — 6개 서비스 중 DB 분리가 가장 단순하다(§8-4 순서 근거).

2-2. 소유권이 불명확하거나 다투는 테이블 — 전수

#테이블문제판정·조치
D1permissionsdomain/common이 소유(domain/common/Permission.kt:11) — 공유 커널이 물리 테이블을 가짐. infrastructure/user/mysql/PermissionRepositoryImpl.kt:4가 구현platform으로 확정. PH0-05 이관 완료가 2단계 진입 선행 조건
D2BATCH_JOB_INSTANCESpring Batch 메타데이터Flyway 관리 밖 — infrastructure/config/BatchMetadataSchemaInitializer.kt:32가 런타임 생성. 45테이블 집계에 없음commerce 단독 소유. Batch 사용처는 infrastructure/goods/batch/SellerTypeBackfillJobConfig.kt 1곳뿐(grep 전수). Initializer도 commerce로 이동
D3regionsfacility 소유이나 지역 코드 참조 테이블 성격facility-booking 소유 유지. 현재 교차 소비 0건
D4users모든 서비스가 userId 보유. 표시명·연락처 필요 가능platform 소유. PH0-04가 이미 ACL 게이트웨이로 전환 — 교차 필요는 이벤트 payload 동봉 또는 원격 조회
D5feature_flags모든 서비스가 플래그를 읽는다 (FeatureFlagEvaluatordomain/common)platform 소유(쓰기) + 전 서비스 읽기는 Redis 캐시(RedisFeatureFlagCacheStore). DB 직접 읽기 금지
D6mcp_tokens mcp_token_scopes permissionsMcpTokenAuthenticationFilter.kt:23,24,25가 3개 Repository 직접 주입 — 전 서비스 공통 인증 체인 (7번째 결합)platform 소유. OQ-4 결정으로 해소 — 필터를 edge에만 두고 edge → platform 원격 검증 (§6-3, §11 W1-06)
D7MongoDB facilitiesfacility-booking 소유. MySQL과 별 인스턴스facility-booking 소유. 다른 서비스가 Mongo 미사용 → 이미 사실상 전용. 2단계에서 전용 유저 부여만

어느 서비스도 소유하지 않는 테이블: 0건. 다투는 테이블 D4·D5·D6 3건 — 전부 “1 소유자 + N 읽기”이며 읽기 경로를 이벤트·Redis·원격 API로 대체하면 소유권 충돌이 사라진다.


3. 1단계 “공유 DB + 분리 서버” 구간의 위험 통제

이 구간은 정의상 분산 모놀리스다. 선행 진단서가 인용한 DORA 통계(MSA 팀 90%가 배치 동시 배포)의 그 상태다. 기간을 한정하고 강제 장치를 건다.

위치 정정 (OQ-3 결정 반영): GRANT 기반 권한 분리는 종착지가 아니다. 사용자가 “1단계 서버 분리 → 2단계 DB 분리”를 명시했으므로 3단계 DB 분리까지 완주한다. GRANT는 2단계 진입 전 안전장치이며, 공유 DB 구간에서 교차 접근이 실제로 0건임을 런타임으로 증명하는 수단이다.

3-1. 교차 스키마 접근 금지의 강제 수단 — DB 권한(GRANT)

리뷰·ArchUnit은 약하다(런타임을 못 막는다). 단일 MySQL 인스턴스에서도 서비스별 DB 유저 + 테이블 단위 GRANT로 물리 차단이 가능하다.

강제 수단강도적용
코드 리뷰약 (사람)보조
ArchUnit R1~R4중 (빌드 시점)1단계 유지
서비스별 DB 유저 + GRANT강 (런타임 물리 차단)1단계 필수 선행
DB 유저권한 대상비고
svc_commercecommerce 11테이블 + BATCH_*SELECT/INSERT/UPDATE/DELETE
svc_paymentpayments 1테이블최소 권한 — 6개 중 가장 좁다
svc_facility_bookingfacility-booking 4테이블+ Mongo 별도 유저
svc_socialsocial 11테이블
svc_platformplatform 18테이블
svc_edge없음DB 권한 0 — Redis만
flyway_migrator전체 DDL마이그레이션 전용, 앱 런타임 미사용
  • 효과: svc_socialproducts를 SELECT하면 런타임 권한 오류로 즉시 실패한다. 남의 테이블 JOIN도 불가능해진다.
  • 지금 걸 수 있는 이유: 컨텍스트 교차 JOIN이 0건(선행 진단 §2-1)이므로 기존 기능이 깨지지 않는다.
  • 검증: 서비스별로 “남의 테이블 SELECT 시도 → 권한 거부” 테스트 1건씩 두어 회귀를 막는다.
  • Flyway 소유권: 1단계에서는 마이그레이션을 단일 소유자로 유지한다. 서비스별 분할은 3단계 작업이다(1단계에 쪼개면 버전 번호 충돌).

3-2. 이 구간의 허용 / 금지 목록

구분항목
허용① 자기 소유 테이블 CRUD ② 자기 소유 테이블 간 JOIN ③ 다른 서비스의 공개 REST API 호출 ④ Kafka 이벤트 발행·구독 ⑤ 공유 Redis 사용 (단 키 네임스페이스를 서비스별로 분리) ⑥ 자기 트랜잭션 내 자기 테이블 다중 쓰기
금지남의 테이블 SELECT/JOIN (GRANT로 물리 차단) ② 크로스 서비스 트랜잭션 — 한 @Transactional 안에서 원격 호출 후 그 결과에 자기 쓰기를 의존 ③ 남의 Flyway 마이그레이션 수정 ④ 공유 Redis 키를 남의 네임스페이스에 쓰기 ⑤ 서비스 간 동기 호출 사슬 3홉 이상 ⑥ 두 서비스를 동시에 배포해야 동작하는 변경(= 분산 모놀리스 확정 신호. 계약 버저닝으로 회피)

3-3. 최대 허용 기간과 2단계 진입 조건

항목
최대 허용 기간8주 — 초과 시 2단계 미완이어도 회고하고 계획을 재수립한다(무기한 방치가 분산 모놀리스 고착의 원인)
2단계(DB 분리) 진입 조건 (AND)① GRANT 적용 후 권한 거부 오류 0건이 2주 지속 (교차 접근 없음을 런타임이 증명) ② permissions 이관(PH0-05) 완료 ③ D4·D5·D6 읽기 경로가 이벤트/Redis/원격 API로 전환 완료 (D6은 §11 W1-06) ④ 서비스별 독립 배포가 각 서비스 2회 이상 성공

1단계 종착 선택지는 없다 — 3단계까지 완주한다(OQ-3 확정).

3-4. 1단계 위험 통제 다이어그램

flowchart LR
    subgraph Svcs["분리된 서비스 6"]
        CM[commerce]
        PY[payment]
        SO[social]
        PL[platform]
    end
    subgraph Guard["강제 장치"]
        G1[서비스별 DB 유저 GRANT]
        G2[ArchUnit 모듈 경계]
        G3[계약 버저닝]
    end
    subgraph Shared["공유 MySQL 단일 인스턴스"]
        T1[(commerce 11)]
        T2[(payments 1)]
        T3[(social 11)]
        T4[(platform 18)]
    end
    CM --> G1
    PY --> G1
    SO --> G1
    PL --> G1
    G1 -->|권한 있음만| T1
    G1 -->|권한 있음만| T2
    G1 -->|권한 있음만| T3
    G1 -->|권한 있음만| T4
    G2 -.->|빌드 차단| Svcs
    G3 -.->|동시배포 방지| Svcs

4. 서비스 간 통신 설계

4-1. 경계를 넘어 남는 호출 — 전수 (B″ 기준)

62개 교차 import 중 17개가 서비스 내부로 흡수되고 45개가 경계를 넘는다.

내부화되는 17건흡수 서비스
community → message 6 · post → community 5 · recruitment → community 2social (13)
facility → booking 2facility-booking (2)
partner → user 1 · dashboard → user 1platform (2)
#호출import성격채택 방식근거
C1facility-booking(booking)payment 결제 개시6쓰기 트리거 (동기 필요 — 사용자가 결제창을 즉시 받아야 함)원격 RESTCreateBookingUseCase @Transactional 0건(import조차 없음) → 이미 트랜잭션 분리
C2social(recruitment)payment 결제 개시3동일원격 RESTApplyRecruitmentUseCase @Transactional 0건. KDoc :18 “PG 네트워크 호출을 감싸지 않도록 클래스 레벨 @Transactional을 두지 않는다”
C3 (신규)commerce(goods)payment 결제 개시4동일원격 RESTCreateGoodsOrderUseCase @Transactional 0건
C4 (신규)commerce(ticketing)payment 결제 개시3동일원격 RESTPurchaseTicketsUseCase @Transactional 0건
C5payment → 주문 4종 확정0 (이벤트)비동기 확정이벤트 (현행 유지)event.payment.payment.v1 + 5 groupId 팬아웃, eventId 멱등. 이미 완성 — 손대지 않는다
C6edge(catalog) → commerce·facility-booking·social10읽기 fan-out원격 REST (병렬 + 300ms 타임아웃)CatalogCompositionService.kt:28 DOMAIN_TIMEOUT_MILLIS=300L, 실패 도메인은 failedDomains로 부분 저하. 로컬 호출을 REST로 바꾸는 것뿐
C7edge(order) → commerce·facility-booking·social8동일원격 REST (병렬 + 300ms)OrderHistoryAsyncConfig.kt:20 전용 executor
C8 (개정)platform(dashboard) → commerce (ticketing 4 + goods 3 = 7) · facility-booking (booking 3 + facility 1 = 4)11읽기 집계원격 REST + TTL 캐싱운영 KPI라 실시간성 요구 낮음. GetMyDashboardSummaryUseCase.kt:10@Cacheable이 이미 부착돼 있어 캐싱 기반이 준비된 상태
C8-i (신규 내부화)platform(dashboard)platform(user)1읽기서비스 내부dashboard가 platform으로 이동해 내부화. 순환 해소의 핵심 (§1-3-1)
C9platform(notification)platform(user) 수신자 연락처1읽기서비스 내부notification·user 동일 서비스. PH0-04로 ACL 게이트웨이화 완료
C10edgeplatform 토큰 검증 (신규, OQ-4)인증원격 RESTD6·§6-3 해소 경로. edge 일괄 인증
C11edge(virtualqueue) → 없음0호출 없음입장 토큰이 HMAC 무상태 검증(domain/common/EntryTokenGuard.kt:13)

통신 총계 검산

항목건수
C1 booking→payment6
C2 recruitment→payment3
C3 goods→payment4
C4 ticketing→payment3
C6 edge(catalog)→3서비스10
C7 edge(order)→3서비스8
C8 platform(dashboard)→2서비스11
경계 넘는 합계45
내부화 (C8-i 1 + C9 1 + social 13 + facility-booking 2)17
총계62

개정 효과: dashboard 이동으로 경계 넘는 호출이 46 → 45(−1) 되고, 더 중요하게 platform ↔ edge 순환이 제거된다. edge의 원격 fan-out 대상은 catalog·order 2개 파사드(18건)로 줄어든다.

4-2. 방식별 판단 근거

방식적용근거
원격 RESTC1C4 (결제 개시 4종), C6C8 (읽기 fan-out), C10 (인증)결제 개시는 이미 트랜잭션 밖이라 의미 변화 없음. 읽기 fan-out은 타임아웃·부분 저하가 이미 구현됨
이벤트 (Kafka)C5 (결제 확정)이미 완성. 추가 이벤트화는 하지 않는다 — 조기 이벤트화는 최종일관성 부담만 늘린다
읽기모델 복제현 단계 미채택D3·D4 정도가 후보인데 교차 소비가 0~1건이라 복제 인프라(동기화·정합 검증) 비용이 이득보다 크다. 3단계 이후 재검토

4-3. 프로토콜 — REST 채택, gRPC 미채택

항목결정근거
프로토콜HTTP/JSON (REST)① 기존 52개 Controller가 이미 REST — 서버 측 변경 0 ② 단일 호스트 loopback이라 직렬화가 병목이 아니다(실측 병목은 앱 CPU) ③ 1인 운영에서 스키마 컴파일 파이프라인 유지 비용이 큼
gRPC미채택벤치마킹한 오늘의집은 gRPC/Protobuf를 썼으나 폴리글랏 다중 팀 전제다. 단일 언어(Kotlin)·1인·단일 호스트에서는 정당화되지 않는다
클라이언트RestClient (infrastructure/external/ExternalRestClientFactory.kt 타임아웃 패턴 재사용)기존 자산
계약 관리서비스별 OpenAPI 스냅샷 + 소비자 측 계약 테스트ProvidedInterfaceContractTest(ADR-003) 선례를 서비스 경계로 확장

4-4. 통신 토폴로지

flowchart LR
    Client[모바일·웹]
    subgraph Edge["edge"]
        GW[nginx + 일괄 인증]
        FC[catalog·order 파사드]
    end
    CM[commerce]
    PY[payment]
    FB[facility-booking]
    SO[social]
    PL[platform +dashboard]
    K[Kafka event.payment.payment.v1]
    Client --> GW
    GW --> FC
    GW --> CM
    GW --> FB
    GW --> SO
    GW -->|토큰 검증| PL
    FC -->|REST 병렬 300ms 18건| CM
    FC -->|REST 병렬 300ms| FB
    FC -->|REST 병렬 300ms| SO
    PL -->|dashboard REST+TTL 11건| CM
    PL -->|dashboard REST+TTL| FB
    CM -->|결제 개시 7| PY
    FB -->|결제 개시 6| PY
    SO -->|결제 개시 3| PY
    PY -->|발행| K
    K -->|구독 멱등| CM
    K -->|구독 멱등| FB
    K -->|구독 멱등| SO

5. SAGA 필요 지점 재판정 (payment 분리 반영)

5-1. 기존 트랜잭션 분할 지점의 B″ 판정

dashboard 이동은 SAGA 판정에 영향이 없다 — dashboard는 쓰기·트랜잭션이 없는 읽기 조합이다.

#지점B″에서SAGA 필요?
T1CreatePartnerUseCase@Transactional에 user 쓰기 + partner 쓰기platform 내부 (user·partner 동일 서비스)불필요 — 로컬 트랜잭션 유지
T2CreateProgramSessionUseCase — facility 읽기 + booking 쓰기facility-booking 내부불필요
T3CreateCommunityPostUseCase — community 인가 읽기 + post 쓰기social 내부불필요
T4AddCommentUseCase:22requireActiveMember + post 쓰기social 내부불필요
T5결제 개시 4종4건 전부 서비스 경계를 넘는다 (C1~C4) — B에서 내부였던 goods·ticketing도 원격화불필요 — 아래 5-2에서 검증
T6결제 확정 (payment → 주문 4종)이미 Kafka 이벤트 + eventId 멱등불필요 — 이미 코레오그래피 SAGA다
T7Gateway 6개 구현PH0-0104 완료. 경계 넘는 것은 C1C4·C6~C8읽기·트리거라 불필요

5-2. T5 재검증 — 단정하지 않고 실측했다

payment 단독 분리로 결제 개시 4종이 전부 원격 호출이 된다. 새 SAGA가 생기는지 4개 파일 전수 검증했다 (89ae6c24).

UseCase@Transactional 애노테이션import ...Transactional판정
application/goods/usecase/CreateGoodsOrderUseCase.kt0건없음트랜잭션 밖
application/ticketing/usecase/PurchaseTicketsUseCase.kt0건없음트랜잭션 밖
application/booking/usecase/CreateBookingUseCase.kt0건없음트랜잭션 밖
application/recruitment/usecase/ApplyRecruitmentUseCase.kt0건 (grep 매치 1건은 :18 KDoc 주석 텍스트)없음트랜잭션 밖

검증 방법: grep -c "@Transactional"import 존재 여부로 애노테이션/주석을 구분했다. ApplyRecruitmentUseCase는 카운트가 1이었으나 import가 없어 주석임을 확인했다 — 카운트만 믿으면 오판할 지점이었다.

5-3. 결론 — 신규 SAGA 0건 유지 (검증됨)

payment를 단독 분리해도 새로 필요해지는 SAGA는 없다. 근거:

  1. 결제 개시 4종이 애초에 트랜잭션 밖이다 — 4개 파일 모두 @Transactional 애노테이션 0건, import 0건. 주문 PENDING 생성과 PG 개시가 이미 별 트랜잭션이므로 원격화가 트랜잭션 구조를 바꾸지 않는다.
  2. 결제 확정이 이미 코레오그래피 SAGA다event.payment.payment.v1 발행 + 각 컨텍스트 *PaymentEventWorker 구독 확정 + eventId 멱등. 오케스트레이터를 새로 도입하지 않는다.
  3. 쓰기 트랜잭션 4건(T1~T4)이 모두 서비스 내부로 흡수된다 — 결합 밀도 상위 쌍(community→message 6, post→community 5, facility→booking 2, partner→user 1)을 같은 서비스에 넣은 결과다.

5-4. SAGA는 아니지만 설계해야 하는 실패 경로

#지점실패 시나리오필요 처리
F-AC1~C4 결제 개시 원격 호출 (4건으로 증가)주문 PENDING 생성 성공 → payment 원격 호출 실패주문이 PENDING으로 고립. 로컬 호출 시에도 있던 위험이나 원격화로 확률 증가 → PENDING 만료 스위퍼(TTL 경과 후 취소)가 유일한 신규 설계 부담. B(2건)보다 적용 범위가 4건으로 늘어난다
F-B실측 F1 — 슬롯 누수 643건Redis 예약 성공 → DB 저장 실패 → 예약 미보상0.5단계로 선행 처리 확정 (§8-2)
F-CC6~C8 읽기 fan-out일부 서비스 타임아웃이미 구현됨failedDomains 부분 저하

6. 인프라 구성

6-1. 구성 요소 판정

요소판정근거
게이트웨이nginx (Spring Cloud Gateway 미채택)docker-compose.lb.yml의 nginx-lb가 실측 검증됨(15 MiB) ② 필요 기능은 경로 라우팅 + 인증 위임 ③ Spring Cloud Gateway는 JVM 1개 추가(+~650 MiB)인데 6서비스 예산에서 정당화 안 됨(§7-4)
서비스 디스커버리compose DNS (Eureka·Consul 미채택)서비스 6개가 고정 이름·단일 호스트. compose 서비스명 DNS + depends_on: condition: service_healthy(이미 사용, docker-compose.yml:6-14)
인증·JWTedge 일괄 인증(방안 b) + RS256 전환 — 확정 (OQ-4)§6-3
JWT 블랙리스트공유 Redis 유지RedisJwtBlacklistStore — 로그아웃 무효화는 전 서비스가 즉시 봐야 한다
설정 관리compose env + .env.{dev,prod} (Config Server 미채택)scripts/deploy.sh.env.dev DOCKER_TAG 치환 방식으로 이미 동작. Config Server는 JVM 1개 추가라 예산 낭비
피처 플래그 전파Redis 캐시 공유 (RedisFeatureFlagCacheStore)D5 해결. platform이 DB 쓰기, 전 서비스가 Redis 읽기
관측보유분 활용 — otel-collector + tempo(추적) + prometheus + loki + grafana분산 추적이 MSA 필수인데 이미 갖춰져 있다. 서비스별 service.name 리소스 속성만 부여하면 트레이스가 경계를 넘어 이어진다
헬스체크·기동 순서기존 패턴 확장healthcheck 6개 + service_healthy 이미 사용. 서비스는 데이터스토어 healthy 후, edge는 5개 서비스 healthy 후 기동
Kafka공유 브로커 유지토픽 3개·groupId 7개가 이미 서비스 경계와 정합. 단 유휴에도 CPU 66%·메모리 1.17 GiB → OQ-5 조사 대상
STOMP 브로커 릴레이Redis pub/sub 릴레이 — social 추출 전제로 확정 (OQ-7)§9-3, §7-4

6-2. MSA 선행 인프라 보유 현황

필수 요소보유근거
분산 추적보유tempo + otel-collector
메트릭·로그 집계보유prometheus + loki + promtail + grafana
게이트웨이/LB보유nginx-lb
이벤트 버스보유Kafka 3토픽 5팬아웃, eventId 멱등
헬스체크·기동 순서보유compose healthcheck 6 + service_healthy
부하 검증 도구보유k6 시나리오 11개 (qa/load/k6/)
E2E 회귀보유Playwright 10 spec (qa/e2e/specs/)
서비스별 DB 권한 분리미보유§3-1 — 1단계 신규
RS256 키 관리 + edge 일괄 인증미보유§6-3 — 1단계 신규
Redis pub/sub STOMP 릴레이미보유§9-3 — social 추출 전제
서비스 간 계약 테스트부분ProvidedInterfaceContractTest 선례만

6-3. 인증 설계 — edge 일괄 인증 + RS256 (OQ-4 확정)

문제

필터읽는 데이터소유분리 시 문제
JwtAuthenticationFilterJWT 서명 + Redis 블랙리스트대칭키 공유 문제 (아래)
PartnerApiKeyAuthenticationFilterpartner_api_keyplatform각 서비스가 platform 테이블을 읽어야 함 → GRANT 차단
McpTokenAuthenticationFiltermcp_tokens·mcp_token_scopes·permissions (:23,:24,:25)platform동일 (7번째 결합)

현재 JWT는 HS256 대칭키다 — JwtTokenProvider.kt:16 @Value("\${app.jwt.secret}"), :20 Keys.hmacShaKeyFor(...). 6개 서비스가 같은 secret을 공유하면 어느 한 서비스가 뚫려도 토큰 위조가 가능해진다.

확정 설계

항목결정
파트너·MCP 인증edge가 일괄 수행 — edge만 platform에 토큰 검증을 원격 요청(C10)하고, 검증된 신원을 내부 헤더로 전파한다. 6개 서비스가 platform 테이블을 읽지 않으므로 GRANT와 충돌하지 않는다
JWT 검증각 서비스 자체 검증 유지 (원격 홉 0) — 단 RS256 비대칭으로 전환. platform만 사설키로 서명하고, 나머지 5개는 공개키로 검증만 한다
키 배포platform이 JWKS 엔드포인트를 노출하거나, 공개키를 compose env로 배포 (단일 호스트라 후자가 단순)
내부 헤더 신뢰 경계edge → 서비스 통신은 compose 내부 네트워크로만 노출하고, 서비스는 외부 포트를 직접 열지 않는다 (nginx만 host 포트 바인딩)
작업 위치1단계 필수 (§11 W1-05·W1-06)

7. 리소스 예산 실측 판정 (6 서비스 / VM 15.6 GiB 고정)

7-1. 실측 하드웨어

항목근거
호스트 물리 RAM32 GiBsysctl hw.memsize = 34,359,738,368
호스트 CPU10코어sysctl hw.ncpu = 10
Docker VM 가용 메모리15.6 GiB (고정 — 상향 안 함)docker info MemTotal = 16,748,941,312 bytes. OQ-1 결정
Docker VM CPU10docker info NCPU

7-2. 현재 JVM 힙 설정 — 미설정 (치명적)

위치실측문제
backend/Dockerfile:40ENTRYPOINT ["java", "-jar", "/app/app.jar"]-Xmx 없음JVM이 컨테이너 메모리의 25%를 max heap으로 잡는다
docker-compose.ymlmem_limit·deploy.resources 0건컨테이너 무제한 → JVM이 VM 전체 15.6 GiB의 25% ≈ 3.9 GiB를 max heap으로 인식
실측 리포트가 쓴 값Xmx 768m + mem_limit 1.8g → 컨테이너당 ~950 MBgit-ignored 오버레이에만 존재. 본 코드 미반영

판정: 이 상태로 6개 서비스를 띄우면 각 JVM이 3.9 GiB를 가정해 합계 23.4 GiB > 15.6 GiB로 OOM이다. 5개일 때(19.5 GiB)보다 악화됐다. 서비스별 명시적 -Xmx·mem_limit·cpus가 물리 분리의 절대 선행 조건이다(OQ-2 확정, §11 W1-02).

7-3. 인프라 컨테이너 메모리 (docker stats 실측)

컨테이너실측실측 CPU비고
kafka1,199 MiB66.1%유휴에도 CPU 66% — OQ-5 조사 대상. 최대 단일 소비자
mysql564 MiB1.4%
redis35 MiB0.6%maxmemory dev 256 / prod 512 MiB
otel-collector761 MiB2.0%관측
mock-pg41 MiB0.0%mock 4종 ≈ 164 MiB
nginx-lb15 MiB0.0%
mongodb / minio / mailhog~400 / ~200 / ~20 MiB (추정 — 미기동)실측 불가
묶음합계
데이터스토어 + mock (base)1,199 + 564 + 256 + 400 + 200 + 164 + 20 + 15 = 2,818 MiB ≈ 2.75 GiB
관측 스택 (otel 761 + prometheus ~400 + tempo ~300 + loki ~200 + grafana ~200 + promtail ~50)≈ 1,911 MiB ≈ 1.87 GiB
Redis pub/sub STOMP 릴레이 (OQ-7)+0 MiB (신규 컨테이너 없음) — 기존 Redis 재사용. pub/sub은 메시지를 저장하지 않아 maxmemory 소비 없음. 클라이언트 출력 버퍼만 증가 → client-output-buffer-limit pubsub 설정 검토 필요
인프라 총계 (관측 포함)≈ 4.62 GiB

릴레이가 예산에 미치는 영향은 메모리 0, CPU 소량이다. Spring의 enableStompBrokerRelay(RabbitMQ/ActiveMQ 필요, +300~500 MiB 컨테이너)를 쓰지 않고 기존 Redis pub/sub로 인스턴스 간 팬아웃하는 방식이라 신규 컨테이너가 없다. 이것이 이 방식을 택하는 예산상 이유다.

7-4. 6 서비스 JVM 예산 — 최소·목표 2안

Spring Boot 비-힙(metaspace·code cache·스레드 스택) ≈ 250~300 MiB를 힙에 가산해 컨테이너 추정치를 잡았다.

최소 구성 (각 1 replica)

서비스라인-Xmx컨테이너replica소계 (MiB)
commerce7,360640m9201920
payment1,419384m6501650
facility-booking5,695640m9201920
social7,898512m7901790
platform (+dashboard)13,132640m9201920
edge (dashboard 제외)2,324384m6501650
합계37,82864,850 ≈ 4.74 GiB

dashboard 이동의 예산 영향은 무시 수준이다 — 267라인(전체 0.7%)이 edge → platform으로 옮겨질 뿐이고, 두 서비스 모두 힙 구간(platform 640m / edge 384m)이 바뀌지 않는다. 합계 불변 4,850 MiB. dashboard는 @Cacheable 캐시 엔트리만 힙을 쓰는 읽기 조합이라 aggregate·영속 컨텍스트 부담이 없다.

인프라JVM합계15.6 GiB 대비판정
최소 구성4.624.749.36 GiB60%✅ 여유 6.2 GiB

목표 구성 (핫 서비스 2 replica)

서비스-Xmx컨테이너replica소계 (MiB)replica 근거
commerce640m92021,840마케팅 스파이크 대상
payment384m65021,300돈 경로 — 단일 장애점 회피
facility-booking640m92021,840실측 조회 CPU 바운드 경로
social512m79021,580릴레이 구축 후에만 가능(§9-3)
platform640m9201920저부하
edge384m6501650무상태 (필요 시 증설 쉬움)
합계108,130 ≈ 7.94 GiB
인프라JVM합계15.6 GiB 대비판정
목표 구성4.627.9412.56 GiB80.5%⚠️ 들어가나 빡빡하다 — 여유 3.04 GiB
참고: 5서비스 목표(B, 7인스턴스)4.626.2810.90 GiB70%여유 4.7 GiB
참고: 현재 모놀리스 4 replica4.623.808.42 GiB54%기준선

판정과 replica 여유 감소 (수치)

항목5서비스(B)6서비스(B″)차이
목표 구성 사용량10.90 GiB12.56 GiB+1.66 GiB
VM 사용률70%80.5%+10.5pt
남은 여유4.7 GiB3.04 GiB−1.66 GiB
추가 가능 인스턴스 (평균 850 MiB 기준)~5개~3개−2개
총 JVM 상한11~12개12~13개 (인스턴스가 작아져 개수는 늘지만)
  • 결론: 예산 초과는 아니다. 목표 구성 12.56 / 15.6 GiB = 80.5% 로 들어간다.
  • 단 여유가 4.7 → 3.04 GiB로 줄어 추가 replica 여지가 ~5개에서 ~3개로 감소한다. 마케팅 이벤트 대비 commerce를 3~4 replica로 올리려면 다른 서비스를 1 replica로 낮춰야 한다.
  • payment 분리의 정확한 비용: 1,419라인(전체 3.8%)을 위해 650 MiB/replica, 목표 구성에서 1,300 MiB(전체 예산의 8.3%). 이것이 “돈 경로 완전 격리”의 청구서다.
  • 힙을 줄여야 할 경우의 우선순위: ① platform 640m → 512m (저부하, −128 MiB) ② social 512m → 448m (릴레이로 인스턴스당 세션 수 분산) ③ commerce·facility-booking줄이지 않는다 — 실측 CPU 바운드·스파이크 대상이라 힙 압박 시 GC로 지연이 악화된다.
  • 80.5%는 운영 상한에 가깝다 — OS·Docker 데몬 오버헤드와 순간 피크를 고려하면 이 이상 인스턴스를 늘리기 어렵다. 초과 필요가 생기면 OQ-1(VM 상향)을 재개해야 한다.

7-5. CPU 판정 — 처리량 이득 없음 (격리 이득만)

항목실측함의
호스트 CPU10코어고정
모놀리스 3 replica 피크7.8코어 (각 254271%)
모놀리스 4 replica 피크~10.7코어이미 호스트 포화
데이터스토어mysql 2963%, redis 819%여유
kafka유휴 66%분리와 무관한 상시 소비 — OQ-5

판정: 6 서비스 10 인스턴스는 같은 총 작업량을 더 많은 JVM에 나눠 담는 것이다.

  • 처리량은 늘지 않는다. JVM 10개의 GC 스레드·JIT·스레드 스택 오버헤드가 4개보다 커서 약간 감소할 가능성이 있다. 5서비스(7 인스턴스)보다도 오버헤드가 크다.
  • 얻는 것은 격리다 — 동기 ③의 실현. 동기 ①(처리량)은 단일 호스트에서 분리로 얻을 수 없다는 선행 진단 결론이 유지된다.
  • 동기 ①의 유효 수단은 replica 증설 + HikariCP 풀 튜닝(실측: knee 60→250~300 rps)이며 물리 분리와 병행해야 한다.
  • CPU 상한 배분 권고 (합 ≤ 10):
서비스cpus 상한근거
commerce2.5스파이크 대상
facility-booking2.5실측 CPU 바운드 조회
social1.5WebSocket + 게시·모집
payment1.0PG 왕복 대기가 지배적(CPU 소비 적음)
platform1.0저부하
edge1.0무상태 fan-out
합계9.5≤ 10 ✓

7-6. HikariCP 커넥션 예산 (1단계 공유 DB의 숨은 제약)

항목
MySQL max_connections기본 151 (실측 튜닝 시 500)
목표 구성 10 인스턴스 × 풀 40 (균등 시)400 커넥션 → 기본 151 초과
판정max_connections 500 상향 + 서비스별 차등 배분 필수

권고 배분 (목표 구성 기준, 합 250):

서비스풀/인스턴스replica소계
commerce40280
facility-booking30260
payment20240
social25250
platform20120
edge010 (DB 권한 없음, §3-1)
합계250 ≤ 500 ✓

실측 병목 ②(HikariCP 고갈)가 분리 후에는 서비스별로 쪼개져 관측된다 — 한 서비스가 풀을 다 먹어도 다른 서비스 풀은 남는다. 이것 자체가 격리 이득이다.


8. 단계별 실행 계획

8-1. 단계 개요

단계내용배포 단위DB
0 (진행 중)경계 위생 — 교차 Repository 제거(6/6 완료)·분류 정합(완료)·permissions 이관(PR)1공유
0.5 (신규 — OQ-6)실측 결함 F1·F3 선행 처리1공유
1Gradle 멀티모듈(6경계) + JVM 힙·풀·GRANT·RS256·edge 인증·Redis 릴레이1공유
2서비스 물리 분리 (추출 순서 §8-3)1 → 6공유 + GRANT 분리
3DB 분리6서비스별 스키마/인스턴스

8-2. 0.5단계 — 실측 결함 선행 처리 (OQ-6 채택)

물리 분리 착수 전에 처리한다. 조치 방법은 티켓으로 위임하고, 여기서는 무엇을·왜 먼저만 정의한다.

결함실측 내용재현 경로왜 분리 전이어야 하는가
F1 — 슬롯 누수 643건Redis 예약 2,000건 vs DB goods_orders PENDING 1,357건 = 643 슬롯이 예약됐으나 주문 미생성(under-sell). Redis 게이트 통과 후 DB 저장이 HikariCP 풀 타임아웃으로 실패했는데 예약이 요청 내에서 보상되지 않음qa/load/k6/goods-limited-drop-spike.js 계열 — limitedQuantity=2000·perUserLimit=1 회차에 400 rps × 30s(총 4,206 요청). 검증: Redis reserved:* 키 수 vs goods_orders PENDING 수 대조① 이 경로가 곧 commercepayment 경계가 된다 — 분리 후에는 Redis 예약(commerce)과 주문·결제(commerce/payment)가 서비스를 걸쳐 실패하므로 원인이 네트워크 실패인지 보상 누락인지 구분이 어려워진다. ② 지금은 단일 프로세스 로그·트랜잭션으로 추적 가능하다. ③ 재고 정합 결함을 안고 분리하면 분리 후 발생한 문제와 기존 결함을 분간할 수 없다(기준선 오염)
F3 — “실패로 보이는 성공”DB 주문 커밋 1,357건 vs 클라이언트 202(accepted) 관측 228건. 주문이 커밋됐는데 클라이언트는 5xx를 받음. Idempotency-Key가 재시도 중복은 막지만 UX·정산 혼선위와 동일 시나리오. 검증: goods_orders 카운트 vs k6 summary의 202 카운트 대조분리 후 이 현상은 “원격 호출 타임아웃”으로 오진될 가능성이 높다 — 실제 원인(응답 유실)과 새 원인(네트워크)이 같은 증상을 낸다. 지금 원인을 확정해 두어야 분리 후 진단이 가능하다
  • 완료 판정: 위 k6 시나리오 재실행 시 ① Redis 예약 수 = DB 주문 수(누수 0) ② 클라이언트 202 수 = DB 주문 수(응답 일치). 결과를 아티팩트로 남긴다.
  • 조치 방법(보상 트랜잭션/응답 처리)은 설계하지 않는다private-senior-be가 티켓으로 분해한다.

8-3. 단계별 전제·완료 판정·롤백

단계전제완료 판정 기준롤백 지점
0없음① infrastructure 교차 Repository 주입 0건 — 현재 1건 잔존(McpTokenAuthenticationFilter.kt:23,24,25, 7번째 결합) ② DomainClassification 20/20 (완료) ③ permissions → user 이관 머지 ④ ArchUnit 전체 GREEN코드 되돌리기 (스키마 무변경)
0.50단계 완료§8-2 완료 판정 2건 + 아티팩트코드 되돌리기
10.5단계 완료./gradlew build GREEN + 모듈 순환 0 ② 6경계로 모듈 분할, 모듈 간 참조가 §4-1의 C1~C11뿐 ③ -Xmx·mem_limit·cpus compose 명시 ④ HikariCP 차등 배분 + max_connections 500 ⑤ GRANT 스크립트 + 권한 거부 테스트 6건 ⑥ RS256 전환 + edge 일괄 인증 동작 ⑦ Redis pub/sub 릴레이 다중 인스턴스 메시지 전달 검증 ⑧ 컨테이너 수 13 유지(런타임 무변경)settings.gradle.kts include 제거 → 단일 모듈 복귀. RS256·릴레이는 플래그/설정 롤백
21단계 완료서비스 추출마다: ① healthy + /actuator/health UP ② 기존 API 응답 동일(섀도잉 비교, §9) ③ E2E 10 spec GREEN ④ 남의 테이블 SELECT → 권한 거부 확인 ⑤ 분산 추적에 경계 트레이스 연결 확인서비스별 개별 롤백 — nginx 가중치 0으로 즉시 복귀
3§3-3 진입 조건 4개 AND서비스별: ① 자기 스키마만 접근 ② 데이터 마이그레이션 5단계 검증 통과 ③ 롤백 리허설 1회 성공expand-contract라 코드 롤백으로 복귀

8-4. 2단계 서비스 추출 순서와 근거

한 번에 다 떼지 않는다. 위험이 낮은 것부터, 배운 것을 다음에 적용한다.

순서추출 서비스왜 이 순서인가위험롤백
1edge (virtualqueue + catalog·order 파사드 2 + 게이트웨이 + 일괄 인증)DB 권한 0 — 데이터 위험 없음 ② 무상태 ③ virtualqueue는 원격 호출 0건(HMAC 무상태) ④ 파사드는 이미 타임아웃·부분 저하 보유 ⑤ 실패해도 읽기 조합만 저하되고 거래 무영향 ⑥ dashboard 제외로 더 단순해졌다 — 원격 fan-out 대상이 30건→18건, platform 의존(domain.user)이 사라져 edge → platform 호출은 토큰 검증(C10) 하나뿐최저매우 쉬움
2payment소유 테이블 1개(payments) — 경계가 가장 명확 ② 아웃바운드 동기 호출 0건(순수 공급자) ③ 인바운드 4경로(C1~C4)가 전부 트랜잭션 밖이라 원격화가 안전(§5-2 검증) ④ 확정 경로는 이미 Kafka ⑤ 돈 경로지만 경계가 가장 단순해 조기 추출이 오히려 안전낮음쉬움 (라우팅)
3platform (+dashboard)① 코어 거래 경로 아님 ② notification·operator·mcp는 코어를 동기 호출하지 않는다(ADR-002 규칙 4, R3 강제) ③ T1이 서비스 내부로 흡수 ④ 단 dashboard 편입으로 부담이 늘었다 — platform이 코어를 읽는 아웃바운드 11건(C8) 을 갖게 돼 “아웃바운드 결합 구조적 0”이 더는 성립하지 않는다. 추출 시 dashboard 11건의 원격 전환 + TTL 캐시 검증이 추가된다 (R3 화이트리스트에 dashboard가 이미 등록돼 있어 규칙 위반은 아니다)낮음~중간쉬움
4social① 결합 밀도 최고 클러스터가 전부 내부화 ② 경계 넘는 호출이 C2(3건) 하나 ③ 단 STOMP WebSocket이 있어 Redis 릴레이 선행 필수(§9-3)중간 (WebSocket)중간
5facility-booking① 실측 조회 CPU 바운드 경로라 격리 효과 최대 ② 경계 넘는 호출은 C1(6건) ③ MongoDB 단독 사용으로 데이터 격리가 사실상 완료중간중간
6commerce (goods·ticketing)마지막 — 마케팅 스파이크 대상이라 위험 최대 ② 앞 5개가 안정화된 뒤 남은 것이 자연히 commerce가 된다(= 모놀리스가 commerce로 수축) ③ 이 시점엔 나머지가 모두 REST 클라이언트로 검증된 계약을 쓴다최고어려움

payment를 2번으로 올린 이유: B에서는 payment가 commerce에 포함돼 마지막이었다. 단독 분리하면 소유 테이블 1개 + 아웃바운드 0 + 트랜잭션 밖 인바운드라 오히려 가장 떼기 쉬운 서비스 중 하나가 된다. 돈 경로라서 뒤로 미루는 것은 위험이 아니라 경계 복잡도로 판단해야 한다. 조기 추출로 결제 계약을 먼저 안정화하면 이후 4개 추출이 그 계약을 재사용한다.

6번째는 사실상 “추출”이 아니라 모놀리스가 commerce로 수축 완료되는 것이다 — strangler fig의 정석이고, 가장 위험한 부분을 마지막에 두면서 별도 추출 작업 없이 끝난다.

8-5. 3단계 DB 분리 — 데이터 마이그레이션 5단계

서비스별로 순차 적용한다. 소유 테이블 수가 적은 순(payment 1 → facility-booking 4 → commerce 11 → social 11 → platform 18)으로 진행해 학습 비용을 낮춘다. Flyway 인라인 백필 DML 금지 — Flyway는 DDL 전용, 값 이관은 배치가 수행한다.

단계내용롤백 지점
1. 듀얼라이트신규 스키마에 테이블 생성(DDL, Flyway). 코드가 기존·신규 양쪽에 동시 기록신규를 아무도 읽지 않으므로 코드 되돌리기
2. 배치 백필Spring Batch 청크(PK 범위) 단위 이관. 청크마다 커밋, rate limit, 멱등(재실행 가능)배치 중단
3. 데이터 검증검증 배치로 “누락 0건 · 기존≠신규 0건” 판정. 아티팩트 보관동일
4. 읽기 전환신규 스키마 읽는 경로 배포 (피처 플래그 게이트)플래그 OFF
5. 배포 후 검증지표·샘플 확인 → 이상 시 플래그 OFF. 안정화 후 구 테이블 제거(별도 마이그레이션)플래그 OFF
  • Flyway 소유권 분할은 3단계에서만 — 서비스별 디렉토리·버전 접두사 분리(1단계에 나누면 번호 충돌).
  • MongoDB(facilities)는 이미 물리 분리 상태 — 3단계에서 전용 유저·권한 부여만.
  • payment가 가장 단순하다 — payments 1테이블이라 3단계 리허설 대상으로 최적.

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

flowchart LR
    subgraph S0["0·0.5단계"]
        A[교차 Repository 0]
        B[F1·F3 결함 처리]
    end
    subgraph S1["1단계 멀티모듈+선행인프라"]
        C[6경계 모듈]
        D[힙·풀·GRANT·RS256·릴레이]
    end
    subgraph S2["2단계 물리분리 (공유DB+GRANT)"]
        E1[1 edge]
        E2[2 payment]
        E3[3 platform]
        E4[4 social]
        E5[5 facility-booking]
        E6[6 commerce 수축]
    end
    subgraph S3["3단계 DB분리"]
        F[payment→…→platform]
    end
    A --> B
    B --> C
    C --> D
    D --> E1
    E1 --> E2
    E2 --> E3
    E3 --> E4
    E4 --> E5
    E5 --> E6
    E6 -->|진입조건 4개 AND| F

9. 무중단 전환

벤치마킹한 오늘의집 Branch By Abstraction + 트래픽 섀도잉을 여기서 실제로 쓴다.

9-1. 서비스 추출 1건의 전환 절차

단계내용클라이언트 영향롤백
A. 라우팅 계층 선행 도입nginx가 모든 경로를 모놀리스로 보내는 상태로 먼저 배치없음nginx 제거
B. 신규 서비스 병행 기동 (트래픽 0)추출 서비스를 같은 코드베이스에서 별 컨테이너로 띄운다. 라우팅은 아직 모놀리스없음컨테이너 중지
C. 섀도잉 검증대상 경로 요청을 신규 서비스에도 복제 전송하고 응답 본문·상태코드 불일치 0건 확인없음 (응답은 모놀리스 것 사용)섀도잉 중단
D. 점진 라우팅 전환nginx 가중치 전환 (10% → 50% → 100%)없음 (동일 계약)가중치 0으로 즉시 복귀
E. 모놀리스에서 해당 경로 제거100% 전환 + 1주 무사고 후 컨트롤러·빈 제거없음이전 태그 재기동
  • A~D 어느 지점에서든 nginx 가중치 조정만으로 즉시 롤백된다. 코드·DB 롤백이 필요 없다.
  • DB는 1단계에서 공유이므로 전환 중 데이터 정합 문제가 없다 — “서버 먼저, DB 나중” 2단계 전략의 최대 이점이다.
  • 섀도잉 도구: qa/load/k6 시나리오 11개를 재사용해 부하 상태의 응답 일치까지 검증한다.

9-2. 계약 호환 유지 규칙

규칙이유
추출 시 URL 경로·요청/응답 스키마를 바꾸지 않는다섀도잉 비교가 성립해야 한다. 리팩토링은 별 작업
서비스 간 계약 변경은 필드 추가만(optional)두 서비스 동시 배포 강제(분산 모놀리스 신호)를 막는다
파괴적 변경은 새 버전 경로(/v2/...)소비자 이관 후 구 경로 폐기
Kafka 스키마는 .v{N} 규칙 유지이미 준수 중

9-3. STOMP WebSocket — Redis pub/sub 릴레이를 social 추출 전제로 (OQ-7 확정)

message가 STOMP 실시간 세션을 갖는다(infrastructure/realtime/MessageBroadcastGatewayImpl.kt). 1 replica 수용이 아니라 릴레이를 선행 구축한다.

릴레이가 요구하는 것

항목요구사항예산 영향
방식Spring enableStompBrokerRelay(RabbitMQ/ActiveMQ 필요) 미채택기존 Redis pub/sub로 인스턴스 간 팬아웃. 각 인스턴스가 자기 로컬 WebSocket 세션에만 전달하고, 다른 인스턴스로 갈 메시지는 Redis 채널로 발행·구독신규 컨테이너 0 — 이것이 이 방식을 택하는 이유
Redis 채널방(room) 단위 또는 단일 브로드캐스트 채널 + 인스턴스 로컬 필터. 채널 네임스페이스는 §3-2 허용 ⑤ 규칙에 따라 social 전용 prefix메모리 0 (pub/sub은 메시지를 저장하지 않아 maxmemory 미소비)
Redis 설정 검토client-output-buffer-limit pubsub — 느린 구독자가 있으면 Redis가 연결을 끊는다. 현재 maxmemory-policy noeviction이므로 pub/sub 버퍼가 메모리를 압박하지 않는지 확인 필요설정 변경만
세션 어피니티불필요 — WebSocket은 지속 TCP 연결이라 업그레이드가 성립한 인스턴스에 자연히 고정된다. nginx ip_hash는 선택 사항없음
멱등·순서같은 메시지가 발신 인스턴스에서 로컬 전달 + Redis 구독으로 중복 수신될 수 있다 → 발신 인스턴스 식별자로 자기 발행 메시지 무시 필요설계 항목
검증인스턴스 A에 접속한 사용자가 보낸 메시지가 인스턴스 B에 접속한 사용자에게 도달하는지 (2 replica 기동 상태)1단계 완료 판정 ⑦

전환 시 처리

항목처리
전환 중 기존 연결모놀리스의 기존 연결은 유지하고 신규 연결만 social로 보낸다. 기존 연결은 자연 종료 대기
롤백라우팅 되돌리면 클라이언트가 재연결 (앱이 이미 재연결 로직 보유)
social replica릴레이 검증 통과 후 2 replica 가능 — §7-4 목표 구성이 이를 전제로 산정됨

10. Open Questions

10-1. 결정 완료 (본 개정에 반영)

OQ결정반영 위치
OQ-1Docker VM 15.6 GiB 유지 (상향 안 함)§7-1, §7-4 (80.5% 판정·여유 3.04 GiB)
OQ-2-Xmx·mem_limit·cpus1단계 필수§7-2 (6개면 23.4 GiB OOM), §11 W1-02
OQ-33단계 DB 분리까지 완주. 1단계 종착 선택지 삭제. GRANT는 2단계 진입 전 안전장치§3 서두, §3-3
OQ-4edge 일괄 인증(방안 b) + JWT RS256 전환 채택§6-3, §11 W1-05·W1-06
OQ-6분리 전 결함 처리 — F1·F3를 0.5단계로 승격§8-1, §8-2
OQ-7Redis pub/sub 릴레이 선행 구축social 추출 전제§9-3, §7-3(예산 0), §11 W1-07
OQ-8payment 단독 분리 → 6 서비스§1-4, §2-1, §4-1, §5, §7-4, §8-4
OQ-9platform 6서비스 유지 (identity + ops로 쪼개지 않음)§1-4 — dashboard 편입으로 421파일/13,132라인(전체 35%)이 되나 저부하라 격리 필요가 낮다
OQ-10edge의 파사드·virtualqueue 동거 유지§1-4 근거 3 — 전용 스레드풀 벌크헤드 기보유. dashboard 제외로 edge가 55파일로 더 작아져 동거 부담이 줄었다
OQ-11최소 구성(6 인스턴스, 60%)으로 시작 후 관측하며 증설§7-4 — 목표 구성(10 인스턴스, 80.5%)은 관측 근거가 쌓인 뒤 단계적으로
D1 (신규)dashboardedge 아님 platform에 배치platform ↔ edge 순환 해소§1-3-1, §1-4, §4-1 C8·C8-i, §7-4, §8-4

10-2. 조사 위임 (별도 티켓)

#항목배경
OQ-5Kafka 유휴 CPU 66% · 메모리 1.17 GiB 원인 규명6 서비스 예산에서 최대 단일 인프라 소비자다. 1.17 GiB는 목표 구성 여유(3.04 GiB)의 38%에 해당한다. 원인 규명·튜닝으로 여유를 확보할 수 있다

10-3. 잔여 미결

#질문배경선택지
OQ-12 (신규)platform이 dashboard 편입으로 아웃바운드 11건을 갖게 된 것을 수용할 것인가?ADR-002 규칙 4는 “지원·서브시스템은 코어를 동기 호출하지 않는다”이고, dashboard는 R3 화이트리스트 예외로 이미 등록돼 있다(읽기 전용 Conformist). 따라서 규칙 위반은 아니나, platform 추출 시 코어 2서비스로 원격 읽기 11건이 생겨 §8-4 위험도가 “낮음 → 낮음~중간”으로 올랐다. 대안은 dashboard를 7번째 서비스로 독립시키는 것인데 예산(+650 MiB)과 서비스 수가 늘어난다platform 편입 수용(권장 — 순환 해소가 우선) / dashboard 독립(7서비스)

OQ-9(platform 유지)·OQ-10(edge 동거)·OQ-11(최소 구성 시작)은 §10-1로 이동해 결정 완료 처리했다.


11. 1단계 실행 작업 항목 (티켓 분해 입력)

private-senior-be가 티켓으로 분해할 입력이다. 본 문서는 항목·의존·병렬성만 정의하고 티켓은 쓰지 않는다.

11-1. 작업 항목 목록

ID작업산출물근거
W1-01Gradle 멀티모듈 6경계 분할commerce / payment / facility-booking / social / platform(+dashboard) / edge(virtualqueue·catalog·order·image) + common + bootstrap. 모듈 간 의존은 domain interface + 이벤트만. dashboard 7파일은 platform 모듈에 배치(순환 해소, §1-3-1)settings.gradle.kts include, 모듈별 build.gradle.kts§1-3-1, §1-4
W1-02JVM 힙·메모리·CPU 상한 명시 — 서비스별 -Xmx(§7-4 표), mem_limit, cpus(§7-5 표)Dockerfile JAVA_OPTS 또는 compose env, compose deploy.resources§7-2 — 미설정 시 6서비스 23.4 GiB OOM
W1-03HikariCP 차등 배분 + max_connections 500 — 서비스별 풀 크기(§7-6 표), connection-timeout 30s→5sapplication.yml, MySQL 설정실측 병목 ②, §7-6
W1-04서비스별 DB 유저 + GRANT 스크립트 + 권한 거부 테스트 6건GRANT SQL, 테스트§3-1
W1-05JWT RS256 전환 — platform 사설키 서명, 나머지 5개 공개키 검증. 키 배포 경로JwtTokenProvider 개편, 키 배포 설정§6-3, OQ-4
W1-06edge 일괄 인증PartnerApiKeyAuthenticationFilter·McpTokenAuthenticationFilter를 edge로 이동, edge→platform 원격 검증(C10), 내부 헤더 전파필터 재배치, platform 검증 API§6-3, 7번째 결합 해소
W1-07Redis pub/sub STOMP 릴레이 — 인스턴스 간 메시지 팬아웃, 자기 발행 무시, client-output-buffer-limit pubsub 검토릴레이 컴포넌트, Redis 설정§9-3, OQ-7 — social 2 replica 전제
W1-08nginx 라우팅 계층 선행 도입 — 전 경로를 모놀리스로 보내는 상태로 배치 (§9-1 A단계)nginx conf§9-1
W1-09서비스별 service.name 리소스 속성 — 분산 추적이 경계를 넘어 이어지도록otel 설정§6-1
W1-10서비스 간 계약 테스트 골격 — OpenAPI 스냅샷 + 소비자 측 계약 테스트계약 테스트§4-3
W1-11PENDING 만료 스위퍼 — 결제 개시 원격 실패 시 고립 주문 취소 (F-A)스케줄러§5-4 F-A

선행 (1단계 아님, 이미 정의됨): 0단계 permissions 이관(PH0-05)·McpTokenAuthenticationFilter 결합(W1-06이 해소) / 0.5단계 F1·F3 결함(§8-2).

11-2. 의존 관계와 병렬 가능성

ID선행 의존수정 파일 충돌 위험
W1-01 (a/b/c/d 4단)0.5단계 완료settings.gradle.kts·모듈별 build.gradle.kts·단계별 소스 패키지 이동. 모든 후행 작업의 병목
W1-02W1-01 단계 진행 중 삽입 가능Dockerfile, compose (단독 소유)
W1-03동일application.yml (W1-05와 충돌 가능 — 아래)
W1-04동일GRANT SQL (신규 파일)
W1-05동일infrastructure/security/JwtTokenProvider.kt
W1-06W1-05 (RS256 확정 후 필터 재배치)infrastructure/security/*Filter.kt, SecurityConfig.kt
W1-07W1-01 social 단계 이후infrastructure/realtime/*, Redis 설정
W1-08W1-02 (컨테이너 정의 확정 후)nginx conf (신규)
W1-09W1-01otel 설정 (단독)
W1-10W1-01테스트 (신규)
W1-11W1-01 commerce 단계 이후application/goods/** 스케줄러 (신규)

11-3. Wave 구성 — 시니어 실측으로 개정 (본 문서의 추정 폐기)

⚠️ 개정 2 정정: 개정 1의 “wave 1 / 8 / 2, 평균 너비 3.7” 추정은 성립하지 않는다. private-senior-be가 실제 분해하며 반박한 근거가 타당하다 — 후행 티켓 대부분이 Kotlin 소스를 만지므로, 전 소스를 이동하는 W1-01과 병행하면 머지 충돌이 확정된다. 본 아키텍트 추정보다 시니어의 실측 분해가 정확하므로 그것으로 대체한다.

시니어 실측 분해 결과:

항목
티켓 수23
wave 수10
평균 wave 너비2.3
W1-01 처리4단(a/b/c/d)으로 분할해 각 단 사이에 다른 티켓을 끼워 평균 3.0 확보
위상 순서 (실측 교차 import 78파일 기준)commonpayment → {commerce, facility-booking} → platformsocialedgebootstrap

위상 순서의 타당성 검증 (본 문서 데이터와 대조):

순서근거 정합
common 최초전 컨텍스트가 domain.common을 참조(§선행 진단: ErrorStatus·BusinessException 119회 등). 공유 커널이 먼저 서야 한다
payment 2번째아웃바운드 동기 호출 0건(§1-2 관찰 1) — 아무것도 의존하지 않으므로 common 직후가 맞다
{commerce, facility-booking} 병렬둘 다 payment만 의존(C1·C3·C4)하고 서로 의존하지 않는다 → 병렬 가능
platform 5번째dashboard 편입으로 commerce·facility-booking을 읽는다(C8, 11건) → 그 둘 뒤에 와야 한다. D1 결정이 이 위상 순서를 만든 원인이다
social 6번째payment(C2) 의존 + community·message·post·recruitment 내부 정리
edge 7번째commerce·facility-booking·social을 fan-out(C6·C7) + platform 토큰 검증(C10) → 가장 마지막 도메인 모듈
bootstrap 최후전 모듈을 조립

D1 결정이 위상 순서에 미친 영향: dashboard가 edge에 있었다면 platformedge보다 먼저 와야 하는 동시에 edgeplatform을 참조해야 해서 위상정렬이 불가능했다(§1-3-1 순환). dashboard를 platform으로 옮기면서 platform → {commerce, facility-booking} 단방향이 되어 위상 순서가 성립했다.

남은 주의 사항:

  • 파일 충돌 1건: **W1-03(HikariCP application.yml)과 W1-05(JWT 설정)**이 같은 application.yml을 만질 수 있다. W1-01로 서비스별 yml이 갈리면 해소되나, 분해 시 Single Writer per File 검증이 필요하다.
  • 평균 너비 2.3(W1-01 분할 후 3.0)은 직선형 DAG가 아니다private-ticket 게이트(“모든 wave 너비 1~2면 분해 실패”)를 통과한다. wave 1이 좁은 것은 멀티모듈 분할의 성격상 불가피하다.

Document History

날짜변경 내용
2026-07-30개정 2 — D1 결정(dashboard→platform) + 총계 정정 + wave 실측 반영. ① 모듈 순환 실측 확인: presentation/mcp/controller/McpOperatorProfileTools.kt:3,4application.dashboard(platform→edge) ↔ application/dashboard/usecase/GetMyDashboardSummaryUseCase.kt:9domain.user.service.UserDomainService(edge→platform). dashboard 7파일(app 5/170라인 + presentation 2/97라인 = 267라인)을 platform에 배치 — §1-3-1 신설(순환 근거·방안 a 채택 근거 3항: 순환 해소·@Cacheable 기보유(:10)·operator 대면), 후보명 B′ → B″. 규모 실측: edge 62→55파일/2,591→2,324라인, platform 414→421파일/12,865→13,132라인. ③ 총계 산술 오류 정정: 초판·개정 1의 “22쌍/48 import”는 오류 — 직접 카운트 결과 22쌍/62 import(쌍 목록·건수는 정확, 합계만 틀림). 파사드 점유율 62%→48%(30/62), 후보별 경계 넘는 import 재산정(A 45 / B 39 / B′ 46 / B″ 45 / C 32) + §4-1 검산표(경계 45 + 내부화 17 = 62) 신설. ④ §4-1 C8 개정platform(dashboard) → commerce 7 + facility-booking 4 = 11건(user 1건은 C8-i로 내부화). §4-4 토폴로지에 platform→코어 dashboard 엣지 추가. ⑤ §7-4 — dashboard 이동 예산 영향 무시 수준(267라인 0.7%, 힙 구간 불변, 합계 4,850 MiB 불변) 명시. ⑥ §8-4 추출 순서 재평가edge(1번)는 fan-out 30→18건·platform 의존이 토큰 검증 1건뿐이 되어 더 단순해짐, platform(3번)은 아웃바운드 11건 발생으로 위험 낮음→낮음~중간 상향(R3 화이트리스트라 규칙 위반 아님). ⑦ §11-3 wave 구성 전면 개정 — 개정 1의 추정(wave 1/8/2, 평균 3.7)을 폐기하고 시니어 실측(23티켓 / 10 wave / 평균 2.3, W1-01 4단 분할로 3.0 확보, 위상 순서 common→payment→{commerce, facility-booking}→platform→social→edge→bootstrap)으로 대체. 위상 순서를 본 문서 데이터로 검증(payment 아웃바운드 0 → 2번째 / platform이 dashboard로 코어를 읽어 코어 뒤 / D1이 위상정렬 성립의 원인임을 명시). ⑧ OQ 정리 — OQ-9·10·11을 결정 완료(§10-1)로 이동, D1 등록, OQ-12 신설(platform 아웃바운드 11건 수용 vs dashboard 7번째 서비스 독립)
2026-07-28개정 1 — 사용자 결정 4건 반영. 기준 커밋 ed22bd1589ae6c24 갱신. 0단계 정정: PH0-04로 RecipientContactResolver 해소(교차 Repository 6/6 완료), 7번째 결합 발견·등록(McpTokenAuthenticationFilter.kt:23,24,25McpTokenRepository·McpTokenScopeRepository·PermissionRepository 직접 주입, SecurityConfig.kt:36 등록) 및 OQ-4로 해소되는 경로 명시. OQ-8 → 후보 B′(6 서비스, payment 단독) — §1-3 후보표에 B′ 추가, §1-4 채택 근거(돈 경로 격리 우선, 경계 넘는 import 39→46), §2-1 재배분(commerce 11 / payment 1), §4-1 통신 전수 재작성(C1C11, 결제 개시 4경로 원격화), §5-2 SAGA 재검증 — 결제 개시 4종 @Transactional 0건을 import 존재 여부로 애노테이션/주석 구분해 실측(ApplyRecruitmentUseCase는 KDoc 텍스트였음) → 신규 SAGA 0건 유지 확인. OQ-1 → VM 15.6 GiB 고정, §7-4를 최소(6인스턴스 9.36 GiB / 60%)·목표(10인스턴스 12.56 GiB / 80.5%) 2안으로 재산정, replica 여유 4.7→3.04 GiB·추가 인스턴스 ~5→~3개 수치화, payment 분리 비용(1,419라인=3.8%에 1,300 MiB=예산 8.3%) 명시, 힙 축소 우선순위 제시. OQ-6 → 0.5단계 신설(§8-2 F1 슬롯누수 643건·F3 응답불일치의 재현 경로·분리 후 추적 곤란 사유, 조치는 티켓 위임). OQ-7 → Redis pub/sub 릴레이를 social 추출 전제로(§9-3 요구사항 6항목, 예산 영향 신규 컨테이너 0, RabbitMQ 방식 미채택 근거). OQ-2·3·4 권장안 확정 반영(1단계 종착 선택지 삭제, GRANT를 2단계 안전장치로 재배치, RS256+edge 일괄 인증). OQ-5 조사 위임 등록. §8-4 추출 순서 개정 — payment를 2번으로(소유 테이블 1·아웃바운드 0·트랜잭션 밖 인바운드라 경계 최단순), commerce 수축 6번 유지. §11 신설 — 1단계 작업 11항목(W1-0111) + 의존·병렬 wave 구성(1/8/2, 평균 3.7) + 파일 충돌 주의 1건. OQ-9~11 신규
2026-07-28최초 작성 — 물리 분리 실행설계 (5 서비스 후보 B 기준). 서비스 경계 3안 비교, 45테이블 소유권, GRANT 물리 차단, 통신 8건, SAGA 0건, 인프라 판정, 리소스 예산, 추출 순서, 무중단 전환