이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- MSA가 실제로는 분산 Monolith가 되었는지 어떤 신호로 판단하는가?
- 실패한 MSA는 성능, 정합성, 장애 격리, 운영 복잡도를 어떻게 악화시키는가?
- 서비스 수를 줄이거나 경계를 다시 잡아야 하는 경우는 언제인가?
개요
MSA 실패는 service 수가 많아서 생기는 문제가 아니다. 서비스를 나눴지만 데이터, 배포, 장애, owner가 분리되지 않았을 때 생긴다. 겉으로는 여러 service가 있지만 실제로는 하나처럼 같이 배포되고, 같은 DB를 고치며, 하나의 요청이 긴 동기 호출 사슬을 지난다. 이 상태를 분산 Monolith라고 부를 수 있다. 분산 Monolith는 Monolith의 단순함은 잃고 MSA의 운영 비용은 얻는 구조다.
이 문서가 바꾸는 설계 축
| 축 | 실패 시 악화되는 내용 |
|---|---|
| 성능 | network hop과 fanout으로 p95 latency가 늘고 timeout이 전파된다. |
| 정합성 | shared DB와 중복 owner로 schema 변경과 데이터 충돌이 늘어난다. |
| 장애 격리 | 한 service 지연이 호출 사슬 전체를 막아 장애가 더 빨리 퍼진다. |
| 운영 복잡도 | tracing, deploy, rollback, on-call은 늘지만 책임 경계는 흐리다. |
대표 시나리오
서비스가 7개로 나뉘었다고 하자. 하지만 다음과 같이 운영된다.
order-api -> member-service -> coupon-service -> payment-service -> point-service
모든 service가 commerce_db의 members, orders, payments table을 직접 조회
배포는 매주 목요일 전체 service 동시 배포
장애 알림은 공통 Slack 채널 하나
trace id는 gateway에서만 기록이 구조는 이름만 MSA다. 장애가 나면 어느 service가 owner인지 모른다. Schema 변경은 모든 service에 영향을 주고, timeout은 호출 사슬을 따라 전파된다.
실패 신호
| 신호 | 의미 | 확인 지표 |
|---|---|---|
| shared DB write | 데이터 owner가 없다. | table별 writer service 수 |
| sync fanout 과다 | latency와 partial failure가 누적된다. | API별 downstream call count |
| 전체 동시 배포 | 독립 배포가 없다. | deploy batch size, deploy dependency |
| 공통 on-call | 책임 경계가 없다. | service별 owner/on-call 유무 |
| contract test 부재 | API 변경이 소비자를 깨뜨린다. | consumer failure after deploy |
| tracing 부재 | 장애 위치를 찾기 어렵다. | trace coverage |
| 공통 library 과다 | 모든 service가 같은 변경에 묶인다. | common module change blast |
| 이 표는 비난이 아니라 구조를 다시 잡기 위한 진단표다. |
Shared DB 증상
가장 위험한 실패 신호는 shared DB다.
-- order-service
update members set point = point - :amount where id = :memberId;
-- point-service
update members set point = point + :amount where id = :memberId;두 service가 같은 table을 수정하면 point owner가 없다. Schema 변경도 독립적으로 할 수 없다. 해결은 service별 DB를 무조건 바로 나누는 것이 아니다. 먼저 writer를 하나로 정하고, 다른 service는 API나 event projection으로 접근하게 바꾼다.
Sync Fanout 증상
MSA 실패는 p95 latency에서 드러난다.
gateway timeout budget = 2,000ms
order-api -> member-service p95 = 180ms
order-api -> coupon-service p95 = 260ms
order-api -> payment-service p95 = 700ms
order-api -> point-service p95 = 240ms
serialization/network overhead = 150ms평균은 괜찮아도 한 downstream이 느려지면 전체 API가 실패한다. Retry가 겹치면 더 악화된다. 사용자 응답에 꼭 필요한 호출만 동기로 남기고, 알림, 적립, analytics는 event로 분리한다.
배포 독립성 부재
서비스가 나뉘었는데 항상 같이 배포한다면 분리 효과가 약하다.
| 증상 | 원인 |
|---|---|
| payment 변경 때 order도 같이 배포 | 내부 DTO 공유 |
| member schema 변경 때 모든 service 수정 | shared DB |
| 공통 library 변경이 전체 service 재배포 | common module 과다 |
| rollback이 service별로 불가능 | API version과 migration 호환성 부재 |
| 독립 배포는 CI/CD 버튼이 아니라 contract, versioning, backward compatibility의 결과다. |
Owner 부재
MSA는 조직 구조와 같이 움직인다. 서비스 owner가 없으면 장애 중 책임이 사라진다. 좋은 service ownership에는 다음이 포함된다.
- API 계약 owner
- DB schema owner
- SLO와 alert owner
- on-call owner
- deploy와 rollback 권한
- cost와 capacity owner Owner가 없으면 service는 운영 단위가 아니라 repository 폴더일 뿐이다.
관측 부재
서비스 수가 늘면 log만으로는 부족하다. 필수 관측은 다음과 같다.
| 관측 | 질문 |
|---|---|
| trace | 요청이 어느 service에서 지연되는가? |
| dependency metric | 어떤 downstream이 실패하는가? |
| service SLO | 어떤 service가 사용자 SLO를 깨뜨리는가? |
| queue lag | event 처리가 밀리는가? |
| DLQ | 어떤 event가 반복 실패하는가? |
| deploy marker | 장애가 어떤 배포와 연결되는가? |
| 관측 없이 MSA를 늘리면 장애 분석 시간이 service 수에 비례해 증가한다. |
되돌리는 선택
실패 신호가 보이면 service를 더 나누는 것이 아니라 줄이거나 경계를 다시 잡을 수 있다.
| 상황 | 대응 |
|---|---|
| owner 없는 작은 service 다수 | module로 합치거나 owner를 재지정 |
| shared DB가 핵심 병목 | writer owner 선정, API/event 전환 |
| sync fanout 과다 | 동기 경로 축소, 비동기 event, read model |
| 공통 deploy train | contract test, versioning, 독립 rollback |
| 관측 부재 | tracing/metric/runbook 먼저 구축 |
| 서비스 합치기는 실패가 아니다. | |
| 운영 단위가 실제 책임 단위와 맞지 않는다는 사실을 인정하는 개선이다. |
분산 Monolith 점검표
다음 질문 중 여러 개가 “예”라면 실패 위험이 높다.
- 하나의 사용자 요청이 4개 이상 service를 동기 호출하는가?
- 여러 service가 같은 table을 write하는가?
- service별 on-call owner가 없는가?
- 모든 service가 같은 release window에 배포되는가?
- 공통 DTO 변경이 모든 service를 깨뜨리는가?
- tracing 없이 log 검색으로 장애를 찾는가?
- saga 상태와 DLQ owner가 없는가? 이 점검표는 MSA 도입 후에도 정기적으로 반복해야 한다.
개인 프로젝트 기준
- MSA 예제를 만들었다면 shared DB writer를 하나로 제한한다.
- 동기 호출 fanout을 그림과 숫자로 표시한다.
- trace id와 service별 log를 최소한 남긴다.
- service별 owner가 없다면 “학습용 분리”라고 명시하고 운영 한계를 적는다.
- 작은 프로젝트에서는 Modular Monolith가 더 나은 선택일 수 있음을 설명한다.
기업 운영 기준
- 서비스 카탈로그에 owner, SLO, dependency, DB, runbook을 기록한다.
- API별 downstream fanout과 timeout budget을 관리한다.
- shared DB write는 예외 승인과 제거 계획을 요구한다.
- 공통 library 변경의 blast radius를 측정한다.
- 서비스 합치기와 경계 재조정을 architecture review의 정상 선택지로 둔다.
위험 신호
- “MSA니까 장애 격리된다”고 말하지만 모든 요청이 긴 동기 사슬을 지난다.
- 여러 service가 같은 DB table을 직접 수정한다.
- 배포는 독립이라고 하지만 rollback은 전체를 같이 해야 한다.
- service owner 없이 플랫폼팀이 모든 장애를 받는다.
- contract test 없이 provider API를 바꾼다.
- tracing, DLQ, idempotency 없이 event-driven이라고 말한다.
확인 질문
확인 질문
- 확인 질문: 분산 Monolith의 핵심 신호는 무엇인가?
- 답변: service는 나뉘었지만 shared DB, sync fanout, 공통 배포, owner 부재 때문에 실제 독립성이 없는 상태다.
- 확인 질문: MSA 실패 신호가 보이면 무조건 service를 더 나눠야 하는가?
- 답변: 아니다. owner를 재정의하거나 module로 합치고, shared DB와 sync fanout을 줄이는 것이 먼저일 수 있다.
- 확인 질문: MSA 운영에서 관측이 중요한 이유는 무엇인가?
- 답변: 장애가 여러 service와 event 경계를 지나기 때문에 trace, dependency metric, DLQ, deploy marker 없이는 원인 격리가 어렵기 때문이다.