모놀리스 vs MSA 서비스 경계 통신 · 게이트웨이 회복탄력성 분산 트랜잭션 관측 · 설정 📝 문제풀이
◀ 심화 CH 11 📋 목차 심화 CH 13 ▶
🧩 심화 CHAPTER 12 · 5년차 트랙

서비스를 쪼갰더니, 이번엔 "네트워크"가 적이 됐다

MSA는 배포 독립성과 팀 자율성을 주지만, 공짜가 아니에요. 단일 프로세스 호출이 네트워크 호출로 바뀌는 순간 지연·부분 실패·데이터 일관성이라는 분산 시스템의 근본 문제가 따라옵니다. 이 장은 언제 쪼개고, 어떻게 나누고, 장애를 어떻게 버티느냐를 다룹니다.

🎯 이 장을 끝내면
🏛️
모놀리스 vs MSA
쪼개는 건 목적이 아니라 수단이다
MSA는 "정답"이 아니라 "선택"이에요.

모놀리스는 하나의 배포 단위에 모든 기능이 들어 있어요. MSA는 비즈니스 능력별로 독립 배포·독립 확장되는 작은 서비스들로 나눕니다. 각각 얻는 것과 잃는 것이 뚜렷해요.

관점모놀리스MSA
배포한 덩어리 → 작은 변경도 전체 재배포서비스별 독립 배포. 영향 범위 격리
확장전체를 함께 스케일아웃(비효율)병목 서비스만 선택 확장
코드베이스 공유 → 조정 비용↑팀별 자율성(기술·릴리스 독립)
호출인프로세스 메서드 호출(빠르고 안정)네트워크 호출 → 지연·부분 실패
데이터단일 DB 트랜잭션으로 강한 일관성DB 분산 → 최종 일관성·분산 트랜잭션
운영단순(1개 관측 대상)분산 추적·서비스 메시 등 복잡도↑
⚠️
"작게 시작하라" — 모놀리스 우선(Monolith First). 도메인을 아직 잘 모르는 초기 스타트업이 처음부터 MSA로 가면, 잘못 그은 서비스 경계를 나중에 옮기느라 네트워크 경계를 넘나드는 리팩터링 지옥에 빠져요. 마틴 파울러의 조언은 잘 모듈화된 모놀리스로 시작하고, 도메인 경계가 선명해지고 조직·트래픽이 실제로 압박을 줄 때 필요한 부분만 떼어내라는 거예요. MSA는 조직적·운영적 성숙도에 대한 세금을 요구합니다.
💸 분산의 오류(Fallacies of Distributed Computing) MSA로 가면 개발자가 자주 잊는 8가지 거짓 전제가 있어요. ① 네트워크는 신뢰할 수 있다 ② 지연은 0이다 ③ 대역폭은 무한하다 ④ 네트워크는 안전하다 … 전부 거짓이에요. 모놀리스에서 a.b()는 실패하지 않지만, MSA에서 같은 호출은 타임아웃·연결 거부·부분 응답으로 언제든 실패합니다. 이 장 후반의 회복탄력성 패턴은 전부 "네트워크 호출은 실패한다"는 전제에서 출발해요.
🖼️ 그림으로 보기 — 모놀리스 vs 마이크로서비스
모놀리스 — 한 덩어리 · 단일 DB 주문 결제 배송 회원 🗄️ 단일 DB 작은 변경도 전체 재배포 · 통째로 확장 마이크로서비스 — 도메인별 분리 주문 서비스 결제 서비스 배송 서비스 🗄️ 주문DB 🗄️ 결제DB 🗄️ 배송DB 각자 API·DB 소유 · 독립 배포/확장 대신 네트워크 호출·데이터 일관성 비용
✂️
서비스 경계
기술이 아니라 도메인으로 나눈다
DDD 바운디드 컨텍스트가 경계선의 기준이에요.

서비스를 UI 서비스·비즈니스 서비스·DB 서비스처럼 기술 계층으로 나누는 건 흔한 함정이에요. 하나의 기능 변경이 세 서비스를 모두 건드려 결국 강결합이 됩니다. 좋은 경계는 비즈니스 능력(도메인)을 따라야 해요 — 이게 DDD의 바운디드 컨텍스트입니다.

