Software Architecture/Architectural Styles
Architectural Styles - 06. 마이크로서비스의 함정
마이크로서비스의 함정 — 쪼갰지만 묶여 있는 구조
2017년, 한 대형 이커머스 회사가 2년에 걸쳐 모놀리스를 40개의 마이크로서비스로 쪼갰다. 발표 자료엔 멋진 서비스 맵이 그려졌다. 그런데 1년 뒤, 그 팀은 한 가지 이상한 현상을 보고했다 — 한 서비스를 배포하면 자주 다른 서비스가 깨졌다. 서비스가 '독립적'이지 않았다. 조사해보니 서비스 30개가 동기 HTTP 호출 체인으로 엮여 있었고, 한 서비스의 응답 스키마를 바꾸면 호출하는 서비스들이 연쇄적으로 깨졌다. 외견상 40개 서비스, 실제로는 30개가 결합된 하나의 시스템 — 분산 모놀리스(distributed monolith)였다.
이 글이 다루는 질문: 마이크로서비스로 '분해'했는데 왜 '결합'이 남을까. 그리고 그 결합을 어떻게 풀거나 피할 수 있을까.
비유로 감 잡기 — 끈으로 묶은 상자
여러 개의 작은 상자를 하나로 묶어 큰 상자처럼 드는 형태를 상상하자. 겉보기엔 여러 상자지만, 실제로는 하나처럼 움직인다. 한 상자를 들어 올리려면 다른 상자들도 함께 움직여야 한다. 모놀리스라는 큰 상자를 쪼개는 대신, 작은 상자들에 끈을 묶어놓은 것.
분산 모놀리스가 이 형태다. 프로세스는 나뉘었지만 의존성은 그대로라, 한 서비스의 변경이 다른 서비스를 끌고 다닌다. Newman이 2판(2021)에서 가장 강하게 경계하는 안티패턴이다. 모놀리스의 단점(배포 의존성)과 마이크로서비스의 단점(분산 복잡성)만 겹친, 최악의 결과.
비유의 한계 — 상자 비유는 '물리적 결합'을 강조하지만, 분산 모놀리스의 결합은 논리적·런타임적이다. 같은 데이터센터에 있을 수도 있고, 같은 쿠버네티스 클러스터에 있을 수도 있다. '끈'은 코드 의존성·API 계약·데이터 스키마 같은 논리적 연결이다. 비유는 '결합의 추상성'에서 무너진다.
분산 모놀리스를 만드는 네 가지 패턴
Newman이 2판에서 정리하는 분산 모놀리스의 원인은 대략 네 가지로 모은다.
1. 공유 데이터베이스 (shared database)
05편에서 본 가장 흔한 안티패턴. 서비스를 프로세스 단위로는 나눴지만, DB는 하나를 공유한다. Order Service가 payments 테이블을 직접 읽고, Payment Service가 orders 테이블을 직접 고친다. 한 서비스의 스키마 변경이 다른 서비스를 깨뜨린다.
// Order Service가 Payment DB에 직접 쿼리
class OrderService {
private final JdbcTemplate jdbc; // 공유 DB
public OrderStatus checkStatus(OrderId id) {
// payments 테이블에 직접 쿼리 — Payment Service 스키마와 결합
return jdbc.queryForObject(
"SELECT status FROM payments WHERE order_id = ?", OrderStatus.class, id.value());
}
}
이 패턴은 "마이크로서비스를 도입하되 DB는 일단 그대로"라는 현실적 타협에서 자주 출발한다. 하지만 한 번 허용하면 점점 더 많은 서비스가 다른 서비스의 테이블에 직접 닿게 되고, 결국 스키마 변경이 불가능해진다.
2. 동기 호출 체인 (synchronous chain)
Order Service → Payment Service → Inventory Service → Shipping Service가 동기 HTTP로 엮인 형태. 한 서비스가 느려지면 전체가 블록된다. 모놀리스의 '장애 전파(failure cascading)'가 그대로, 혹은 더 복잡한 형태로 재현된다.
flowchart LR
C[Client] --> OS[Order Service]
OS -->|HTTP 동기| PS[Payment Service]
PS -->|HTTP 동기| IS[Inventory Service]
IS -->|HTTP 동기| SS[Shipping Service]
SS -. 지연 발생 .-> X[전체 타임아웃]
이 구조에서 Shipping Service가 3초 지연되면, 그 위로 올라가는 모든 서비스가 3초를 기다린다. 스레드 풀도 고갈된다 — 모놀리스 시절의 재앙이 마이크로서비스에서 반복된다.
3. 잘못 그은 경계 (wrong boundaries)
서비스를 기술적 계층(Service 클래스별, Repository별)이나 CRUD 엔터티별(OrderService, CustomerService)로 잘랐을 때 벌어진다. 한 비즈니스 흐름('주문')이 5개 서비스를 건드리게 되고, 그 5개는 사실상 하나처럼 움직여야 한다 — 같이 배포되고, 같이 변경된다. 이것도 분산 모놀리스다.
05편의 자르기 기준 표를 참조할 것. 비즈니스 능력이나 하위 도메인으로 자르지 않으면 이 함정에 빠진다.
4. 과도한 서비스 세분화 (nanoservices)
서비스를 너무 작게 잘라 한 비즈니스 흐름이 20개 서비스를 거치게 만드는 극단. '마이크로서비스'라는 이름에 현혹돼 "작을수록 좋다"고 믿는 경우. Newman은 이를 '나노서비스(nanoservices)'라 부르며 경계한다. 통신 오버헤드가 처리 자체보다 커지고, 디버깅이 불가능해진다.
관측성 — 분산 환경의 숨은 비용
모놀리스에선 한 요청의 로그가 한 프로세스 안에 있다. 한 번 grep이면 전체 흐름이 보인다. 마이크로서비스에선 한 사용자 요청이 5개 서비스를 거치고, 로그가 5개 서비스에 흩어진다. 어느 서비스에서 느려졌는지 찾으려면 분산 추적(distributed tracing)이 필수다.
flowchart LR
C[Client] --> A[Service A<br/>trace-id: abc123<br/>span: 50ms]
A --> B[Service B<br/>trace-id: abc123<br/>span: 800ms]
B --> D[Service C<br/>trace-id: abc123<br/>span: 30ms]
D --> E[Service D<br/>trace-id: abc123<br/>span: 40ms]
B -. 병목 .-> X[여기서 800ms]
이 trace-id를 따라가며 어디서 지연이 발생했는지 찾는 도구가 Jaeger, Zipkin, OpenTelemetry다. 모놀리스에선 불필요했던 이 인프라가 마이크로서비스에선 기본기다. 이걸 깔지 않고 마이크로서비스로 가면, "어디서 느린지 모르는" 지옥을 맞는다.
분산 모놀리스 감지 — 코드와 운영에서 어떻게 알아내나
| 신호 | 의미 |
|---|---|
| 한 서비스 배포 시 다른 서비스도 함께 배포해야 함 | 서비스 간 강결합 — 독립 배포 아님 |
| 한 서비스의 스키마 변경이 다른 서비스 빌드를 깨뜨림 | 공유 DB — 데이터 결합 |
| 한 서비스 장애 시 다른 서비스가 연쇄 장애 | 동기 호출 체인 |
| 분산 추적 없이 "어디서 느린지" 못 찾음 | 관측성 부재 |
| 한 사용자 요청이 5개 이상 서비스를 동기로 거침 | 과도한 세분화 또는 동기 체인 |
| 팀별로 다른 서비스를 담당하지만 서로 배포 일정 맞춰야 함 | Conway 미정렬 + 서비스 결합 |
이 표의 신호가 많으면, 그 시스템은 마이크로서비스가 아니라 분산 모놀리스다. Newman의 진단은 단호하다 — "차라리 모놀리스로 돌아가라"가 그의 조언 중 하나다.
데이터 일관성 — 모놀리스에선 자연스럽던 것이 마이크로서비스에선 비용
모놀리스에선 한 트랜잭션으로 여러 테이블을 원자적으로 바꾸는 게 자연스럽다 (ACID). 마이크로서비스에선 각 서비스가 자기 DB를 갖기 때문에, 여러 서비스의 DB를 한 트랜잭션으로 묶을 수 없다. 2PC(2단계 커밋)는 이론적으론 가능하지만 블로킹과 성능 문제로 실제론 거의 안 쓰인다. 대안이 Saga 패턴이다.
Saga — 분산 트랜잭션을 보상으로 푸는 패턴
Saga는 한 비즈니스 흐름을 여러 단계의 로컬 트랜잭션으로 쪼개고, 각 단계가 실패하면 이전 단계를 '보상(compensate)'하는 방식이다. 중요한 건 — 보상은 롤백이 아니다.
// 주문 → 결제 → 재고 감소 → 배송 예약 흐름
// 각 단계가 실패하면 이전 단계를 보상
class OrderSaga {
void execute(OrderRequest req) {
try {
Order order = orderService.create(req); // 1. 주문 생성
paymentService.charge(order); // 2. 결제
inventoryService.decrement(req.items()); // 3. 재고 감소
shippingService.reserve(order); // 4. 배송 예약
} catch (PaymentFailed e) {
orderService.cancel(order); // 1단계 보상: 주문 취소
} catch (InventoryShortage e) {
paymentService.refund(order); // 2단계 보상: 결제 환불
orderService.cancel(order); // 1단계 보상
} catch (ShippingUnavailable e) {
inventoryService.restore(req.items()); // 3단계 보상: 재고 복원
paymentService.refund(order); // 2단계 보상
orderService.cancel(order); // 1단계 보상
}
}
}
이 흐름에서 핵심 — 결제가 성공한 뒤 재고가 부족하면, 결제를 '취소'하는 게 아니라 '환불'한다. 원래 상태로 되돌리는 게 아니라, 반대 행위를 한다는 의미에서 '보상(compensation)'이라 부른다. 2PC와 다른 점이다 — 2PC는 커밋 전에 모든 참여자가 준비됐는지 확인하지만, Saga는 일단 커밋하고 문제 생기면 보상한다. 중간 상태가 외부에 보인다.
이 차이가 마이크로서비스의 데이터 일관성 모델을 근본적으로 바꾼다. '강한 일관성'을 포기하고 '최종 일관성'을 감수해야 한다. 금융 원장처럼 강한 일관성이 필수인 시스템엔 Saga가 위험할 수 있다 — 그런 시스템은 모놀리스가 더 안전하다.
Saga의 두 형태 — orchestration vs choreography
| 형태 | 설명 | 장점 | 단점 |
|---|---|---|---|
| Orchestration | 중앙조정자(Saga orchestrator)가 각 서비스를 호출 | 흐름 한눈에 보임, 복잡한 분기 가능 | 중앙조정자 = 단일 장애점 |
| Choreography | 각 서비스가 이벤트 구독해 반응 | 중앙 의존 없음, 느슨한 결합 | 전체 흐름 파악 어려움 |
Newman은 흐름이 복잡할 땐 orchestration을, 단순할 땐 choreography를 권한다. 둘 다 정답은 아니다 — 흐름 복잡도에 따라 선택이다.
Conway — 마이크로서비스의 숨은 전제
"마이크로서비스를 도입하면 팀이 독립적으로 일할 수 있다"는 말은 반쪽짜리 진실이다. 그 말이 성립하려면 조직이 이미 팀 단위로 나뉘어 있어야 한다. 한 팀이 모든 서비스를 담당한다면, 마이크로서비스의 독립 배포·독립 기술 선택·독립 배포 주기는 의미가 없다 — 팀이 하나면 결정도 하나다.
Matthew Skelton과 Manuel Pais가 (2019)에서 정리한 Conway의 법칙 역방향 — inverse Conway maneuver — 는 이걸 뒤집는다. 조직이 마이크로서비스 구조를 원하면, 먼저 팀 구조를 그 서비스 구조에 맞춰 재편하라는 것. 팀 구조를 바꾸지 않고 아키텍처만 바꾸면, 조직이 아키텍처를 자기 구조로 끌어당겨 결국 분산 모놀리스가 된다.
설계 사례 — 분산 모놀리스 풀기
05편에서 만든 잘못된 분해(동기 체인)를 점진히 고쳐 본다.
1단계 — 동기 호출 체인 (분산 모놀리스)
// Order Service가 모든 서비스를 동기로 호출
class OrderService {
private final PaymentClient payment;
private final InventoryClient inventory;
private final ShippingClient shipping;
public OrderId placeOrder(OrderRequest req) {
Order order = orders.save(new Order(req));
payment.charge(order); // 1. 동기 — Payment 끝날 때까지 대기
inventory.decrement(req); // 2. 동기 — Inventory 끝날 때까지 대기
shipping.reserve(order); // 3. 동기 — Shipping 끝날 때까지 대기
return order.id(); // 모든 서비스 끝나야 응답
}
}
세 서비스 중 하나라도 느려지면 주문이 지연된다. 모놀리스의 장애 전파가 그대로 재현된다.
2단계 — 비동기 이벤트로 전환
// Order Service는 주문만 저장하고 이벤트 발행
class OrderService {
private final OrderRepository orders;
private final EventPublisher events;
public OrderId placeOrder(OrderRequest req) {
Order order = new Order(req);
order.markPending(); // '결제 대기' 상태로 저장
orders.save(order);
events.publish(new OrderPlacedEvent(order.id(), req)); // 비동기
return order.id(); // 즉시 응답 — 결제는 백그라운드
}
}
// Payment Service가 이벤트 구독
class PaymentOnOrderPlaced {
void handle(OrderPlacedEvent e) {
try {
payment.charge(e.orderId(), e.total());
events.publish(new PaymentSucceeded(e.orderId()));
} catch (Exception ex) {
events.publish(new PaymentFailed(e.orderId(), ex.getMessage()));
}
}
}
// Order Service가 PaymentSucceeded/PaymentFailed 구독
class OrderOnPaymentResult {
void handle(PaymentSucceeded e) { orders.markPaid(e.orderId()); }
void handle(PaymentFailed e) { orders.markCancelled(e.orderId()); }
}
Order Service는 더 이상 Payment Service를 기다리지 않는다. 이벤트를 발행하고 끝. Payment Service가 자기 속도에 맞춰 처리한다. 결제가 3초 걸려도 주문은 즉시 접수된다.
이 전환의 대가 — '주문 완료'와 '결제 완료'가 분리된다. 클라이언트는 '주문 접수됨, 결제 진행 중' 상태를 처리해야 한다. 강한 일관성에서 최종 일관성으로의 전환이다. 이게 마이크로서비스가 감수해야 하는 근본적 비용이다.
회복탄력성 — 분산 환경의 장애를 다루는 패턴
마이크로서비스에선 "다른 서비스가 응답하지 않는다"가 일상이다. Martin Fowler가 (2002)에서 정리하고, Netflix의 Hystrix로 유명해진 회복탄력성(resilience) 패턴들이 이 상황을 다룬다.
| 패턴 | 설명 | 효과 |
|---|---|---|
| Timeout | 호출마다 최대 대기 시간 설정 | 무한정 블록 방지, 스레드 풀 보호 |
| Retry (지수 백오프) | 일시적 장애 시 재시도, 점진적 간격 | 일시적 네트워크 오류 회복 |
| Circuit Breaker | 연속 실패 시 일정 시간 호출 차단 | 장애 서비스 보호, 빠른 실패로 전환 |
| Bulkhead | 스레드·커넥션 풀을 서비스별 분리 | 한 서비스 장애가 자원 고갈로 번지는 것 차단 |
| Fallback | 호출 실패 시 대체 응답(캐시, 기본값) | 부분적 기능이라도 유지 |
이 패턴들은 마이크로서비스의 '기본기'다. 모놀리스에선 불필요했지만, 분산 환경에선 없으면 한 서비스 장애가 전체 장애로 번진다. Resilience4j(Java), Polly(.NET), opossum(Node.js) 같은 라이브러리가 이 패턴들을 구현해 제공한다.
마이크로서비스의 비용을 직시하기
Newman이 2판에서 가장 강조하는 메시지를 하나로 요약하면 — 마이크로서비스의 비용은 '분산' 자체에서 온다. 데이터 일관성 약화, 분산 추적 부담, 장애 전파 새 형태, 팀 정렬 필요. 이 비용은 '더 좋은 도구'나 '더 많은 노력'으로 제거할 수 있는 게 아니라, 분산이라는 선택 자체에서 오는 것이다.
그래서 마이크로서비스는 '성숙한 조직'의 선택지다. CI/CD, 관측성 인프라, 팀 커뮤니케이션 구조, 도메인 이해가 모두 성숙했을 때 비로소 그 이점(독립성)이 비용(분산 복잡성)을 상회한다. 그 전에는 모듈형 모놀리스가 더 나은 선택지인 경우가 많다. Newman이 1판에서 "모놀리스 먼저"를 약하게 말했다면, 2판에선 그것을 강하게 말한다 — 그만큼 6년 사이 실패 사례가 쌓였다.
다음으로 — 이벤트 기반 아키텍처
분산 모놀리스를 피하는 한 방법이 비동기 이벤트다. 07편(이벤트 기반 아키텍처)에서 이 패턴을 깊이 다룬다. 이벤트 기반은 마이크로서비스의 함정(동기 호출 체인)을 피하는 도구이기도 하지만, 그 자체로 또 다른 trade-off(최종 일관성, 디버깅 복잡성)를 갖는다.
참고
- Newman — (2nd ed, O'Reilly, 2021), Ch.4 (데이터 격리)·Ch.6 (관측성)·Ch.11 (진화) — 접근 2026-07-20
- Fowler — "CircuitBreaker" (martinfowler.com/bliki), 회복탄력성 관련 글 — 접근 2026-07-20
- Skelton, Pais — (IT Pro Press, 2019), inverse Conway maneuver
- Richards, Ford — (O'Reilly, 2020), Ch.17 (마이크로서비스의 함정)
- Garcia-Molina, Salem — "Sagas" (1987), Saga 원 논문 — 접근 2026-07-20
'Software Architecture > Architectural Styles' 카테고리의 다른 글
| Architectural Styles - 08. CQRS (0) | 2026.07.20 |
|---|---|
| Architectural Styles - 07. 이벤트 기반 아키텍처 (0) | 2026.07.20 |
| Architectural Styles - 05. 마이크로서비스 (0) | 2026.07.20 |
| Architectural Styles - 04. 클린 아키텍처 (0) | 2026.07.20 |
| Architectural Styles - 03. 헥사고날 아키텍처 (0) | 2026.07.20 |