사용자가 물리적으로 분리된 서비스를 목적지로 확정했다. 멀티모듈은 목적지가 아니라 경유지다.
항목
확정
목적지
별도 배포 단위로 쪼개진 서비스 (모듈러 모놀리스는 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.md — AS-IS 실측 근거는 그 문서가 SSOT다. 본 문서는 그 실측 위에 “어떻게 안전하게 물리 분리할 것인가”만 다룬다.
본 문서는 분리 여부를 재논쟁하지 않는다. 단 물리적으로 불가능하거나 예산이 빡빡한 부분은 수치로 명시한다(§7).
트리 신선도 게이트 (설계 기준 커밋)
항목
값
HEAD
89ae6c240314fb85b03067d78239c5b63fef99de
origin/main...HEAD
0 / 0 (detached at origin/main)
워킹트리
clean
개정 전 기준
ed22bd15 → PH0-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 직접 주입: :23McpTokenRepository, :24McpTokenScopeRepository, :25PermissionRepository. import는 :6·:7·:3. SecurityConfig.kt:36에 필터 체인 등록
미머지. 관련 잔재: infrastructure/user/mysql/PermissionRepositoryImpl.kt:4가 domain.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 → message
6
order → ticketing / recruitment / goods / booking
2 / 2 / 2 / 2
booking → payment
6
facility → booking
2
post → community
5
catalog → ticketing / recruitment / facility
2 / 2 / 2
goods → payment
4
recruitment → community
2
dashboard → ticketing
4
partner → user
1
catalog → goods
4
dashboard → user / facility
1 / 1
ticketing → payment
3
recruitment → payment
3
dashboard → goods / booking
3 / 3
⚠️ 총계 정정 (개정 2): 초판·개정 1이 “22쌍 / 48 import”로 기술했으나 산술 오류였다. 89ae6c24에서 직접 카운트한 결과 총 62 import / 고유 22쌍이다(위 표의 건수 합 = 62). 쌍 목록·건수는 정확했고 합계만 틀렸다. 아래 모든 비율·차집합 계산을 62 기준으로 재산정했다.
핵심 관찰 2건:
payment 인바운드 16건(booking 6 + goods 4 + ticketing 3 + recruitment 3), 아웃바운드 0건. payment는 순수 공급자다 — 단독 분리 시 원격 인바운드 16건, 원격 아웃바운드 0건이라 의존 방향이 단순하다.
파사드 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 포함)
application/dashboard/usecase/GetMyDashboardSummaryUseCase.kt:9 → domain.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를 소비한다
사용자가 “돈 경로 완전 격리”를 결합 밀도보다 우선했다. 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단계 멀티모듈화 자체가 성립하지 않는다.
payment 격리의 실질 이득: ① payment 장애가 조회·커뮤니티·시설을 죽이지 않음 ② PG 연동 변경이 commerce 배포와 무관 ③ payment만 별도 스케일·자원 상한 ④ 45테이블 중 payments 1개만 소유해 2단계 DB 분리가 가장 단순.
edge 유지 근거: 남은 파사드 2개(catalog·order)는 이미 전용 스레드풀로 벌크헤드돼 있다 — CatalogAsyncConfig.kt:18-25(core 4 / max 8 / queue 50), OrderHistoryAsyncConfig.kt:20. virtualqueue와 동거해도 스레드 격리가 구현된 상태다 (OQ-10 동거 확정).
비용은 메모리다 — 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
없음 — 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. 소유권이 불명확하거나 다투는 테이블 — 전수
#
테이블
문제
판정·조치
D1
permissions
domain/common이 소유(domain/common/Permission.kt:11) — 공유 커널이 물리 테이블을 가짐. infrastructure/user/mysql/PermissionRepositoryImpl.kt:4가 구현
platform으로 확정. PH0-05 이관 완료가 2단계 진입 선행 조건
D2
BATCH_JOB_INSTANCE 등 Spring Batch 메타데이터
Flyway 관리 밖 — infrastructure/config/BatchMetadataSchemaInitializer.kt:32가 런타임 생성. 45테이블 집계에 없음
commerce 단독 소유. Batch 사용처는 infrastructure/goods/batch/SellerTypeBackfillJobConfig.kt1곳뿐(grep 전수). Initializer도 commerce로 이동
D3
regions
facility 소유이나 지역 코드 참조 테이블 성격
facility-booking 소유 유지. 현재 교차 소비 0건
D4
users
모든 서비스가 userId 보유. 표시명·연락처 필요 가능
platform 소유.PH0-04가 이미 ACL 게이트웨이로 전환 — 교차 필요는 이벤트 payload 동봉 또는 원격 조회
D5
feature_flags
모든 서비스가 플래그를 읽는다 (FeatureFlagEvaluator가 domain/common)
platform 소유(쓰기) + 전 서비스 읽기는 Redis 캐시(RedisFeatureFlagCacheStore). DB 직접 읽기 금지
D6
mcp_tokensmcp_token_scopespermissions
McpTokenAuthenticationFilter.kt:23,24,25가 3개 Repository 직접 주입 — 전 서비스 공통 인증 체인 (7번째 결합)
platform 소유.OQ-4 결정으로 해소 — 필터를 edge에만 두고 edge → platform 원격 검증 (§6-3, §11 W1-06)
D7
MongoDB facilities
facility-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로 물리 차단이 가능하다.
지금 걸 수 있는 이유: 컨텍스트 교차 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 2
social (13)
facility → booking 2
facility-booking (2)
partner → user 1 · dashboard → user 1
platform (2)
#
호출
import
성격
채택 방식
근거
C1
facility-booking(booking) → payment 결제 개시
6
쓰기 트리거 (동기 필요 — 사용자가 결제창을 즉시 받아야 함)
원격 REST
CreateBookingUseCase@Transactional0건(import조차 없음) → 이미 트랜잭션 분리
C2
social(recruitment) → payment 결제 개시
3
동일
원격 REST
ApplyRecruitmentUseCase@Transactional0건. KDoc :18 “PG 네트워크 호출을 감싸지 않도록 클래스 레벨 @Transactional을 두지 않는다”
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 필요?
T1
CreatePartnerUseCase — @Transactional에 user 쓰기 + partner 쓰기
platform 내부 (user·partner 동일 서비스)
불필요 — 로컬 트랜잭션 유지
T2
CreateProgramSessionUseCase — facility 읽기 + booking 쓰기
facility-booking 내부
불필요
T3
CreateCommunityPostUseCase — community 인가 읽기 + post 쓰기
social 내부
불필요
T4
AddCommentUseCase:22 — requireActiveMember + post 쓰기
social 내부
불필요
T5
결제 개시 4종
4건 전부 서비스 경계를 넘는다 (C1~C4) — B에서 내부였던 goods·ticketing도 원격화
불필요 — 아래 5-2에서 검증
T6
결제 확정 (payment → 주문 4종)
이미 Kafka 이벤트 + eventId 멱등
불필요 — 이미 코레오그래피 SAGA다
T7
Gateway 6개 구현
PH0-0104 완료. 경계 넘는 것은 C1C4·C6~C8
읽기·트리거라 불필요
5-2. T5 재검증 — 단정하지 않고 실측했다
payment 단독 분리로 결제 개시 4종이 전부 원격 호출이 된다. 새 SAGA가 생기는지 4개 파일 전수 검증했다 (89ae6c24).
현재 JWT는 HS256 대칭키다 — JwtTokenProvider.kt:16@Value("\${app.jwt.secret}"), :20Keys.hmacShaKeyFor(...). 6개 서비스가 같은 secret을 공유하면 어느 한 서비스가 뚫려도 토큰 위조가 가능해진다.
확정 설계
항목
결정
파트너·MCP 인증
edge가 일괄 수행 — edge만 platform에 토큰 검증을 원격 요청(C10)하고, 검증된 신원을 내부 헤더로 전파한다. 6개 서비스가 platform 테이블을 읽지 않으므로 GRANT와 충돌하지 않는다
JWT 검증
각 서비스 자체 검증 유지 (원격 홉 0) — 단 RS256 비대칭으로 전환. platform만 사설키로 서명하고, 나머지 5개는 공개키로 검증만 한다
컨테이너 무제한 → JVM이 VM 전체 15.6 GiB의 25% ≈ 3.9 GiB를 max heap으로 인식
실측 리포트가 쓴 값
Xmx 768m + mem_limit 1.8g → 컨테이너당 ~950 MB
git-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).
관측 스택 (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)
commerce
7,360
640m
920
1
920
payment
1,419
384m
650
1
650
facility-booking
5,695
640m
920
1
920
social
7,898
512m
790
1
790
platform (+dashboard)
13,132
640m
920
1
920
edge (dashboard 제외)
2,324
384m
650
1
650
합계
37,828
6
4,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.62
4.74
9.36 GiB
60%
✅ 여유 6.2 GiB
목표 구성 (핫 서비스 2 replica)
서비스
-Xmx
컨테이너
replica
소계 (MiB)
replica 근거
commerce
640m
920
2
1,840
마케팅 스파이크 대상
payment
384m
650
2
1,300
돈 경로 — 단일 장애점 회피
facility-booking
640m
920
2
1,840
실측 조회 CPU 바운드 경로
social
512m
790
2
1,580
릴레이 구축 후에만 가능(§9-3)
platform
640m
920
1
920
저부하
edge
384m
650
1
650
무상태 (필요 시 증설 쉬움)
합계
10
8,130 ≈ 7.94 GiB
인프라
JVM
합계
15.6 GiB 대비
판정
목표 구성
4.62
7.94
12.56 GiB
80.5%
⚠️ 들어가나 빡빡하다 — 여유 3.04 GiB
참고: 5서비스 목표(B, 7인스턴스)
4.62
6.28
10.90 GiB
70%
여유 4.7 GiB
참고: 현재 모놀리스 4 replica
4.62
3.80
8.42 GiB
54%
기준선
판정과 replica 여유 감소 (수치)
항목
5서비스(B)
6서비스(B″)
차이
목표 구성 사용량
10.90 GiB
12.56 GiB
+1.66 GiB
VM 사용률
70%
80.5%
+10.5pt
남은 여유
4.7 GiB
3.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 판정 — 처리량 이득 없음 (격리 이득만)
항목
실측
함의
호스트 CPU
10코어
고정
모놀리스 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 상한
근거
commerce
2.5
스파이크 대상
facility-booking
2.5
실측 CPU 바운드 조회
social
1.5
WebSocket + 게시·모집
payment
1.0
PG 왕복 대기가 지배적(CPU 소비 적음)
platform
1.0
저부하
edge
1.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
소계
commerce
40
2
80
facility-booking
30
2
60
payment
20
2
40
social
25
2
50
platform
20
1
20
edge
0
1
0 (DB 권한 없음, §3-1)
합계
250 ≤ 500 ✓
실측 병목 ②(HikariCP 고갈)가 분리 후에는 서비스별로 쪼개져 관측된다 — 한 서비스가 풀을 다 먹어도 다른 서비스 풀은 남는다. 이것 자체가 격리 이득이다.
8. 단계별 실행 계획
8-1. 단계 개요
단계
내용
배포 단위
DB
0 (진행 중)
경계 위생 — 교차 Repository 제거(6/6 완료)·분류 정합(완료)·permissions 이관(PR)
물리 분리 착수 전에 처리한다. 조치 방법은 티켓으로 위임하고, 여기서는 무엇을·왜 먼저만 정의한다.
결함
실측 내용
재현 경로
왜 분리 전이어야 하는가
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 수 대조
① 이 경로가 곧 commerce ↔ payment 경계가 된다 — 분리 후에는 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 주문 수(응답 일치). 결과를 아티팩트로 남긴다.
① infrastructure 교차 Repository 주입 0건 — 현재 1건 잔존(McpTokenAuthenticationFilter.kt:23,24,25, 7번째 결합) ② DomainClassification 20/20 (완료) ③ permissions → user 이관 머지 ④ ArchUnit 전체 GREEN
코드 되돌리기 (스키마 무변경)
0.5
0단계 완료
§8-2 완료 판정 2건 + 아티팩트
코드 되돌리기
1
0.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·릴레이는 플래그/설정 롤백
2
1단계 완료
서비스 추출마다: ① healthy + /actuator/health UP ② 기존 API 응답 동일(섀도잉 비교, §9) ③ E2E 10 spec GREEN ④ 남의 테이블 SELECT → 권한 거부 확인 ⑤ 분산 추적에 경계 트레이스 연결 확인
서비스별 개별 롤백 — nginx 가중치 0으로 즉시 복귀
3
§3-3 진입 조건 4개 AND
서비스별: ① 자기 스키마만 접근 ② 데이터 마이그레이션 5단계 검증 통과 ③ 롤백 리허설 1회 성공
① DB 권한 0 — 데이터 위험 없음 ② 무상태 ③ virtualqueue는 원격 호출 0건(HMAC 무상태) ④ 파사드는 이미 타임아웃·부분 저하 보유 ⑤ 실패해도 읽기 조합만 저하되고 거래 무영향 ⑥ dashboard 제외로 더 단순해졌다 — 원격 fan-out 대상이 30건→18건, platform 의존(domain.user)이 사라져 edge → platform 호출은 토큰 검증(C10) 하나뿐
최저
매우 쉬움
2
payment
① 소유 테이블 1개(payments) — 경계가 가장 명확 ② 아웃바운드 동기 호출 0건(순수 공급자) ③ 인바운드 4경로(C1~C4)가 전부 트랜잭션 밖이라 원격화가 안전(§5-2 검증) ④ 확정 경로는 이미 Kafka ⑤ 돈 경로지만 경계가 가장 단순해 조기 추출이 오히려 안전
낮음
쉬움 (라우팅)
3
platform (+dashboard)
① 코어 거래 경로 아님 ② notification·operator·mcp는 코어를 동기 호출하지 않는다(ADR-002 규칙 4, R3 강제) ③ T1이 서비스 내부로 흡수 ④ 단 dashboard 편입으로 부담이 늘었다 — platform이 코어를 읽는 아웃바운드 11건(C8) 을 갖게 돼 “아웃바운드 결합 구조적 0”이 더는 성립하지 않는다. 추출 시 dashboard 11건의 원격 전환 + TTL 캐시 검증이 추가된다 (R3 화이트리스트에 dashboard가 이미 등록돼 있어 규칙 위반은 아니다)
낮음~중간
쉬움
4
social
① 결합 밀도 최고 클러스터가 전부 내부화 ② 경계 넘는 호출이 C2(3건) 하나 ③ 단 STOMP WebSocket이 있어 Redis 릴레이 선행 필수(§9-3)
중간 (WebSocket)
중간
5
facility-booking
① 실측 조회 CPU 바운드 경로라 격리 효과 최대 ② 경계 넘는 호출은 C1(6건) ③ MongoDB 단독 사용으로 데이터 격리가 사실상 완료
중간
중간
6
commerce (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. 안정화 후 구 테이블 제거(별도 마이그레이션)
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-1
Docker VM 15.6 GiB 유지 (상향 안 함)
§7-1, §7-4 (80.5% 판정·여유 3.04 GiB)
OQ-2
-Xmx·mem_limit·cpus → 1단계 필수
§7-2 (6개면 23.4 GiB OOM), §11 W1-02
OQ-3
3단계 DB 분리까지 완주. 1단계 종착 선택지 삭제. GRANT는 2단계 진입 전 안전장치
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-01
Gradle 멀티모듈 6경계 분할 — commerce / payment / facility-booking / social / platform(+dashboard) / edge(virtualqueue·catalog·order·image) + common + bootstrap. 모듈 간 의존은 domain interface + 이벤트만. dashboard 7파일은 platform 모듈에 배치(순환 해소, §1-3-1)
⚠️ 개정 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파일 기준)
common → payment → {commerce, facility-booking} → platform → social → edge → bootstrap
위상 순서의 타당성 검증 (본 문서 데이터와 대조):
순서
근거 정합
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에 있었다면 platform이 edge보다 먼저 와야 하는 동시에 edge가 platform을 참조해야 해서 위상정렬이 불가능했다(§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,4 → application.dashboard(platform→edge) ↔ application/dashboard/usecase/GetMyDashboardSummaryUseCase.kt:9 → domain.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건 반영. 기준 커밋 ed22bd15 → 89ae6c24 갱신. 0단계 정정: PH0-04로 RecipientContactResolver 해소(교차 Repository 6/6 완료), 7번째 결합 발견·등록(McpTokenAuthenticationFilter.kt:23,24,25 — McpTokenRepository·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건, 인프라 판정, 리소스 예산, 추출 순서, 무중단 전환