📦
주문 컨텍스트
Order 전용 모델·DB
💳
결제 컨텍스트
Payment 전용 모델·DB
🚚
배송 컨텍스트
Shipping 전용 모델·DB
🗄️ 서비스별 DB(Database per Service) — 데이터 소유 원칙 각 서비스는 자기 데이터를 자기 DB에만 두고, 다른 서비스는 그 DB를 직접 조회하지 못해요. 필요한 데이터는 반드시 그 서비스의 API·이벤트를 통해서만 얻습니다. 이유는 결합도예요. 여러 서비스가 같은 테이블을 공유하면(shared database), 한 팀의 스키마 변경이 다른 팀을 깨뜨려 독립 배포라는 MSA의 핵심 이점이 사라집니다. DB를 공유하는 순간 그건 사실상 분산된 모놀리스예요.
🧭
같은 단어, 다른 의미(Ubiquitous Language). "고객(Customer)"은 주문 컨텍스트에선 배송지·연락처가 중요하고, 결제 컨텍스트에선 결제수단·한도가 중요해요. 하나의 거대한 공용 Customer 클래스로 억지로 합치지 말고, 컨텍스트마다 자기에게 필요한 모델을 갖는 게 바운디드 컨텍스트의 핵심입니다.
⚠️
강결합을 피하라. 서비스 A가 서비스 B의 내부 DB 스키마·내부 클래스에 의존하면 A와 B는 사실상 하나예요. 통신은 안정적인 계약(API/이벤트 스키마)으로만 하고, 내부 구현은 서로 몰라야 독립적으로 진화할 수 있어요.
🔌
통신 · 게이트웨이
동기냐 비동기냐, 그리고 관문
호출 방식이 결합도와 회복력을 좌우해요.
방식대표 기술성격
동기 요청/응답REST(HTTP/JSON), gRPC(HTTP/2·protobuf)단순·직관적. 호출자가 응답을 기다림 → 시간적 결합·장애 전파 위험
비동기 메시징Kafka, RabbitMQ 등 메시지 브로커발신 후 잊음(fire-and-forget)·이벤트. 느슨한 결합·버퍼링. 대신 최종 일관성
⚖️ 동기 REST/gRPC vs 비동기 메시징 동기는 즉시 응답이 필요하고 흐름이 단순할 때 좋아요. 하지만 B가 느리면 A도 느려지고, B가 죽으면 A도 위태로워집니다(장애 전파·연쇄 실패).
비동기는 발신자가 수신자의 가용성에 덜 의존해요. 브로커가 메시지를 보관하니 수신자가 잠깐 죽어도 복구 후 처리합니다. 대신 즉시 일관성이 아니라 최종 일관성이고, 디버깅·순서 보장이 까다로워요. gRPC는 protobuf 바이너리·HTTP/2 스트리밍으로 서비스 간 내부 통신에서 REST보다 빠르고 스키마가 엄격합니다. 외부 공개 API엔 REST, 내부 고성능 통신엔 gRPC를 섞어 쓰는 조합이 흔해요.
💡
API 게이트웨이 — 하나의 관문. 클라이언트가 수십 개 서비스 주소를 알 필요 없이 게이트웨이 한 곳으로 들어와요. 게이트웨이는 라우팅(요청을 알맞은 서비스로), 인증·인가(공통 보안), 속도 제한(rate limiting), 그리고 여러 서비스 응답을 모으는 집약(aggregation)을 담당합니다(Spring Cloud Gateway 등). 다만 게이트웨이에 로직을 몰아넣으면 그게 새로운 병목·모놀리스가 되니 횡단 관심사만 두세요.
🧭
서비스 디스커버리 — 주소를 하드코딩하지 않는다. 컨테이너·오토스케일 환경에선 서비스 인스턴스의 IP가 수시로 바뀌어요. 서비스 레지스트리(Eureka, Consul, 또는 쿠버네티스 DNS)에 인스턴스가 자기를 등록하고, 호출자는 "결제 서비스 아무 인스턴스"를 이름으로 찾아 부릅니다. 여기에 클라이언트/서버 사이드 로드밸런싱이 결합돼요.
🛡️
회복탄력성
하나가 죽어도 전체는 버틴다
Resilience4j로 장애 전파를 끊는 다섯 가지 패턴.

MSA에서 가장 무서운 건 연쇄 장애(cascading failure)예요. 하위 서비스 하나가 느려지면, 그걸 기다리는 상위 서비스의 스레드가 전부 묶이고, 그 상위를 기다리는 서비스도 묶이면서 시스템 전체가 도미노로 무너져요. 이걸 막는 패턴들이에요.

