MSA — 마이크로서비스 아키텍처의 핵심과 트레이드오프
MSA는 유행이 아니라 트레이드오프다. 모놀리식이 나쁜 게 아니고, MSA가 좋은 게 아니다. 조직 규모, 서비스 복잡도, 팀 역량에 따라 답이 달라진다. 이 관점을 갖고 있느냐가 핵심이다.
모놀리식 vs MSA
모놀리식 아키텍처
하나의 코드베이스, 하나의 배포 단위로, 모든 기능이 한 프로세스 안에서 돌아갑니다.
장점부터 볼게요.
- 개발이 단순해요. 프로젝트 하나 열면 전체 코드가 다 보입니다.
- 로컬에서 통합 테스트 돌리기 쉬워요. 서비스 간 네트워크 호출이 없으니까요.
- 트랜잭션 관리가 간단합니다. DB 하나에
@Transactional걸면 끝이에요. - 배포도 간단해요. JAR 하나 빌드해서 올리면 됩니다.
그런데 서비스가 커지면 문제가 생깁니다.
- 코드가 수십만 줄 넘어가면 빌드 시간만 10분, 20분. 개발 속도가 느려져요.
- 주문 기능 하나 고쳤는데 결제 쪽에서 사이드이펙트가 터집니다. 모듈 간 경계가 모호해서 그래요.
- 배포할 때마다 전체를 다시 올려야 해요. 금요일 오후에 배포? 야근 각입니다.
- 특정 기능만 스케일 아웃하고 싶어도 전체를 복제해야 합니다. 자원 낭비예요.
마이크로서비스 아키텍처
기능 단위로 서비스를 쪼개고, 각 서비스가 독립적으로 배포·운영됩니다.
- 서비스별로 독립 배포가 가능해요. 주문 서비스만 수정해서 바로 올릴 수 있습니다.
- 팀 단위로 서비스를 소유하니까 의사결정이 빠릅니다. "이 서비스는 우리 팀 거예요."
- 서비스마다 다른 기술 스택을 쓸 수 있어요. 주문은 Java, 추천은 Python — 상황에 맞게요.
- 장애 격리가 됩니다. 알림 서비스가 죽어도 주문은 계속 돌아가요(잘 설계했다면).
대신 복잡도가 확 올라갑니다.
- 분산 시스템 특유의 문제가 있어요. 네트워크 지연, 부분 실패, 데이터 정합성.
- 서비스 간 통신 설계가 까다롭습니다. 동기? 비동기? 어디서 장애가 전파될지 몰라요.
- 운영 복잡도도 높아요. 로그는 서비스마다 흩어져 있고, 트레이싱 없으면 디버깅이 지옥입니다.
- 인프라 비용도 올라갑니다. 각 서비스마다 CI/CD 파이프라인, 모니터링, 로깅이 필요해요.
전환 시점
언제 MSA로 전환해야 하는 걸까요?
정해진 기준은 없지만, 보통 이런 신호가 보이면 고려합니다.
- **팀이 커졌다 **: 개발자가 20명 넘어가면 모놀리식에서 코드 충돌이 빈번해집니다.
- ** 배포 병목 **: 한 팀이 배포하려면 다른 팀 작업이 끝날 때까지 기다려야 해요.
- ** 스케일링 요구가 불균형 **: 검색은 트래픽이 많은데 관리자 기능은 거의 안 쓰는 상황이에요.
- ** 기술 부채가 심각 **: 모놀리식 전체를 리팩토링하는 것보다 일부를 떼어내는 게 현실적입니다.
반대로, 스타트업 초기에 MSA부터 시작하는 건 대부분 오버 엔지니어링이에요. 마틴 파울러도 "Monolith First"를 권장합니다. 도메인 경계가 명확해지기 전에 서비스를 쪼개면 나중에 재통합하느라 더 고생해요.
MSA의 핵심 원칙
단일 책임 원칙 (Single Responsibility)
서비스 하나가 하나의 비즈니스 도메인을 담당합니다. 주문 서비스는 주문만, 결제 서비스는 결제만. 이게 지켜져야 서비스 간 결합도가 낮아져요.
실수하기 쉬운 부분이 있는데요. "기능"으로 쪼개는 게 아니라 "비즈니스 도메인"으로 쪼개야 한다는 겁니다. 사용자 인증과 사용자 프로필을 굳이 다른 서비스로 분리할 필요가 있을까요? 둘 다 "사용자" 도메인이에요. 너무 잘게 쪼개면 네트워크 호출만 늘어납니다.
독립 배포 (Independent Deployment)
서비스 A를 배포할 때 서비스 B를 같이 배포해야 한다면 MSA가 아닙니다. 이걸 지키려면 API 계약(contract)이 명확해야 해요. 하위 호환성을 깨는 API 변경은 버전 관리로 대응합니다. /api/v1/orders, /api/v2/orders 이런 식으로요.
기술 다양성 (Technology Heterogeneity)
각 서비스가 자기 상황에 맞는 기술을 선택할 수 있어요. 실시간 채팅은 Node.js, 데이터 분석은 Python, 핵심 비즈니스 로직은 Java. DB도 마찬가지입니다. 관계형 데이터는 PostgreSQL, 세션 저장은 Redis, 검색은 Elasticsearch.
단, 이게 "마음대로 아무거나 쓰라"는 뜻은 아니에요. 조직 내에서 지원 가능한 기술 범위 안에서 선택해야 합니다. 운영할 사람이 없는 기술을 도입하면 그게 곧 기술 부채가 돼요.
서비스 디스커버리
MSA에서는 서비스 인스턴스가 동적으로 생겼다 사라집니다. 오토스케일링으로 인스턴스가 3개에서 10개로 늘어나기도 하고, 장애로 갑자기 죽기도 해요. 그래서 "이 서비스가 지금 어디서 돌아가고 있지?"를 알아내는 메커니즘이 필요한데, 그게 서비스 디스커버리입니다.
클라이언트 사이드 디스커버리
서비스 레지스트리(예: Eureka)에 모든 인스턴스 정보가 등록되어 있고, 호출하는 쪽(클라이언트)이 레지스트리에서 목록을 가져와 직접 로드밸런싱합니다.
클라이언트 → Eureka에서 인스턴스 목록 조회 → 직접 라운드로빈 등으로 선택 → 호출
- ** 장점 **: 클라이언트가 로드밸런싱 전략을 제어할 수 있어요. 단순한 구조입니다.
- ** 단점 **: 클라이언트 코드에 디스커버리 로직이 들어갑니다. 언어별로 라이브러리가 필요해요.
- ** 대표 **: Netflix Eureka + Ribbon (지금은 Spring Cloud LoadBalancer로 대체됨)
서버 사이드 디스커버리
호출하는 쪽은 로드밸런서(또는 DNS)에만 요청하고, 로드밸런서가 레지스트리를 보고 적절한 인스턴스로 라우팅합니다.
클라이언트 → 로드밸런서/DNS → 레지스트리에서 인스턴스 조회 → 라우팅
- ** 장점 **: 클라이언트가 디스커버리를 신경 쓸 필요가 없어요. 언어 중립적입니다.
- ** 단점 **: 로드밸런서가 SPOF(Single Point of Failure)가 될 수 있어요. 추가 홉이 생겨서 레이턴시가 살짝 늘어납니다.
- ** 대표 **: Kubernetes Service, AWS ALB
K8s 환경에서는 서버 사이드가 자연스러워요. K8s Service와 CoreDNS가 디스커버리를 알아서 처리해주니까 애플리케이션 레벨에서 Eureka 같은 걸 따로 돌릴 필요가 없습니다.
API Gateway
클라이언트가 수십 개 마이크로서비스에 직접 요청을 보내면 어떻게 될까요? 클라이언트가 각 서비스의 주소를 다 알아야 하고, 인증 로직이 서비스마다 중복되고, CORS 설정도 제각각이 됩니다. API Gateway는 이런 문제를 해결하는 단일 진입점이에요.
주요 역할
- ** 라우팅 **:
/api/orders/**→ 주문 서비스,/api/payments/**→ 결제 서비스 - ** 인증/인가 **: JWT 검증을 게이트웨이에서 한 번만 처리합니다. 내부 서비스는 신뢰할 수 있는 헤더(예:
x-user-id)만 보면 돼요. - Rate Limiting: 사용자별, IP별 요청 수 제한. DDoS 방어 첫 번째 라인이에요.
- ** 로드밸런싱 **: 같은 서비스의 여러 인스턴스에 요청을 분배합니다.
- ** 요청/응답 변환 **: 클라이언트에 맞게 응답 포맷을 변환하거나 여러 서비스의 응답을 합쳐서 내려줄 수 있어요 (BFF 패턴).
- ** 로깅/모니터링 **: 모든 요청이 게이트웨이를 지나가니까 여기서 메트릭을 수집하면 됩니다.
구현체 비교
Spring Cloud Gateway
- Spring 생태계와 자연스럽게 통합돼요. WebFlux 기반이라 논블로킹입니다.
- 필터 체인으로 요청/응답을 가공할 수 있어요. 커스텀 필터 작성이 자유로운 편이에요.
- 자바 개발자에게 친숙하지만, 러닝커브가 있는 편입니다.
Kong
- Nginx 기반이에요. 성능이 좋고 플러그인 생태계가 풍부합니다.
- 인증, Rate Limiting, 로깅 같은 건 플러그인 하나 켜면 바로 적용돼요.
- 언어에 종속되지 않습니다. Lua로 커스텀 플러그인 작성이 가능해요.
** 기타 **: AWS API Gateway(서버리스 환경), Envoy(서비스 메시에서 사이드카로), NGINX Plus.
게이트웨이가 SPOF가 되지 않도록 이중화는 필수예요. 그리고 게이트웨이에 비즈니스 로직을 넣으면 안 됩니다. 순수하게 횡단 관심사(cross-cutting concerns)만 처리해야 해요.
서비스 간 통신
동기 통신
호출한 쪽이 응답을 기다리는 방식이에요. 직관적이지만 결합도가 높습니다.
REST (HTTP)
- 가장 보편적이에요. JSON 기반이라 디버깅도 쉽습니다.
- 단점은 성능이에요. HTTP 오버헤드 + JSON 직렬화/역직렬화 비용이 있어요.
- 서비스 간 내부 통신에 REST를 쓰면 레이턴시가 누적됩니다. A → B → C → D, 각각 50ms면 200ms.
gRPC
- Protocol Buffers 기반 바이너리 직렬화로, REST보다 훨씬 빨라요.
- HTTP/2를 사용합니다. 멀티플렉싱, 스트리밍을 지원해요.
.proto파일로 API 명세를 정의하니까 타입 세이프합니다. 코드 생성도 자동이에요.- 단점은 브라우저에서 직접 호출이 안 돼요 (gRPC-Web이 있긴 한데 제한적). 디버깅도 REST보다 불편합니다.
실무에서는 외부 API는 REST, 서비스 간 내부 통신은 gRPC — 이렇게 섞어 쓰는 경우가 많아요.
비동기 통신
메시지 큐(Kafka, RabbitMQ)를 통해 통신합니다. 호출하는 쪽은 메시지를 던져놓고 바로 리턴해요.
- ** 장점 **: 서비스 간 결합도가 낮아져요. 수신 서비스가 죽어 있어도 큐에 메시지가 쌓여있다가 복구되면 처리합니다.
- ** 장점 **: 트래픽 버퍼링이 돼요. 갑자기 요청이 폭주해도 큐가 흡수해줍니다.
- ** 단점 **: 메시지 순서 보장, 중복 처리(멱등성) 같은 문제를 직접 다뤄야 해요.
- ** 단점 **: 디버깅이 어렵습니다. 메시지가 어디서 멈췄는지 추적하려면 별도 도구가 필요해요.
동기와 비동기를 어떤 기준으로 선택하느냐? 핵심은 "응답이 지금 당장 필요한가" 다. 결제 결과는 바로 알아야 하니까 동기. 이메일 발송은 나중에 해도 되니까 비동기. 이 판단이 설계의 시작이다.
분산 트랜잭션
모놀리식에서는 DB 트랜잭션 하나로 원자성을 보장했어요. MSA에서는 서비스마다 DB가 다릅니다. 주문 서비스 DB에는 주문이 들어갔는데, 결제 서비스 DB에서 결제가 실패하면? 이 데이터 정합성 문제가 MSA의 가장 큰 고민 중 하나예요.
2PC (Two-Phase Commit)의 한계
전통적인 분산 트랜잭션 프로토콜이에요. 코디네이터가 모든 참여자에게 "커밋해도 되나?" 물어보고 (Phase 1), 전부 OK하면 "커밋해라" 명령합니다 (Phase 2).
문제가 많아요.
- **성능 **: 모든 참여자가 응답할 때까지 락을 잡고 있어야 합니다. 레이턴시와 처리량 모두 나빠져요.
- ** 가용성 **: 코디네이터가 죽으면 참여자들이 락을 잡은 채로 멈춥니다. SPOF예요.
- ** 확장성 **: 참여자가 늘어날수록 점점 느려져요. NoSQL 같은 건 아예 2PC를 지원하지 않습니다.
결론적으로, MSA 환경에서 2PC는 사실상 쓸 수 없어요. 대신 ** 최종 일관성(Eventual Consistency)**을 받아들이고 SAGA 패턴을 씁니다.
SAGA 패턴
각 서비스의 로컬 트랜잭션을 순차적으로 실행하고, 중간에 실패하면 이전 단계의 보상 트랜잭션(Compensating Transaction)을 실행해서 롤백하는 방식이에요.
예를 들어 주문 생성 플로우가 이렇다고 해볼게요.
1. 주문 생성 → 2. 결제 처리 → 3. 재고 차감 → 4. 배송 요청
3단계에서 재고가 부족하면?
3. 재고 차감 실패 → 2. 결제 취소(보상) → 1. 주문 취소(보상)
보상 트랜잭션을 직접 구현해야 한다는 게 핵심이에요. 자동으로 롤백되는 게 아닙니다.
Choreography (이벤트 기반)
중앙 조정자 없이 각 서비스가 이벤트를 발행하고, 다음 서비스가 구독해서 처리합니다.
주문 서비스 → [주문 생성 이벤트] → 결제 서비스 → [결제 완료 이벤트] → 재고 서비스 → ...
- ** 장점 **: 서비스 간 결합도가 낮아요. 새 서비스 추가가 쉽습니다.
- ** 단점 **: 전체 흐름을 파악하기 어렵습니다. 서비스가 5개만 넘어가도 이벤트 체인이 복잡해져서 디버깅이 힘들어요.
- 3~4개 서비스 정도의 단순한 플로우에 적합해요.
Orchestration (중앙 조정자)
Saga Orchestrator가 전체 플로우를 관리합니다. "결제해라", "재고 차감해라"를 순서대로 지시하고, 실패하면 보상을 지시해요.
Orchestrator → 주문 서비스: 생성
Orchestrator → 결제 서비스: 처리
Orchestrator → 재고 서비스: 차감 ← 실패!
Orchestrator → 결제 서비스: 취소(보상)
Orchestrator → 주문 서비스: 취소(보상)
- ** 장점 **: 전체 플로우가 한 곳에 모여있어서 이해하기 쉽습니다. 복잡한 비즈니스 로직에 적합해요.
- ** 단점 **: Orchestrator에 로직이 집중되면서 God Object가 될 위험이 있어요. Orchestrator 자체의 가용성도 관리해야 합니다.
- 실무에서 복잡한 트랜잭션은 대부분 Orchestration을 씁니다.
Choreography와 Orchestration의 차이는 단순히 구조만 아는 게 아니라, 각각 언제 쓰는지까지 이해하는 게 중요해요. 복잡도에 따른 선택 기준을 알아두면 좋습니다.
서킷 브레이커
MSA에서 서비스 A가 서비스 B를 호출하는데, B가 응답을 안 합니다. A는 타임아웃까지 기다려요. 그 사이에 A로 들어오는 요청이 쌓입니다. A의 스레드 풀이 고갈돼요. A도 죽습니다. 이게 ** 장애 전파(Cascading Failure)**예요.
서킷 브레이커는 전기 회로의 차단기처럼, 하류 서비스가 문제가 있으면 호출 자체를 차단해서 장애 전파를 막아줍니다.
3가지 상태
Closed (정상)
- 모든 요청이 정상적으로 전달돼요.
- 실패율을 계속 모니터링합니다.
- 실패율이 임계치(예: 50%)를 넘으면 → Open으로 전환.
Open (차단)
- 요청을 보내지 않고 즉시 실패 응답(fallback)을 리턴해요.
- "지금 서비스가 불안정하니까 굳이 호출해봤자 시간 낭비야."
- 일정 시간(예: 30초)이 지나면 → Half-Open으로 전환.
Half-Open (반개방)
- 제한된 수의 요청만 실제로 보내봅니다. 탐색 단계예요.
- 성공하면 → Closed로 복귀해요. 서비스가 살아났구나 하고요.
- 실패하면 → 다시 Open으로 갑니다. 아직 복구가 안 된 거예요.
Closed --[실패율 초과]--> Open --[타임아웃]--> Half-Open
^ |
| |
+------[성공]------------------------------------+
|
Open <--------[실패]----------------------------+
Resilience4j
Spring 생태계에서 서킷 브레이커 구현할 때 가장 많이 씁니다. 과거에는 Netflix Hystrix였는데 유지보수 모드로 전환되면서 Resilience4j가 사실상 표준이 됐어요.
핵심 설정 값들이에요.
failureRateThreshold: 실패율 임계치 (기본 50%)waitDurationInOpenState: Open 상태 유지 시간 (기본 60초)slidingWindowSize: 실패율 계산에 사용하는 호출 수 (기본 100)minimumNumberOfCalls: 이만큼 호출이 쌓이기 전까지는 실패율을 계산하지 않아요
서킷 브레이커뿐 아니라 Rate Limiter, Retry, Bulkhead, TimeLimiter도 함께 제공합니다. 조합해서 쓸 수 있어요.
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallback")
@Retry(name = "paymentService")
public PaymentResponse processPayment(PaymentRequest request) {
return paymentClient.process(request);
}
public PaymentResponse fallback(PaymentRequest request, Exception e) {
// 대체 응답 또는 큐에 넣어서 나중에 재처리
return PaymentResponse.pending();
}
서킷 브레이커의 fallback으로 뭘 할 수 있을까요? 캐시된 데이터 반환, 기본값 반환, 큐에 넣어서 나중에 처리 — 상황에 따라 다릅니다.
데이터 관리
Database per Service
MSA의 기본 원칙은 서비스마다 자기 DB를 가지는 거예요. 다른 서비스의 DB에 직접 접근하지 않습니다.
왜 이렇게 하냐면요,
- DB 스키마를 변경할 때 다른 서비스에 영향을 주지 않기 위해서예요.
- 서비스별로 최적의 DB를 선택하기 위해서입니다 (Polyglot Persistence).
- 느슨한 결합을 유지하기 위해서예요. DB를 공유하는 순간 서비스 간 결합도가 올라갑니다.
그런데 다른 서비스의 데이터가 필요하면요? API를 통해 조회하거나, 이벤트를 통해 자기 서비스에 필요한 데이터의 복제본을 유지합니다.
이벤트 소싱 (Event Sourcing)
현재 상태를 직접 저장하는 게 아니라, 상태 변화를 일으킨 이벤트를 순서대로 저장합니다. 현재 상태는 이벤트를 처음부터 재생하면 구할 수 있어요.
은행 계좌를 생각하면 됩니다. 잔액을 저장하는 게 아니라 입금/출금 내역을 저장하는 거예요. 현재 잔액은 내역을 다 합산하면 나옵니다.
- ** 장점 **: 모든 변경 이력이 남아요. 감사(Audit)에 유리하고, 특정 시점의 상태를 복원할 수 있습니다.
- ** 장점 **: 이벤트를 다른 서비스가 구독해서 리액션할 수 있어요. MSA와 궁합이 좋습니다.
- ** 단점 **: 이벤트가 쌓이면 재생 시간이 길어져요. 스냅샷으로 해결하지만 복잡도가 올라갑니다.
- ** 단점 **: 이벤트 스키마가 변경되면 마이그레이션이 까다로워요.
CQRS (Command Query Responsibility Segregation)
읽기(Query)와 쓰기(Command)를 분리하는 패턴이에요. 쓰기용 모델과 읽기용 모델을 따로 만듭니다.
왜 분리할까요? 쓰기에 최적화된 정규화 모델과 읽기에 최적화된 비정규화 모델은 요구사항이 다르기 때문이에요. 주문 데이터를 저장할 때는 정규화해서 넣지만, 화면에 보여줄 때는 주문 + 상품 + 결제 정보가 합쳐진 뷰가 필요합니다.
이벤트 소싱과 같이 쓰는 경우가 많아요. 이벤트 저장소에 쓰기를 하고, 이벤트를 기반으로 읽기 전용 뷰를 갱신하는 구조입니다.
- ** 장점 **: 읽기/쓰기 각각 독립적으로 스케일링할 수 있어요.
- ** 단점 **: 쓰기 후 읽기 뷰가 갱신되기까지 딜레이가 있습니다 (최종 일관성). 시스템 복잡도가 확 올라가요.
주의할 점은 CQRS를 모든 서비스에 적용하는 게 아니라는 거예요. 읽기/쓰기 비율이 극단적으로 차이 나는 서비스에만 적용하는 게 맞습니다.
배포 전략
서비스별 독립 배포
MSA의 핵심 이점이 여기서 나옵니다. 주문 서비스를 배포해도 결제 서비스에는 영향이 없어요. 이걸 가능하게 하려면 몇 가지가 전제되어야 합니다.
- API 하위 호환성을 유지해야 해요. 새 필드를 추가하는 건 괜찮지만, 기존 필드를 삭제하거나 타입을 바꾸면 안 됩니다.
- 서비스별로 독립된 CI/CD 파이프라인이 필요해요.
- 컨테이너 기반 배포가 표준입니다. Docker 이미지로 만들어서 K8s에 올리는 게 일반적인 방식이에요.
Canary 배포
새 버전을 전체 트래픽이 아닌 일부(예: 5%)에만 먼저 적용합니다. 에러율, 레이턴시 등을 모니터링하면서 문제 없으면 점진적으로 비율을 늘려요.
- 위험 최소화가 핵심이에요. 새 버전에 버그가 있어도 5%만 영향 받습니다.
- K8s에서는 Istio나 Argo Rollouts로 구현해요.
- 모니터링 자동화와 함께 쓰면 이상 감지 시 자동 롤백도 가능합니다.
Feature Flag
코드는 배포하되, 기능은 플래그로 켜고 끕니다. 배포와 릴리즈를 분리하는 전략이에요.
if (featureFlags.isEnabled("new-checkout-flow", userId)) {
return newCheckoutService.process(order);
} else {
return legacyCheckoutService.process(order);
}
- 특정 사용자 그룹에게만 기능을 노출(A/B 테스트)하거나, 문제 발생 시 배포 없이 기능만 끌 수 있어요.
- LaunchDarkly, Unleash 같은 전용 도구가 있고, 간단하면 DB나 설정 파일로도 구현 가능합니다.
- 주의할 점은 오래된 플래그를 정리해야 한다는 거예요. 안 그러면 코드가 if-else 지옥이 됩니다.
주의할 점
MSA에서 로깅/트레이싱은 어떻게 하나?
서비스가 10개인데 로그가 서비스마다 흩어져 있으면 디버깅이 불가능해요. 두 가지가 필요합니다.
** 중앙 집중 로깅 **: 모든 서비스의 로그를 한 곳에 모읍니다. ELK 스택(Elasticsearch + Logstash + Kibana)이나 Grafana Loki가 대표적이에요. Fluentd로 수집해서 Elasticsearch에 넣고, Kibana로 검색합니다.
** 분산 트레이싱 **: 하나의 요청이 여러 서비스를 거칠 때, 전체 경로를 추적하려면 요청에 고유한 Trace ID를 붙여서 전파해야 해요. Jaeger, Zipkin이 대표적이고, Spring에서는 Micrometer Tracing(구 Spring Cloud Sleuth)으로 자동 전파가 됩니다.
[Gateway] traceId=abc123 → [주문] traceId=abc123 → [결제] traceId=abc123
이 Trace ID만 있으면 요청의 전체 흐름을 한눈에 볼 수 있어요. 어디서 느려졌는지, 어디서 에러가 났는지 바로 파악할 수 있습니다.
DDD와 MSA의 관계 — Bounded Context
"MSA에서 서비스 경계를 어떻게 나누나요?" 이 질문에 DDD(Domain-Driven Design)의 Bounded Context를 모르면 답하기 어렵습니다.
Bounded Context는 특정 도메인 모델이 유효한 범위를 정의해요. 같은 "User"라는 개념이라도 인증 서비스에서의 User(이메일, 비밀번호)와 배송 서비스에서의 User(이름, 주소)는 다른 모델입니다. 각각의 맥락(Context)에서 의미가 달라요.
MSA에서 서비스 경계 = Bounded Context라고 봐도 크게 틀리지 않아요. 이렇게 나누면,
- 서비스 내부는 높은 응집도 (같은 도메인의 개념들이 모여 있어요)
- 서비스 간에는 낮은 결합도 (각자의 모델을 가집니다)
가 자연스럽게 달성됩니다.
핵심만 정리하면 "단순히 기능으로 쪼개는 게 아니라, 도메인 모델의 경계를 기준으로 서비스를 나눈다"는 거예요. 그래야 나중에 서비스 간 의존성이 꼬이지 않습니다.
언제 모놀리식이 더 나은가?
이 질문은 오히려 좋은 기회예요. MSA를 맹신하지 않는다는 걸 보여줄 수 있으니까요.
- ** 팀 규모가 작을 때 **: 개발자 5명이 MSA를 운영하면 인프라 관리에 시간을 다 씁니다.
- ** 도메인이 아직 불명확할 때 **: 서비스 경계를 잘못 나누면 나중에 재통합 비용이 커요.
- ** 빠른 프로토타이핑 **: MVP 단계에서는 모놀리식이 훨씬 빠릅니다.
- ** 강한 트랜잭션 정합성이 필요할 때 **: 금융 결산처럼 분산 트랜잭션으로는 보장하기 어려운 경우예요.
- ** 운영 역량이 부족할 때 **: K8s, 서비스 메시, 분산 트레이싱 — 이걸 운영할 역량이 없으면 MSA는 재앙입니다.
"Monolith First" 전략을 추천하는 이유가 여기에 있어요. 모놀리식으로 시작하고, 도메인 경계가 명확해지면 필요한 부분만 점진적으로 떼어내는 게 현실적으로 가장 안전한 접근입니다.