패턴하는 일핵심
타임아웃정해진 시간 안에 응답 없으면 포기무한 대기 방지 → 스레드 회수. 모든 원격 호출의 기본
재시도(Retry)일시적 실패를 다시 시도지수 백오프 + 지터로. 멱등성 필수
서킷 브레이커실패율이 높으면 회로를 열어(Open) 즉시 실패연쇄 장애 차단 + 대상 서비스 회복 시간 확보
벌크헤드(Bulkhead)호출별로 자원(스레드풀·동시성)을 격리한 호출의 폭주가 다른 호출을 굶기지 않게
폴백(Fallback)실패 시 대체 응답(캐시·기본값)사용자에게 완전 실패 대신 degraded 응답
🔁 재시도의 함정 — 지수 백오프 + 지터, 그리고 멱등성 실패했다고 즉시·계속 재시도하면, 이미 힘든 서비스에 오히려 부하 폭탄을 던져 회복을 막아요(retry storm). 그래서 지수 백오프(exponential backoff)로 대기 시간을 1초 → 2초 → 4초처럼 지수적으로 늘리고, 거기에 지터(jitter, 무작위 흔들림)를 더해 여러 클라이언트가 동시에 몰려 재시도하는 것을 흩뜨립니다. 더 중요한 건 멱등성(idempotency)이에요. "응답이 안 왔다"가 요청이 실패했다는 뜻은 아니에요 — 서버는 처리했는데 응답만 유실됐을 수 있어요. 이 상태로 재시도하면 결제가 두 번 될 수 있죠. 그래서 재시도 대상 연산은 같은 요청을 여러 번 보내도 결과가 한 번과 같도록(멱등성 키 등) 설계해야 합니다.
💡
서킷 브레이커의 3가지 상태. Closed(정상, 요청 통과 + 실패율 집계) → 실패율이 임계값을 넘으면 Open(회로 차단, 요청을 아예 보내지 않고 즉시 실패/폴백) → 일정 시간 뒤 Half-Open(시험 삼아 몇 개만 통과시켜 회복 확인) → 성공하면 다시 Closed. 핵심은 이미 죽은 서비스를 계속 두드리지 않고 빠르게 실패시켜, 호출자 자원을 지키고 대상에게 숨 쉴 시간을 주는 거예요.
🖼️ 그림으로 보기 — 서킷 브레이커 상태 전이
CLOSED 정상 통과 · 실패율 집계 OPEN 즉시 실패 · 폴백 HALF-OPEN 시험 요청 일부만 통과 실패율 임계값 초과 대기 시간 경과 시험 성공 시험 실패
// Resilience4j — 서킷 브레이커 + 재시도 + 폴백 조합
@Retry(name = "payment")            // 지수 백오프 + 지터로 재시도
@CircuitBreaker(name = "payment", fallbackMethod = "fallback")
public PaymentResult pay(Order order) {
    return paymentClient.charge(order); // 원격 호출(타임아웃 설정 필수)
}

// 회로가 Open이거나 재시도 소진 시 호출 — degraded 응답
public PaymentResult fallback(Order order, Throwable t) {
    return PaymentResult.pending("결제 지연 — 잠시 후 재시도됩니다");
}
⚠️
타임아웃 없는 재시도·서킷 브레이커는 반쪽이에요. 하위 호출이 영원히 매달려 있으면 서킷 브레이커는 실패로 세지도 못하고, 스레드는 계속 묶여요. 모든 원격 호출에 반드시 타임아웃을 걸고, 그 위에 재시도·서킷 브레이커·벌크헤드를 얹는 게 순서입니다.
🔗
분산 트랜잭션
여러 DB를 하나의 원자성으로 묶으려는 유혹
2PC는 지양, 사가로 최종 일관성을 만든다.

서비스마다 DB가 따로 있으니, "주문 생성 + 결제 + 재고 차감"을 하나의 트랜잭션으로 묶고 싶어져요. 하지만 여러 DB에 걸친 ACID 트랜잭션은 MSA에서 사실상 불가능하거나 위험합니다.

🚫 왜 2PC(2단계 커밋)를 지양하나 2PC는 코디네이터가 모든 참여자에게 "준비됐나?(prepare)" 묻고 전부 OK면 "커밋" 명령을 내려요. 문제는 준비~커밋 사이에 자원(락)을 잡고 대기한다는 점이에요. 참여자가 하나라도 느리거나 죽으면 전체가 블로킹되고, 코디네이터가 죽으면 참여자들이 어정쩡하게 락을 물고 멈춰요. 확장성·가용성이 생명인 MSA와 상극입니다. 그래서 현대 MSA는 강한 일관성을 포기하고 최종 일관성(eventual consistency)을 받아들이는 사가(Saga) 패턴을 씁니다.

사가(Saga)는 하나의 큰 분산 트랜잭션을 각 서비스의 로컬 트랜잭션들의 연속으로 쪼개요. 각 단계는 자기 DB에 대해선 정상 커밋합니다. 중간에 실패하면 롤백 대신 이미 성공한 단계들을 되돌리는 보상 트랜잭션(compensating transaction)을 역순으로 실행해요.

📝
주문 생성
보상: 주문 취소
💳
결제 승인
보상: 결제 환불
📦
재고 차감
❌ 실패 시 앞 단계 보상
🖼️ 그림으로 보기 — 사가(Saga) · 보상 트랜잭션
각 단계 = 자기 DB에 로컬 트랜잭션 커밋 ① 주문 생성 주문 DB 커밋 ② 결제 승인 결제 DB 커밋 ③ 재고 차감 ❌ 실패! 보상: 결제 환불 ↩ 보상: 주문 취소 ↩ 실패하면 이미 커밋된 단계를 역순으로 보상 트랜잭션 실행
사가 방식동작특징
코레오그래피(Choreography)각 서비스가 이벤트를 발행/구독하며 자율적으로 다음 단계 진행중앙 조율자 없음. 느슨하지만 흐름 파악이 어려움
오케스트레이션(Orchestration)중앙 오케스트레이터가 각 단계를 명령·조율흐름이 한눈에 보임. 오케스트레이터가 중심점
⚠️
보상 트랜잭션은 "완벽한 롤백"이 아니에요. 결제를 취소해도 이미 발송된 확인 메일은 되돌릴 수 없어요. 사가는 의미적 롤백(semantic rollback)이라 각 단계의 보상 로직을 비즈니스가 직접 설계해야 하고, 중간의 짧은 순간엔 비일관 상태가 노출될 수 있음을 전제해야 합니다. 또한 보상 트랜잭션도 재시도로 인해 멱등성이 필요해요.
🔭
관측 · 설정
흩어진 서비스를 하나처럼 본다
상관관계 ID·분산 추적·중앙 설정.

모놀리스에선 스택 트레이스 하나로 흐름이 보였지만, 하나의 요청이 5개 서비스를 거치면 로그가 5곳에 흩어져요. "이 느린 요청이 어디서 막혔나?"를 답하려면 요청 단위로 로그를 꿰는 장치가 필요합니다.

🧵 상관관계 ID(Correlation ID)와 분산 추적 게이트웨이에서 요청이 들어올 때 고유 ID(trace ID)를 발급하고, 이후 모든 서비스 호출에 이 ID를 전파해요(HTTP 헤더 등). 각 서비스는 로그에 이 ID를 남기니, ID 하나로 요청의 전체 여정을 재구성할 수 있어요. 여기에 각 구간의 소요 시간(span)을 더한 게 분산 추적(distributed tracing)입니다. OpenTelemetry·Zipkin·Jaeger 같은 도구로 "어느 서비스, 어느 구간에서 지연이 났는지"를 폭포수 그래프로 봐요. Micrometer Tracing/Spring Cloud Sleuth가 자바에서 이 전파를 자동화합니다.
💡
중앙 설정(Centralized Config). 수십 개 서비스에 설정이 흩어져 있으면, DB 주소·기능 플래그 하나 바꾸는 데 전부 재배포해야 해요. Spring Cloud Config·Consul·쿠버네티스 ConfigMap처럼 설정을 중앙에서 관리·주입하면 재배포 없이 갱신(핫 리로드)할 수 있고, 환경별(dev/stage/prod) 설정을 일관되게 관리합니다. 단, 비밀 값은 Vault 등 시크릿 관리로 분리하세요.
Correlation ID Distributed Tracing OpenTelemetry Central Config Structured Log Metrics
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
시니어 관점으로 점검
면접·설계 리뷰에서 실제로 묻는 결의 문제예요. 틀려도 바로 해설이 나옵니다.
🧪
개념을 "안다"와 "설명할 수 있다"는 달라요. 네 유형(객관식 · O/X · 빈칸 · 코드결과)으로 점검해보세요.
심화 CH 13 🌊 리액티브와 WebFlux — 논블로킹, Reactor(Mono/Flux), 백프레셔, 이벤트 루프 (제작 예정)