Split View: 마이크로서비스 아키텍처 2025 완전 가이드: 모놀리스에서 MSA로, 그리고 다시 모듈러 모놀리스로
마이크로서비스 아키텍처 2025 완전 가이드: 모놀리스에서 MSA로, 그리고 다시 모듈러 모놀리스로
- 들어가며
- 1. 아키텍처 진화의 역사
- 2. MSA가 과잉인 경우: 현실적인 교훈
- 3. 아키텍처 비교: 모놀리스 vs MSA vs 모듈러 모놀리스
- 4. DDD와 서비스 분해
- 5. 통신 패턴: REST vs gRPC vs 이벤트 기반
- 6. Service Mesh: Istio vs Linkerd
- 7. 분산 트랜잭션과 Saga 패턴
- 8. 관측성: OpenTelemetry
- 9. API Gateway 패턴
- 10. 배포 전략
- 11. 마이그레이션: Strangler Fig 패턴
- 12. 안티패턴
- 13. 의사결정 프레임워크
- 14. 면접 대비 Q&A (15선)
- Q1. 마이크로서비스의 핵심 특성 5가지를 설명하세요.
- Q2. CAP 정리와 마이크로서비스의 관계를 설명하세요.
- Q3. Saga 패턴의 두 가지 구현 방식과 차이점은?
- Q4. Service Mesh의 Sidecar 패턴을 설명하세요.
- Q5. 분산 추적(Distributed Tracing)의 원리를 설명하세요.
- Q6. Strangler Fig 패턴의 장점과 주의사항은?
- Q7. API Gateway와 Service Mesh의 차이는?
- Q8. Event Sourcing과 CQRS의 관계를 설명하세요.
- Q9. 마이크로서비스에서 데이터 정합성을 어떻게 보장하나요?
- Q10. gRPC가 REST보다 유리한 시나리오는?
- Q11. Circuit Breaker 패턴을 설명하세요.
- Q12. 서비스 디스커버리 방식을 비교하세요.
- Q13. 모듈러 모놀리스에서 MSA로 전환하는 기준은?
- Q14. Outbox 패턴이란 무엇인가요?
- Q15. 마이크로서비스 테스트 전략(Testing Pyramid)을 설명하세요.
- 15. 퀴즈
- 참고 자료
들어가며
2024년 Amazon Prime Video 팀이 마이크로서비스를 모놀리스로 되돌려 비용을 90% 절감했다는 발표는 업계에 큰 충격을 줬습니다. DHH(Ruby on Rails 창시자)는 "마이크로서비스는 대부분의 팀에게 과잉 엔지니어링"이라고 일갈했고, Shopify는 모듈러 모놀리스로 대규모 트래픽을 성공적으로 처리하고 있습니다.
그렇다고 마이크로서비스가 죽은 것은 아닙니다. Netflix, Uber, Spotify는 여전히 수천 개의 마이크로서비스를 운영하며, 조직 규모와 도메인 복잡성에 따라 MSA가 올바른 선택인 경우는 분명히 존재합니다.
이 글에서는 아키텍처 진화의 역사부터 2025년 현재의 트렌드까지, 모놀리스/MSA/모듈러 모놀리스를 객관적으로 비교하고, 실전에서 필요한 패턴과 기술을 체계적으로 다룹니다.
1. 아키텍처 진화의 역사
1.1 모놀리스 시대 (2000년대)
모든 기능이 하나의 배포 단위에 포함된 전통적인 아키텍처입니다.
┌─────────────────────────────────────┐
│ Monolith Application │
│ ┌─────────┐ ┌─────────┐ ┌───────┐ │
│ │ User │ │ Order │ │Payment│ │
│ │ Module │ │ Module │ │Module │ │
│ └────┬─────┘ └────┬────┘ └──┬────┘ │
│ └──────┬─────┴─────────┘ │
│ ┌────▼────┐ │
│ │ Shared │ │
│ │ DB │ │
│ └─────────┘ │
└─────────────────────────────────────┘
장점: 단순한 개발/배포, 트랜잭션 관리 용이, 디버깅 편리
단점: 코드베이스 비대화, 배포 병목, 기술 스택 고정, 확장성 한계
1.2 SOA 시대 (2005~2015)
Service-Oriented Architecture는 엔터프라이즈 서비스 버스(ESB)를 중심으로 서비스를 연결했습니다.
┌────────┐ ┌────────┐ ┌────────┐
│Service A│ │Service B│ │Service C│
└───┬─────┘ └───┬─────┘ └───┬─────┘
└──────────────┼──────────────┘
┌────▼────┐
│ ESB │
│(Message │
│ Bus) │
└─────────┘
SOA는 서비스 재사용성과 표준화를 추구했지만, ESB가 단일 장애점이 되고, SOAP/WSDL의 복잡성이 문제였습니다.
1.3 마이크로서비스 시대 (2014~현재)
Martin Fowler와 James Lewis가 정의한 마이크로서비스는 SOA의 진화형으로, 각 서비스가 독립적으로 배포/확장됩니다.
┌─────────┐ ┌─────────┐ ┌─────────┐
│ User │ │ Order │ │ Payment │
│ Service │ │ Service │ │ Service │
│ :8080 │ │ :8081 │ │ :8082 │
└──┬──────┘ └──┬──────┘ └──┬──────┘
│ REST/gRPC │ Event │
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐
│UserDB│ │OrderDB│ │PayDB │
└──────┘ └──────┘ └──────┘
1.4 2025년: 모듈러 모놀리스의 재부상
Timeline:
2000 ──── 2005 ──── 2014 ──── 2020 ──── 2025
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Monolith SOA Microservices 서비스 Modular
메시 확산 Monolith
재부상
2. MSA가 과잉인 경우: 현실적인 교훈
2.1 Amazon Prime Video 사례
2023년 Amazon Prime Video 팀은 오디오/비디오 모니터링 서비스를 마이크로서비스에서 모놀리스로 전환하여 인프라 비용을 90% 절감했습니다.
문제점:
- AWS Step Functions의 상태 전환 비용이 폭발적으로 증가
- 서비스 간 데이터 전달을 위한 S3 중간 저장 비용
- 마이크로서비스 오케스트레이션 오버헤드
해결:
- 단일 프로세스 내에서 모든 처리 단계를 실행
- 네트워크 호출 대신 메모리 내 통신
- S3 중간 저장 제거
2.2 DHH의 비판
Ruby on Rails 창시자 DHH는 "대부분의 팀은 모놀리스로 충분하다"라고 주장합니다.
| 주장 | 근거 |
|---|---|
| 분산 시스템 복잡성 과소평가 | 네트워크 장애, 데이터 정합성, 디버깅 난이도 |
| 팀 규모와 불일치 | 5~10명 팀이 20개 서비스 운영은 비효율 |
| 운영 비용 폭발 | 인프라, 모니터링, 배포 파이프라인 비용 |
| 조기 분리의 위험 | 도메인 이해 부족 시 잘못된 경계 설정 |
2.3 Shopify의 모듈러 모놀리스 성공
Shopify는 연간 수십조 원의 거래를 처리하면서도 모듈러 모놀리스 아키텍처를 사용합니다.
# Shopify의 Packwerk를 이용한 모듈 경계 정의
# packages/checkout/package.yml
enforce_dependencies: true
enforce_privacy: true
dependencies:
- packages/inventory
- packages/payment
3. 아키텍처 비교: 모놀리스 vs MSA vs 모듈러 모놀리스
3.1 비교 매트릭스
| 항목 | 모놀리스 | 모듈러 모놀리스 | 마이크로서비스 |
|---|---|---|---|
| 배포 단위 | 1개 | 1개 (모듈별 빌드) | N개 (서비스별) |
| 팀 규모 | 1~20명 | 5~50명 | 20~수백명 |
| 통신 | 메서드 호출 | 모듈 인터페이스 | 네트워크(REST/gRPC) |
| 데이터 저장소 | 공유 DB | 모듈별 스키마 | 서비스별 DB |
| 트랜잭션 | ACID | ACID (모듈 간 제한) | Saga/보상 |
| 확장성 | 수직 | 수직 + 부분 수평 | 수평 (서비스별) |
| 운영 복잡성 | 낮음 | 중간 | 높음 |
| 기술 다양성 | 단일 스택 | 단일 스택 | 폴리글랏 |
| 장애 격리 | 없음 | 부분적 | 완전 격리 |
| 초기 비용 | 낮음 | 중간 | 높음 |
3.2 모듈러 모놀리스 아키텍처 상세
┌─────────────────────────────────────────────┐
│ Modular Monolith │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ User │ │ Order │ │ Payment │ │
│ │ Module │ │ Module │ │ Module │ │
│ │ │ │ │ │ │ │
│ │ Public │ │ Public │ │ Public │ │
│ │ API │◄─► API │◄─► API │ │
│ │ │ │ │ │ │ │
│ │ Private │ │ Private │ │ Private │ │
│ │ Impl │ │ Impl │ │ Impl │ │
│ └──┬──────┘ └──┬──────┘ └──┬──────┘ │
│ │ │ │ │
│ ┌──▼──┐ ┌──▼──┐ ┌──▼──┐ │
│ │user │ │order│ │pay │ │
│ │schema│ │schema│ │schema│ │
│ └─────┘ └─────┘ └─────┘ │
│ └────────────┼────────────┘ │
│ ┌──────▼──────┐ │
│ │ Shared DB │ │
│ │ (분리 스키마)│ │
│ └─────────────┘ │
└─────────────────────────────────────────────┘
// Java 모듈러 모놀리스 예시 (Spring Modulith)
@ApplicationModule(
allowedDependencies = {"order", "shared"}
)
package com.example.payment;
// 모듈 간 통신은 이벤트로
@Service
public class PaymentService {
private final ApplicationEventPublisher events;
@Transactional
public PaymentResult processPayment(PaymentRequest request) {
Payment payment = Payment.create(request);
paymentRepository.save(payment);
// 모듈 간 이벤트 발행 (직접 의존 없음)
events.publishEvent(new PaymentCompletedEvent(
payment.getId(),
payment.getOrderId(),
payment.getAmount()
));
return PaymentResult.success(payment);
}
}
3.3 선택 기준 플로차트
시작: 새 프로젝트
│
▼
팀 규모가 50명 이상인가? ──Yes──▶ 도메인이 명확히 분리되어 있는가?
│ │
No Yes ──▶ MSA 고려
│ │
▼ No ──▶ 모듈러 모놀리스
독립적 확장이 필요한 서비스가 있는가?
│
Yes ──▶ 해당 서비스만 분리 (하이브리드)
│
No ──▶ 모놀리스 또는 모듈러 모놀리스
4. DDD와 서비스 분해
4.1 Bounded Context 식별
Domain-Driven Design에서 Bounded Context는 마이크로서비스의 자연스러운 경계입니다.
┌─────────────── E-Commerce Domain ───────────────┐
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Catalog │ │ Order │ │
│ │ Context │ │ Context │ │
│ │ │ │ │ │
│ │ - Product │ │ - Order │ │
│ │ - Category │ │ - OrderItem │ │
│ │ - Price │ │ - Shipment │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Identity │ │ Payment │ │
│ │ Context │ │ Context │ │
│ │ │ │ │ │
│ │ - User │ │ - Payment │ │
│ │ - Role │ │ - Refund │ │
│ │ - Permission │ │ - Invoice │ │
│ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────┘
4.2 Context Mapping 패턴
┌────────────┐ ┌────────────┐
│ Upstream │ │ Downstream │
│ Context │ ──Conformist────▶ │ Context │
│ │ │ │
│ (Order) │ ──ACL──────────▶ │ (Payment) │
│ │ │ │
│ │ ──OHS/PL───────▶ │ (Shipping) │
└────────────┘ └────────────┘
ACL: Anti-Corruption Layer (번역 계층)
OHS: Open Host Service (공개 API)
PL: Published Language (공유 스키마)
4.3 서비스 분해 전략
// 1. Aggregate 기준 분해
// Order Aggregate Root
public class Order {
private OrderId id;
private CustomerId customerId;
private List<OrderLine> lines;
private OrderStatus status;
private Money totalAmount;
// Aggregate 내부의 비즈니스 로직
public void addItem(ProductId productId, int quantity, Money price) {
OrderLine line = new OrderLine(productId, quantity, price);
this.lines.add(line);
this.totalAmount = calculateTotal();
}
public void confirm() {
if (this.lines.isEmpty()) {
throw new OrderException("빈 주문은 확정할 수 없습니다");
}
this.status = OrderStatus.CONFIRMED;
}
}
4.4 분해 시 피해야 할 실수
- 데이터 기준 분해: 테이블 단위로 서비스를 나누면 과도한 통신 발생
- 기술 기준 분해: 프론트엔드/백엔드/DB 레이어로 분리하면 MSA의 이점 없음
- 조기 분해: 도메인 이해가 부족할 때 분리하면 잘못된 경계 설정
- 너무 세분화: 나노서비스(Nanoservice)는 운영 부담만 증가
5. 통신 패턴: REST vs gRPC vs 이벤트 기반
5.1 동기 통신 비교
| 항목 | REST (HTTP/JSON) | gRPC (HTTP/2 + Protobuf) |
|---|---|---|
| 직렬화 | JSON (텍스트) | Protocol Buffers (바이너리) |
| 성능 | 상대적 느림 | 2~10배 빠름 |
| 스트리밍 | 제한적 (SSE/WebSocket) | 네이티브 양방향 스트리밍 |
| 코드 생성 | OpenAPI (선택적) | 필수 (proto 파일) |
| 브라우저 지원 | 네이티브 | gRPC-Web 필요 |
| 가독성 | 높음 (JSON) | 낮음 (바이너리) |
| 사용 케이스 | 외부 API, 단순 CRUD | 서비스 간 내부 통신 |
5.2 gRPC 서비스 정의
// order_service.proto
syntax = "proto3";
package order.v1;
service OrderService {
// 단항 RPC
rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
// 서버 스트리밍 - 주문 상태 업데이트
rpc WatchOrderStatus (WatchRequest) returns (stream OrderStatusUpdate);
// 클라이언트 스트리밍 - 대량 주문 등록
rpc BulkCreateOrders (stream CreateOrderRequest) returns (BulkCreateResponse);
// 양방향 스트리밍 - 실시간 주문 처리
rpc ProcessOrders (stream OrderAction) returns (stream OrderResult);
}
message CreateOrderRequest {
string customer_id = 1;
repeated OrderItem items = 2;
ShippingAddress shipping = 3;
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
int64 price_cents = 3;
}
message CreateOrderResponse {
string order_id = 1;
OrderStatus status = 2;
int64 total_cents = 3;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_CONFIRMED = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_DELIVERED = 4;
}
5.3 이벤트 기반 비동기 통신
Producer Consumer
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Order │───▶│ Kafka │───▶│ Payment │
│ Service │ │ Topic: │ │ Service │
│ │ │ orders │ │ │
└─────────┘ └─────────┘ └─────────┘
│
├────────▶ ┌─────────┐
│ │Inventory│
│ │ Service │
│ └─────────┘
│
└────────▶ ┌─────────┐
│Notific- │
│ation │
│ Service │
└─────────┘
// Kafka Producer - 주문 이벤트 발행
@Service
public class OrderEventPublisher {
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
public void publishOrderCreated(Order order) {
OrderCreatedEvent event = OrderCreatedEvent.builder()
.orderId(order.getId())
.customerId(order.getCustomerId())
.items(order.getItems())
.totalAmount(order.getTotalAmount())
.timestamp(Instant.now())
.build();
kafkaTemplate.send("orders.created", order.getId(), event)
.whenComplete((result, ex) -> {
if (ex != null) {
log.error("이벤트 발행 실패: orderId={}",
order.getId(), ex);
// 보상 로직 또는 재시도
}
});
}
}
// Kafka Consumer - 결제 처리
@Service
public class PaymentEventConsumer {
@KafkaListener(
topics = "orders.created",
groupId = "payment-service",
containerFactory = "kafkaListenerContainerFactory"
)
public void handleOrderCreated(OrderCreatedEvent event) {
log.info("주문 이벤트 수신: orderId={}", event.getOrderId());
paymentService.processPayment(event);
}
}
5.4 통신 패턴 선택 가이드
동기 호출이 필요한가?
│
Yes ──▶ 외부 클라이언트 대상인가?
│ │
│ Yes ──▶ REST (OpenAPI)
│ │
│ No ──▶ 고성능 필요? ──Yes──▶ gRPC
│ │
│ No ──▶ REST
│
No ──▶ 순서 보장이 필요한가?
│
Yes ──▶ Kafka (파티션 키로 순서 보장)
│
No ──▶ 팬아웃이 필요한가?
│
Yes ──▶ SNS + SQS / Kafka 토픽
│
No ──▶ SQS / RabbitMQ
6. Service Mesh: Istio vs Linkerd
6.1 Service Mesh란?
서비스 메시는 마이크로서비스 간 통신을 인프라 레벨에서 관리하는 전용 계층입니다.
Without Service Mesh: With Service Mesh:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│Service A│──│Service B│ │Service A│ │Service B│
│ │ │ │ │ ┌─────┐ │ │ ┌─────┐ │
│(retry, │ │(retry, │ │ │Proxy│─┼──┼─│Proxy│ │
│ auth, │ │ auth, │ │ │(side│ │ │ │(side│ │
│ metrics)│ │ metrics)│ │ │car) │ │ │ │car) │ │
└─────────┘ └─────────┘ │ └─────┘ │ │ └─────┘ │
└─────────┘ └─────────┘
모든 서비스에 통신 관심사를
통신 로직 구현 인프라로 위임
6.2 주요 기능
| 기능 | 설명 |
|---|---|
| 트래픽 관리 | 로드 밸런싱, 라우팅, 카나리 배포 |
| 보안 | mTLS 자동 적용, 인증/인가 |
| 관측성 | 분산 추적, 메트릭 수집, 로그 |
| 복원력 | 재시도, 서킷 브레이커, 타임아웃 |
6.3 Istio vs Linkerd 비교
| 항목 | Istio | Linkerd |
|---|---|---|
| 프록시 | Envoy (C++) | linkerd2-proxy (Rust) |
| 리소스 사용량 | 높음 (100~200MB/pod) | 낮음 (20~30MB/pod) |
| 기능 범위 | 포괄적 (VM 지원 등) | 핵심 기능 집중 |
| 학습 곡선 | 가파름 | 완만함 |
| mTLS | 수동 설정 필요 | 기본 활성화 |
| CNCF 등급 | 졸업 프로젝트 | 졸업 프로젝트 |
| 권장 규모 | 대규모 (100+ 서비스) | 중소규모 (10~100 서비스) |
6.4 Istio 트래픽 관리 예시
# VirtualService - 카나리 배포
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
- match:
- headers:
x-canary:
exact: "true"
route:
- destination:
host: order-service
subset: v2
- route:
- destination:
host: order-service
subset: v1
weight: 90
- destination:
host: order-service
subset: v2
weight: 10
---
# DestinationRule - 서킷 브레이커
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: order-service
spec:
host: order-service
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
http:
h2UpgradePolicy: DEFAULT
http1MaxPendingRequests: 100
http2MaxRequests: 1000
outlierDetection:
consecutive5xxErrors: 5
interval: 30s
baseEjectionTime: 30s
maxEjectionPercent: 50
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
6.5 Service Mesh 없이도 되는 경우
- 서비스 수가 10개 미만
- Kubernetes의 기본 기능으로 충분할 때
- 팀에 서비스 메시 운영 경험이 없을 때
- 지연 시간에 매우 민감한 시스템 (사이드카 오버헤드)
7. 분산 트랜잭션과 Saga 패턴
7.1 분산 트랜잭션의 문제
마이크로서비스에서는 각 서비스가 자체 DB를 가지므로, 여러 서비스에 걸친 ACID 트랜잭션이 불가능합니다.
주문 생성 시나리오:
1. Order Service: 주문 생성
2. Inventory Service: 재고 차감
3. Payment Service: 결제 처리
4. Notification Service: 알림 발송
만약 3단계에서 결제 실패하면?
-> 1, 2단계 롤백 필요!
7.2 Saga 패턴 - 코레오그래피 방식
┌─────────┐ OrderCreated ┌─────────┐ StockReserved ┌─────────┐
│ Order │──────────────▶│Inventory│───────────────▶│ Payment │
│ Service │ │ Service │ │ Service │
└─────────┘ └─────────┘ └─────────┘
▲ ▲ │
│ StockRelease │ PaymentFailed │
│ (보상) │◀──────────────────────────┘
│◀────────────────────────┘
│ OrderCancelled (보상)
각 서비스가 이벤트를 구독하고 자체적으로 다음 단계를 트리거합니다.
7.3 Saga 패턴 - 오케스트레이션 방식
┌────────────────┐
│ Order Saga │
│ Orchestrator │
└───┬──┬──┬──┬──┘
1.주문생성 │ │ │ │ 4.알림
┌──────┘ │ │ └──────┐
▼ │ │ ▼
┌─────────┐ │ │ ┌──────────┐
│ Order │ │ │ │Notificat-│
│ Service │ │ │ │ion Svc │
└─────────┘ │ │ └──────────┘
2.재고차감 │ │ 3.결제
┌─────┘ └─────┐
▼ ▼
┌─────────┐ ┌─────────┐
│Inventory│ │ Payment │
│ Service │ │ Service │
└─────────┘ └─────────┘
// Saga Orchestrator 구현
@Service
public class OrderSagaOrchestrator {
public Mono<OrderResult> createOrder(CreateOrderCommand command) {
return Mono.just(new SagaState(command))
// Step 1: 주문 생성
.flatMap(state -> orderService.createOrder(state.getCommand())
.map(order -> state.withOrder(order)))
// Step 2: 재고 예약
.flatMap(state -> inventoryService
.reserveStock(state.getOrder())
.map(reservation -> state.withReservation(reservation))
.onErrorResume(e -> compensateOrder(state, e)))
// Step 3: 결제 처리
.flatMap(state -> paymentService
.processPayment(state.getOrder())
.map(payment -> state.withPayment(payment))
.onErrorResume(e ->
compensateReservation(state, e)))
// Step 4: 완료
.flatMap(state -> {
orderService.confirmOrder(
state.getOrder().getId());
return Mono.just(OrderResult.success(state));
});
}
private Mono<SagaState> compensateReservation(
SagaState state, Throwable e) {
log.warn("결제 실패, 재고 보상 시작: orderId={}",
state.getOrder().getId());
return inventoryService
.releaseStock(state.getReservation())
.then(compensateOrder(state, e));
}
private Mono<SagaState> compensateOrder(
SagaState state, Throwable e) {
log.warn("주문 보상 시작: orderId={}",
state.getOrder().getId());
return orderService
.cancelOrder(state.getOrder().getId())
.then(Mono.error(
new SagaFailedException("Saga 실패", e)));
}
}
7.4 코레오그래피 vs 오케스트레이션
| 항목 | 코레오그래피 | 오케스트레이션 |
|---|---|---|
| 결합도 | 느슨함 | 중앙 조정자 의존 |
| 복잡도 | 서비스 수 증가 시 높음 | 일정하게 유지 |
| 디버깅 | 어려움 (이벤트 추적) | 비교적 쉬움 |
| 단일 장애점 | 없음 | 오케스트레이터 |
| 적합한 경우 | 3~4단계 이하 단순 플로우 | 5+ 단계 복잡 플로우 |
8. 관측성: OpenTelemetry
8.1 관측성의 3가지 축
┌─────────────┐
│ Observability│
└──────┬──────┘
┌──────────┼──────────┐
▼ ▼ ▼
┌─────────┐ ┌──────┐ ┌─────────┐
│ Logs │ │Metrics│ │ Traces │
│(이벤트) │ │(수치) │ │(요청흐름)│
└─────────┘ └──────┘ └─────────┘
무엇이 얼마나 어디서
발생했는가 발생하는가 발생했는가
8.2 OpenTelemetry 통합 아키텍처
┌─────────────────────────────────────────────────────┐
│ Application │
│ ┌─────────────────────────────────────────────┐ │
│ │ OpenTelemetry SDK │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Tracer │ │ Meter │ │ Logger │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └────────────────────┬────────────────────────┘ │
└───────────────────────┼─────────────────────────────┘
│ OTLP
▼
┌─────────────────┐
│ OTel Collector │
│ ┌───────────┐ │
│ │ Receivers │ │
│ │Processors│ │
│ │ Exporters │ │
│ └───────────┘ │
└────┬──┬──┬──────┘
│ │ │
┌────────┘ │ └────────┐
▼ ▼ ▼
┌─────────┐ ┌────────┐ ┌─────────┐
│ Jaeger │ │Promethe│ │ Loki │
│ (Traces)│ │us/Mimir│ │ (Logs) │
│ │ │(Metrics│ │ │
└─────────┘ └────────┘ └─────────┘
│ │ │
└─────┬─────┘───────────┘
▼
┌───────────┐
│ Grafana │
│(Dashboard)│
└───────────┘
8.3 분산 추적 구현
// Spring Boot + OpenTelemetry 자동 설정
// build.gradle
// implementation 'io.opentelemetry.instrumentation:
// opentelemetry-spring-boot-starter'
// 커스텀 스팬 생성
@Service
public class OrderService {
private final Tracer tracer;
public Order createOrder(CreateOrderRequest request) {
Span span = tracer.spanBuilder("order.create")
.setAttribute("order.customer_id",
request.getCustomerId())
.setAttribute("order.item_count",
request.getItems().size())
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 주문 생성 로직
Order order = processOrder(request);
span.setAttribute("order.id", order.getId());
span.setAttribute("order.total",
order.getTotal().doubleValue());
span.setStatus(StatusCode.OK);
return order;
} catch (Exception e) {
span.setStatus(StatusCode.ERROR, e.getMessage());
span.recordException(e);
throw e;
} finally {
span.end();
}
}
}
8.4 핵심 메트릭 (RED/USE)
RED Method (서비스 관점):
┌──────────────────────────────────────┐
│ R - Rate: 초당 요청 수 │
│ E - Errors: 에러율 (%) │
│ D - Duration: 응답 시간 (p50/p95/p99)│
└──────────────────────────────────────┘
USE Method (리소스 관점):
┌──────────────────────────────────────┐
│ U - Utilization: 리소스 사용률 │
│ S - Saturation: 대기열 길이 │
│ E - Errors: 리소스 에러 수 │
└──────────────────────────────────────┘
9. API Gateway 패턴
9.1 API Gateway의 역할
┌─────────────────────┐
│ API Gateway │
Client ────────▶│ │
│ - 인증/인가 │
│ - Rate Limiting │
│ - 요청 라우팅 │
│ - 로드 밸런싱 │
│ - 응답 캐싱 │
│ - 요청/응답 변환 │
│ - Circuit Breaker │
│ - 로깅/모니터링 │
└──┬──────┬──────┬────┘
│ │ │
▼ ▼ ▼
┌─────┐┌─────┐┌─────┐
│Svc A││Svc B││Svc C│
└─────┘└─────┘└─────┘
9.2 BFF (Backend for Frontend) 패턴
┌────────┐ ┌──────────────┐
│ Web │────▶│ Web BFF │──┐
│ Client │ │(GraphQL) │ │
└────────┘ └──────────────┘ │
│ ┌──────────┐
┌────────┐ ┌──────────────┐ ├───▶│ Order │
│ Mobile │────▶│ Mobile BFF │──┤ │ Service │
│ App │ │(REST, 경량) │ │ └──────────┘
└────────┘ └──────────────┘ │
│ ┌──────────┐
┌────────┐ ┌──────────────┐ ├───▶│ User │
│ IoT │────▶│ IoT BFF │──┤ │ Service │
│ Device │ │(MQTT 변환) │ │ └──────────┘
└────────┘ └──────────────┘ │
│ ┌──────────┐
└───▶│ Product │
│ Service │
└──────────┘
9.3 주요 API Gateway 비교
| 항목 | Kong | AWS API Gateway | Envoy Gateway | APISIX |
|---|---|---|---|---|
| 기반 | Nginx + Lua | AWS 관리형 | Envoy Proxy | Nginx + Lua |
| 배포 | 셀프호스팅/클라우드 | AWS 전용 | Kubernetes | 셀프호스팅 |
| 프로토콜 | REST, gRPC, WS | REST, WS, HTTP | REST, gRPC | REST, gRPC, MQTT |
| 플러그인 | 풍부함 | Lambda 통합 | 확장 가능 | 풍부함 |
| 가격 | 오픈소스/엔터프라이즈 | 요청당 과금 | 오픈소스 | 오픈소스 |
10. 배포 전략
10.1 블루-그린 배포
Phase 1: Blue(현재) 운영 중
┌─────────────┐ ┌──────────┐
│ Load │────▶│ Blue v1 │ (100% 트래픽)
│ Balancer │ │ (Active) │
└─────────────┘ └──────────┘
┌──────────┐
│ Green v2 │ (대기 중)
│ (Idle) │
└──────────┘
Phase 2: Green으로 전환
┌─────────────┐ ┌──────────┐
│ Load │ │ Blue v1 │ (대기)
│ Balancer │ │ (Idle) │
└─────────────┘ └──────────┘
│ ┌──────────┐
└───────────▶│ Green v2 │ (100% 트래픽)
│ (Active) │
└──────────┘
10.2 카나리 배포
# Kubernetes + Argo Rollouts 카나리 배포
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: order-service
spec:
replicas: 10
strategy:
canary:
steps:
- setWeight: 5
- pause:
duration: 5m
- analysis:
templates:
- templateName: success-rate
args:
- name: service-name
value: order-service
- setWeight: 25
- pause:
duration: 10m
- analysis:
templates:
- templateName: success-rate
- setWeight: 50
- pause:
duration: 15m
- setWeight: 100
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: order-service:v2
ports:
- containerPort: 8080
10.3 배포 전략 비교
| 전략 | 다운타임 | 롤백 속도 | 리소스 비용 | 위험도 |
|---|---|---|---|---|
| 롤링 업데이트 | 없음 | 보통 | 낮음 | 중간 |
| 블루-그린 | 없음 | 즉시 | 높음 (2배) | 낮음 |
| 카나리 | 없음 | 빠름 | 중간 | 낮음 |
| A/B 테스트 | 없음 | 빠름 | 중간 | 낮음 |
| 재생성 | 있음 | 느림 | 낮음 | 높음 |
11. 마이그레이션: Strangler Fig 패턴
11.1 패턴 개요
교살자 무화과 패턴은 레거시 모놀리스를 점진적으로 마이크로서비스로 전환하는 전략입니다. 무화과나무가 숙주 나무를 감싸며 자라듯이, 새로운 서비스가 레거시를 점진적으로 대체합니다.
Phase 1: 프록시 배치
┌────────┐ ┌──────────┐ ┌────────────┐
│ Client │────▶│ Proxy │────▶│ Monolith │
└────────┘ │(Facade) │ │ (모든기능) │
└──────────┘ └────────────┘
Phase 2: 일부 기능 분리
┌────────┐ ┌──────────┐ ┌────────────┐
│ Client │────▶│ Proxy │─┬──▶│ Monolith │
└────────┘ │ │ │ │ (기능 축소) │
└──────────┘ │ └────────────┘
│
└──▶┌────────────┐
│ New Service│
│ (분리된기능)│
└────────────┘
Phase 3: 대부분 기능 이관
┌────────┐ ┌──────────┐ ┌────────────┐
│ Client │────▶│ Proxy │─┬──▶│ Monolith │
└────────┘ │ │ │ │ (최소 기능) │
└──────────┘ │ └────────────┘
│
├──▶┌────────────┐
│ │ Service A │
│ └────────────┘
├──▶┌────────────┐
│ │ Service B │
│ └────────────┘
└──▶┌────────────┐
│ Service C │
└────────────┘
Phase 4: 모놀리스 제거
┌────────┐ ┌──────────┐
│ Client │────▶│ API GW │─┬──▶ Service A
└────────┘ └──────────┘ ├──▶ Service B
└──▶ Service C
11.2 마이그레이션 단계
1. 분석 (2~4주)
├── 도메인 모델링 (Event Storming)
├── 의존성 분석 (코드/데이터)
└── 우선순위 결정
2. 기반 구축 (4~8주)
├── CI/CD 파이프라인
├── 컨테이너/오케스트레이션 환경
├── 관측성 스택
└── API Gateway 배치
3. 첫 서비스 분리 (4~6주)
├── 가장 독립적인 도메인 선택
├── Anti-Corruption Layer 구현
├── 데이터 마이그레이션
└── 듀얼 라이트 / 이벤트 브리지
4. 반복 분리 (서비스당 2~4주)
├── 다음 서비스 분리
├── 모놀리스 코드 제거
└── 통합 테스트
5. 모놀리스 제거
├── 잔여 기능 이관
├── 데이터 정리
└── 인프라 해제
11.3 데이터 마이그레이션 전략
// Dual Write 패턴 - 전환 기간 동안 양쪽에 쓰기
@Service
public class UserServiceMigration {
private final MonolithUserRepository monolithRepo;
private final NewUserServiceClient newServiceClient;
private final FeatureFlag featureFlag;
public User createUser(CreateUserRequest request) {
// 항상 모놀리스에 쓰기 (기존)
User user = monolithRepo.save(toEntity(request));
// 새 서비스에도 쓰기 (마이그레이션)
try {
newServiceClient.createUser(
toNewServiceRequest(user));
} catch (Exception e) {
log.warn("새 서비스 동기화 실패, 나중에 재시도", e);
migrationQueue.enqueue(new SyncEvent(user));
}
return user;
}
public User getUser(String userId) {
// Feature Flag로 읽기 전환
if (featureFlag.isEnabled("read-from-new-service")) {
try {
return newServiceClient.getUser(userId);
} catch (Exception e) {
log.warn("새 서비스 읽기 실패, 폴백", e);
return monolithRepo.findById(userId);
}
}
return monolithRepo.findById(userId);
}
}
12. 안티패턴
12.1 대표적인 MSA 안티패턴
| 안티패턴 | 설명 | 해결책 |
|---|---|---|
| 분산 모놀리스 | 서비스를 나눴지만 강결합 유지 | Bounded Context 재설정 |
| 나노서비스 | 너무 작은 서비스 (함수 단위) | 서비스 병합, 관련 기능 그룹화 |
| 공유 DB | 여러 서비스가 같은 DB 직접 접근 | DB per Service 원칙 |
| 동기 체인 | A에서 B, C, D 순차 동기 호출 | 이벤트 기반 비동기화 |
| 골든 해머 | 모든 문제에 MSA 적용 | 적합성 평가 후 아키텍처 선택 |
| 버전 지옥 | API 버전이 난립 | 하위 호환성 유지, 계약 테스트 |
12.2 분산 모놀리스 징후
분산 모놀리스 체크리스트:
[ ] 하나의 서비스를 배포하려면 다른 서비스도 함께 배포해야 한다
[ ] 서비스 간 동기 호출이 3단계 이상 연쇄된다
[ ] 여러 서비스가 같은 데이터베이스를 직접 읽고 쓴다
[ ] 서비스 간 공유 라이브러리의 버전을 맞춰야 한다
[ ] 하나의 서비스 장애가 전체 시스템을 다운시킨다
[ ] 서비스 경계가 기술 레이어(프론트/백/DB)로 나뉘어 있다
3개 이상 해당되면 분산 모놀리스일 가능성이 높습니다.
12.3 동기 호출 체인 문제
문제: 동기 체인
Client -> Order -> Inventory -> Payment -> Shipping
(3초)
전체 응답시간: 3초 + 각 서비스 처리시간
하나라도 실패하면 전체 실패
해결: 비동기 이벤트 + 응답은 즉시
Client -> Order (200 OK, 주문 접수됨)
|
+-- Event: OrderCreated
| |
| +-- Inventory (비동기 처리)
| +-- Payment (비동기 처리)
| +-- Shipping (비동기 처리)
|
+-- 상태 조회 API 제공
13. 의사결정 프레임워크
13.1 아키텍처 선택 매트릭스
다음 질문에 점수를 매기세요 (1~5점).
| 질문 | 1점 (낮음) | 5점 (높음) |
|---|---|---|
| 팀 규모 | 5명 이하 | 50명 이상 |
| 도메인 복잡성 | 단순 CRUD | 복잡한 비즈니스 로직 |
| 독립적 확장 필요성 | 전체 동일 부하 | 서비스별 극도로 다른 부하 |
| 배포 빈도 | 월 1회 | 일 수십 회 |
| 기술 다양성 필요 | 단일 스택 | 서비스별 최적 기술 필요 |
| 팀 자율성 | 중앙 집중 | 팀별 독립 운영 |
| 운영 역량 | DevOps 초보 | 성숙한 플랫폼 팀 |
점수 해석:
- 7~15점: 모놀리스 (또는 모듈러 모놀리스)
- 16~25점: 모듈러 모놀리스 (또는 하이브리드)
- 26~35점: 마이크로서비스
13.2 기술 스택 추천
모놀리스:
├── Spring Boot + JPA
├── Django + PostgreSQL
├── Rails + PostgreSQL
└── Next.js Full Stack
모듈러 모놀리스:
├── Spring Modulith
├── .NET Aspire
├── Go (모듈 패턴)
└── Rust (Workspace)
마이크로서비스:
├── 통신: gRPC (내부) + REST (외부)
├── 메시징: Kafka / RabbitMQ
├── 오케스트레이션: Kubernetes
├── 서비스 메시: Istio / Linkerd
├── 관측성: OpenTelemetry + Grafana
├── CI/CD: ArgoCD / GitHub Actions
└── API Gateway: Kong / Envoy Gateway
14. 면접 대비 Q&A (15선)
Q1. 마이크로서비스의 핵심 특성 5가지를 설명하세요.
- 단일 책임: 각 서비스가 하나의 비즈니스 기능 담당
- 독립 배포: 다른 서비스와 무관하게 배포 가능
- 분산 데이터: 서비스별 자체 데이터 저장소 보유
- 기술 다양성: 서비스별 최적의 기술 스택 선택 가능
- 장애 격리: 한 서비스 장애가 전체 시스템에 전파되지 않음
Q2. CAP 정리와 마이크로서비스의 관계를 설명하세요.
CAP 정리에 따르면 분산 시스템은 일관성(Consistency), 가용성(Availability), 분할 내성(Partition Tolerance) 중 2가지만 보장할 수 있습니다. 마이크로서비스는 네트워크 분할이 불가피하므로 P를 항상 선택해야 하며, AP(가용성 우선) 또는 CP(일관성 우선) 중 선택합니다. 대부분의 MSA는 최종 일관성(Eventual Consistency)을 수용하는 AP 시스템을 선택합니다.
Q3. Saga 패턴의 두 가지 구현 방식과 차이점은?
코레오그래피: 각 서비스가 이벤트를 발행/구독하여 자율적으로 다음 단계를 수행합니다. 중앙 조정자가 없어 결합도가 낮지만, 서비스가 많아지면 이벤트 흐름 추적이 어렵습니다.
오케스트레이션: 중앙 오케스트레이터가 각 서비스를 순서대로 호출하고 보상 로직을 관리합니다. 플로우가 명확하지만 오케스트레이터가 단일 장애점이 될 수 있습니다.
Q4. Service Mesh의 Sidecar 패턴을 설명하세요.
각 서비스 Pod에 프록시 컨테이너(사이드카)를 함께 배치합니다. 서비스의 모든 인바운드/아웃바운드 트래픽이 사이드카를 통과하며, 인증(mTLS), 라우팅, 재시도, 관측성 등의 횡단 관심사를 애플리케이션 코드 수정 없이 처리합니다. Istio는 Envoy 프록시를, Linkerd는 linkerd2-proxy를 사이드카로 사용합니다.
Q5. 분산 추적(Distributed Tracing)의 원리를 설명하세요.
요청이 시스템에 진입할 때 고유한 Trace ID가 생성됩니다. 각 서비스는 이 Trace ID를 전파하며, 자신의 처리 단위를 Span으로 기록합니다. 부모-자식 관계로 연결된 Span들이 모여 하나의 Trace를 구성하고, 이를 통해 전체 요청 경로와 각 구간의 소요 시간을 시각화할 수 있습니다. W3C Trace Context 표준 헤더를 사용합니다.
Q6. Strangler Fig 패턴의 장점과 주의사항은?
장점: 점진적 전환으로 위험 최소화, 빅뱅 마이그레이션 회피, 모놀리스와 새 서비스 공존 가능, 롤백 용이
주의사항: 전환 기간 동안 두 시스템 동시 운영 비용, 데이터 동기화 복잡성, 라우팅 규칙 관리 부담, 전환 완료 시점 결정의 어려움
Q7. API Gateway와 Service Mesh의 차이는?
API Gateway: 외부 트래픽(North-South)을 관리합니다. 인증, Rate Limiting, 요청 변환, API 집약 등 에지 기능을 담당합니다.
Service Mesh: 내부 서비스 간 트래픽(East-West)을 관리합니다. mTLS, 서비스 디스커버리, 로드 밸런싱, 서킷 브레이커 등 서비스 간 통신을 담당합니다.
둘은 보완 관계이며, 대규모 MSA에서는 함께 사용합니다.
Q8. Event Sourcing과 CQRS의 관계를 설명하세요.
Event Sourcing: 상태를 직접 저장하는 대신, 상태 변경 이벤트의 시퀀스를 저장합니다. 현재 상태는 이벤트를 리플레이하여 재구성합니다.
CQRS: 명령(쓰기)과 조회(읽기) 모델을 분리합니다. 쓰기는 이벤트 스토어에, 읽기는 최적화된 뷰(Read Model)에서 처리합니다. Event Sourcing은 CQRS 없이도 사용 가능하지만, 함께 사용하면 쓰기 성능과 읽기 성능을 독립적으로 최적화할 수 있습니다.
Q9. 마이크로서비스에서 데이터 정합성을 어떻게 보장하나요?
강한 일관성 대신 최종 일관성(Eventual Consistency)을 수용합니다. 구체적 패턴으로는 Saga 패턴(분산 트랜잭션), Outbox 패턴(이벤트 발행 보장), Change Data Capture(DB 변경 감지), 멱등성 보장(중복 처리 안전)을 사용합니다. 데이터 정합성이 반드시 필요한 경우에는 해당 기능들을 같은 서비스에 배치합니다.
Q10. gRPC가 REST보다 유리한 시나리오는?
서비스 간 내부 통신에서 높은 처리량이 필요할 때, 양방향 스트리밍이 필요할 때, 강타입 계약(proto 파일)이 중요할 때, 바이너리 직렬화로 페이로드 크기를 줄여야 할 때 유리합니다. 반면, 브라우저 클라이언트, 서드파티 개발자용 공개 API, 단순 CRUD에서는 REST가 더 적합합니다.
Q11. Circuit Breaker 패턴을 설명하세요.
전기 회로의 차단기처럼, 하위 서비스 장애 시 호출을 차단하여 장애 전파를 방지합니다. 세 가지 상태가 있습니다. Closed(정상 호출), Open(호출 차단, 즉시 실패 반환), Half-Open(일부 요청으로 복구 확인). 연속 실패가 임계값을 넘으면 Open으로 전환하고, 일정 시간 후 Half-Open에서 복구를 테스트합니다.
Q12. 서비스 디스커버리 방식을 비교하세요.
클라이언트 사이드 디스커버리: 클라이언트가 서비스 레지스트리(Eureka, Consul)에 직접 질의하여 대상 인스턴스를 선택합니다. 로드 밸런싱 로직이 클라이언트에 있습니다.
서버 사이드 디스커버리: 로드 밸런서가 서비스 레지스트리를 참조하여 라우팅합니다. Kubernetes의 Service가 대표적입니다. 클라이언트가 디스커버리 로직을 알 필요가 없어 더 단순합니다.
Q13. 모듈러 모놀리스에서 MSA로 전환하는 기준은?
다음 조건이 충족될 때 전환을 고려합니다. 특정 모듈의 확장 요구가 다른 모듈과 현저히 다를 때, 팀 규모가 50명 이상으로 성장하여 독립 배포가 필요할 때, 특정 모듈에 다른 기술 스택이 필요할 때, 장애 격리가 반드시 필요한 비즈니스 요구가 있을 때입니다.
Q14. Outbox 패턴이란 무엇인가요?
로컬 트랜잭션에서 비즈니스 데이터와 함께 이벤트를 Outbox 테이블에 저장합니다. 별도의 프로세스(Polling Publisher 또는 CDC)가 Outbox 테이블에서 이벤트를 읽어 메시지 브로커에 발행합니다. 이를 통해 비즈니스 로직과 이벤트 발행의 원자성을 보장합니다.
Q15. 마이크로서비스 테스트 전략(Testing Pyramid)을 설명하세요.
단위 테스트(서비스 내부 로직), 통합 테스트(DB/외부 연동), 계약 테스트(서비스 간 API 호환성, Pact 등), 컴포넌트 테스트(단일 서비스 E2E), E2E 테스트(전체 시스템)로 구성합니다. 계약 테스트가 특히 중요한데, 프로바이더 서비스의 변경이 컨슈머를 깨뜨리지 않는지 자동으로 검증합니다.
15. 퀴즈
Q1. 모듈러 모놀리스의 주요 장점 3가지와 MSA 대비 한계 2가지를 설명하세요.
장점: (1) MSA의 복잡성(네트워크 통신, 분산 트랜잭션, 서비스 메시) 없이 모듈 단위 분리 가능, (2) ACID 트랜잭션 사용 가능으로 데이터 정합성 보장 용이, (3) 단일 배포 단위로 운영/배포 간편.
한계: (1) 서비스별 독립 확장 불가(특정 모듈만 수평 확장 어려움), (2) 기술 스택이 단일 언어/프레임워크로 제한됨.
Q2. Saga 패턴에서 보상 트랜잭션(Compensating Transaction)이 실패하면 어떻게 처리하나요?
보상 트랜잭션 자체의 실패는 시스템을 비정합 상태로 만들 수 있어 매우 중요한 문제입니다. 해결 방안: (1) 재시도 정책(지수 백오프)으로 보상 작업을 반복 시도, (2) Dead Letter Queue에 실패한 보상 이벤트를 저장하여 수동/자동 복구, (3) 보상 트랜잭션을 멱등하게 설계하여 안전한 재시도 보장, (4) 모니터링/알림으로 운영팀에 즉시 통보하여 수동 개입.
Q3. Service Mesh 도입 시 사이드카 프록시의 오버헤드를 최소화하는 방법은?
(1) 경량 프록시 선택 (Linkerd의 Rust 기반 프록시는 Envoy 대비 메모리 사용량이 크게 적음), (2) 사이드카 리소스 요청/제한을 적절히 설정 (CPU 100m, Memory 50Mi 등), (3) 메시 기능 중 필요한 것만 활성화 (불필요한 텔레메트리 비활성화), (4) eBPF 기반 서비스 메시(Cilium Service Mesh) 고려 - 사이드카 없이 커널 레벨에서 처리, (5) 지연시간에 극도로 민감한 서비스는 메시에서 제외.
Q4. Strangler Fig 패턴 적용 시 가장 먼저 분리해야 할 서비스를 고르는 기준은?
(1) 다른 모듈과의 의존성이 가장 적은 기능 (독립적으로 동작 가능), (2) 비즈니스 가치가 높아 팀의 동기 부여에 도움, (3) 잘 정의된 도메인 경계가 존재, (4) 독립적 확장이나 별도 기술이 필요한 영역, (5) 테스트가 충분하여 동작 검증이 용이. 반대로 핵심 결제, 인증 같은 고위험 영역은 나중에 분리하는 것이 안전합니다.
Q5. OpenTelemetry에서 Context Propagation이 중요한 이유와 동작 방식을 설명하세요.
Context Propagation은 분산 환경에서 하나의 요청을 추적하기 위해 Trace ID와 Span ID를 서비스 간에 전달하는 메커니즘입니다. 중요한 이유: 이것 없이는 각 서비스의 로그/메트릭/트레이스가 분리되어 요청 흐름 파악이 불가능합니다. 동작 방식: (1) 첫 서비스에서 Trace ID 생성, (2) HTTP 헤더(W3C traceparent) 또는 gRPC 메타데이터로 다음 서비스에 전파, (3) 메시지 큐 환경에서는 메시지 헤더에 컨텍스트 포함, (4) 각 서비스에서 수신한 컨텍스트를 바탕으로 자식 Span 생성.
참고 자료
- Martin Fowler, "Microservices" - https://martinfowler.com/articles/microservices.html
- Sam Newman, "Building Microservices, 2nd Edition" (O'Reilly, 2021)
- Chris Richardson, "Microservices Patterns" (Manning, 2018)
- Microservices.io - Patterns - https://microservices.io/patterns/index.html
- Amazon Prime Video Tech Blog, "Scaling up the Prime Video monitoring service" (2023)
- DHH, "Even Amazon can't make sense of serverless or microservices" (2023)
- Spring Modulith Documentation - https://docs.spring.io/spring-modulith/reference/
- Shopify Engineering, "Deconstructing the Monolith" - https://shopify.engineering/deconstructing-monolith-designing-software-maximizes-developer-productivity
- Istio Documentation - https://istio.io/latest/docs/
- Linkerd Documentation - https://linkerd.io/2/overview/
- OpenTelemetry Documentation - https://opentelemetry.io/docs/
- gRPC Documentation - https://grpc.io/docs/
- Argo Rollouts - https://argoproj.github.io/rollouts/
- Confluent, "Event-Driven Microservices" - https://developer.confluent.io/patterns/
- Vaughn Vernon, "Implementing Domain-Driven Design" (Addison-Wesley, 2013)
- Eric Evans, "Domain-Driven Design" (Addison-Wesley, 2003)
- CNCF Landscape - Service Mesh - https://landscape.cncf.io/
Microservices Architecture 2025 Complete Guide: From Monolith to MSA, and Back to Modular Monolith
- Introduction
- 1. The History of Architectural Evolution
- 2. When MSA Is Overkill: Real-World Lessons
- 3. Architecture Comparison: Monolith vs MSA vs Modular Monolith
- 4. DDD and Service Decomposition
- 5. Communication Patterns: REST vs gRPC vs Event-Driven
- 6. Service Mesh: Istio vs Linkerd
- 7. Distributed Transactions and the Saga Pattern
- 8. Observability: OpenTelemetry
- 9. API Gateway Pattern
- 10. Deployment Strategies
- 11. Migration: The Strangler Fig Pattern
- 12. Anti-Patterns
- 13. Decision Framework
- 14. Interview Prep Q&A (Top 15)
- Q1. Explain five core characteristics of microservices.
- Q2. Explain the CAP theorem and its relationship with microservices.
- Q3. What are the two implementation approaches for the Saga pattern?
- Q4. Explain the Sidecar pattern in Service Mesh.
- Q5. Explain the principles of Distributed Tracing.
- Q6. What are the benefits and caveats of the Strangler Fig Pattern?
- Q7. What is the difference between API Gateway and Service Mesh?
- Q8. Explain the relationship between Event Sourcing and CQRS.
- Q9. How do you ensure data consistency in microservices?
- Q10. When is gRPC more advantageous than REST?
- Q11. Explain the Circuit Breaker pattern.
- Q12. Compare service discovery approaches.
- Q13. When should you transition from Modular Monolith to MSA?
- Q14. What is the Outbox Pattern?
- Q15. Explain the microservices Testing Pyramid.
- 15. Quiz
- References
Introduction
In 2024, Amazon Prime Video announced that they reverted from microservices to a monolith, cutting costs by 90% -- a revelation that sent shockwaves through the industry. DHH (creator of Ruby on Rails) declared that "microservices are over-engineering for most teams," and Shopify successfully handles massive traffic with a modular monolith architecture.
That said, microservices are far from dead. Netflix, Uber, and Spotify still operate thousands of microservices, and there are clear cases where MSA is the right choice depending on organizational scale and domain complexity.
This article provides an objective comparison of monolith, MSA, and modular monolith -- from the history of architectural evolution to current 2025 trends -- along with a systematic coverage of patterns and technologies needed in practice.
1. The History of Architectural Evolution
1.1 The Monolith Era (2000s)
The traditional architecture where all functionality is contained in a single deployment unit.
┌─────────────────────────────────────┐
│ Monolith Application │
│ ┌─────────┐ ┌─────────┐ ┌───────┐ │
│ │ User │ │ Order │ │Payment│ │
│ │ Module │ │ Module │ │Module │ │
│ └────┬─────┘ └────┬────┘ └──┬────┘ │
│ └──────┬─────┴─────────┘ │
│ ┌────▼────┐ │
│ │ Shared │ │
│ │ DB │ │
│ └─────────┘ │
└─────────────────────────────────────┘
Pros: Simple development/deployment, easy transaction management, convenient debugging
Cons: Codebase bloat, deployment bottlenecks, locked-in tech stack, scalability limits
1.2 The SOA Era (2005-2015)
Service-Oriented Architecture connected services around an Enterprise Service Bus (ESB).
┌────────┐ ┌────────┐ ┌────────┐
│Service A│ │Service B│ │Service C│
└───┬─────┘ └───┬─────┘ └───┬─────┘
└──────────────┼──────────────┘
┌────▼────┐
│ ESB │
│(Message │
│ Bus) │
└─────────┘
SOA pursued service reusability and standardization, but the ESB became a single point of failure, and the complexity of SOAP/WSDL was problematic.
1.3 The Microservices Era (2014-Present)
Microservices, defined by Martin Fowler and James Lewis, are an evolution of SOA where each service is independently deployable and scalable.
┌─────────┐ ┌─────────┐ ┌─────────┐
│ User │ │ Order │ │ Payment │
│ Service │ │ Service │ │ Service │
│ :8080 │ │ :8081 │ │ :8082 │
└──┬──────┘ └──┬──────┘ └──┬──────┘
│ REST/gRPC │ Event │
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐
│UserDB│ │OrderDB│ │PayDB │
└──────┘ └──────┘ └──────┘
1.4 2025: The Rise of the Modular Monolith
Timeline:
2000 ──── 2005 ──── 2014 ──── 2020 ──── 2025
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Monolith SOA Microservices Service Modular
Mesh Monolith
Spread Resurgence
2. When MSA Is Overkill: Real-World Lessons
2.1 The Amazon Prime Video Case
In 2023, the Amazon Prime Video team migrated their audio/video monitoring service from microservices to a monolith, reducing infrastructure costs by 90%.
Problems:
- Explosive growth of AWS Step Functions state transition costs
- S3 intermediate storage costs for inter-service data transfer
- Microservice orchestration overhead
Solution:
- Execute all processing steps within a single process
- In-memory communication instead of network calls
- Eliminate S3 intermediate storage
2.2 DHH's Critique
DHH, the creator of Ruby on Rails, argues that "most teams are perfectly fine with a monolith."
| Argument | Rationale |
|---|---|
| Distributed system complexity underestimated | Network failures, data consistency, debugging difficulty |
| Mismatch with team size | A team of 5-10 running 20 services is inefficient |
| Operational cost explosion | Infrastructure, monitoring, deployment pipeline costs |
| Premature decomposition risk | Poor boundary definition when domain understanding is lacking |
2.3 Shopify's Modular Monolith Success
Shopify processes tens of billions of dollars in transactions annually while using a modular monolith architecture.
# Module boundary definition using Shopify's Packwerk
# packages/checkout/package.yml
enforce_dependencies: true
enforce_privacy: true
dependencies:
- packages/inventory
- packages/payment
3. Architecture Comparison: Monolith vs MSA vs Modular Monolith
3.1 Comparison Matrix
| Aspect | Monolith | Modular Monolith | Microservices |
|---|---|---|---|
| Deployment Unit | 1 | 1 (module-level build) | N (per service) |
| Team Size | 1-20 | 5-50 | 20-hundreds |
| Communication | Method calls | Module interfaces | Network (REST/gRPC) |
| Data Store | Shared DB | Schema per module | DB per service |
| Transactions | ACID | ACID (limited across modules) | Saga/Compensation |
| Scalability | Vertical | Vertical + partial horizontal | Horizontal (per service) |
| Operational Complexity | Low | Medium | High |
| Tech Diversity | Single stack | Single stack | Polyglot |
| Fault Isolation | None | Partial | Full isolation |
| Initial Cost | Low | Medium | High |
3.2 Modular Monolith Architecture in Detail
┌─────────────────────────────────────────────┐
│ Modular Monolith │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ User │ │ Order │ │ Payment │ │
│ │ Module │ │ Module │ │ Module │ │
│ │ │ │ │ │ │ │
│ │ Public │ │ Public │ │ Public │ │
│ │ API │◄─► API │◄─► API │ │
│ │ │ │ │ │ │ │
│ │ Private │ │ Private │ │ Private │ │
│ │ Impl │ │ Impl │ │ Impl │ │
│ └──┬──────┘ └──┬──────┘ └──┬──────┘ │
│ │ │ │ │
│ ┌──▼──┐ ┌──▼──┐ ┌──▼──┐ │
│ │user │ │order│ │pay │ │
│ │schema│ │schema│ │schema│ │
│ └─────┘ └─────┘ └─────┘ │
│ └────────────┼────────────┘ │
│ ┌──────▼──────┐ │
│ │ Shared DB │ │
│ │(Separated │ │
│ │ Schemas) │ │
│ └─────────────┘ │
└─────────────────────────────────────────────┘
// Java modular monolith example (Spring Modulith)
@ApplicationModule(
allowedDependencies = {"order", "shared"}
)
package com.example.payment;
// Inter-module communication via events
@Service
public class PaymentService {
private final ApplicationEventPublisher events;
@Transactional
public PaymentResult processPayment(PaymentRequest request) {
Payment payment = Payment.create(request);
paymentRepository.save(payment);
// Publish event to other modules (no direct dependency)
events.publishEvent(new PaymentCompletedEvent(
payment.getId(),
payment.getOrderId(),
payment.getAmount()
));
return PaymentResult.success(payment);
}
}
3.3 Decision Flowchart
Start: New Project
│
▼
Team size > 50? ──Yes──▶ Are domains clearly separated?
│ │
No Yes ──▶ Consider MSA
│ │
▼ No ──▶ Modular Monolith
Need independently scalable services?
│
Yes ──▶ Extract only those services (Hybrid)
│
No ──▶ Monolith or Modular Monolith
4. DDD and Service Decomposition
4.1 Identifying Bounded Contexts
In Domain-Driven Design, the Bounded Context serves as the natural boundary for microservices.
┌─────────────── E-Commerce Domain ───────────────┐
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Catalog │ │ Order │ │
│ │ Context │ │ Context │ │
│ │ │ │ │ │
│ │ - Product │ │ - Order │ │
│ │ - Category │ │ - OrderItem │ │
│ │ - Price │ │ - Shipment │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Identity │ │ Payment │ │
│ │ Context │ │ Context │ │
│ │ │ │ │ │
│ │ - User │ │ - Payment │ │
│ │ - Role │ │ - Refund │ │
│ │ - Permission │ │ - Invoice │ │
│ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────┘
4.2 Context Mapping Patterns
┌────────────┐ ┌────────────┐
│ Upstream │ │ Downstream │
│ Context │ ──Conformist────▶ │ Context │
│ │ │ │
│ (Order) │ ──ACL──────────▶ │ (Payment) │
│ │ │ │
│ │ ──OHS/PL───────▶ │ (Shipping) │
└────────────┘ └────────────┘
ACL: Anti-Corruption Layer (translation layer)
OHS: Open Host Service (public API)
PL: Published Language (shared schema)
4.3 Service Decomposition Strategies
// 1. Decomposition by Aggregate
// Order Aggregate Root
public class Order {
private OrderId id;
private CustomerId customerId;
private List<OrderLine> lines;
private OrderStatus status;
private Money totalAmount;
// Business logic within the aggregate
public void addItem(ProductId productId, int quantity, Money price) {
OrderLine line = new OrderLine(productId, quantity, price);
this.lines.add(line);
this.totalAmount = calculateTotal();
}
public void confirm() {
if (this.lines.isEmpty()) {
throw new OrderException("Cannot confirm an empty order");
}
this.status = OrderStatus.CONFIRMED;
}
}
4.4 Decomposition Mistakes to Avoid
- Data-driven decomposition: Splitting services by table leads to excessive inter-service communication
- Technology-driven decomposition: Splitting by frontend/backend/DB layers loses MSA benefits
- Premature decomposition: Splitting before understanding the domain leads to incorrect boundaries
- Over-granularity: Nanoservices only increase operational burden
5. Communication Patterns: REST vs gRPC vs Event-Driven
5.1 Synchronous Communication Comparison
| Aspect | REST (HTTP/JSON) | gRPC (HTTP/2 + Protobuf) |
|---|---|---|
| Serialization | JSON (text) | Protocol Buffers (binary) |
| Performance | Relatively slow | 2-10x faster |
| Streaming | Limited (SSE/WebSocket) | Native bidirectional streaming |
| Code Generation | OpenAPI (optional) | Required (proto files) |
| Browser Support | Native | Requires gRPC-Web |
| Readability | High (JSON) | Low (binary) |
| Use Case | External APIs, simple CRUD | Internal inter-service communication |
5.2 gRPC Service Definition
// order_service.proto
syntax = "proto3";
package order.v1;
service OrderService {
// Unary RPC
rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
// Server streaming - order status updates
rpc WatchOrderStatus (WatchRequest) returns (stream OrderStatusUpdate);
// Client streaming - bulk order registration
rpc BulkCreateOrders (stream CreateOrderRequest) returns (BulkCreateResponse);
// Bidirectional streaming - real-time order processing
rpc ProcessOrders (stream OrderAction) returns (stream OrderResult);
}
message CreateOrderRequest {
string customer_id = 1;
repeated OrderItem items = 2;
ShippingAddress shipping = 3;
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
int64 price_cents = 3;
}
message CreateOrderResponse {
string order_id = 1;
OrderStatus status = 2;
int64 total_cents = 3;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_CONFIRMED = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_DELIVERED = 4;
}
5.3 Event-Driven Asynchronous Communication
Producer Consumer
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Order │───▶│ Kafka │───▶│ Payment │
│ Service │ │ Topic: │ │ Service │
│ │ │ orders │ │ │
└─────────┘ └─────────┘ └─────────┘
│
├────────▶ ┌─────────┐
│ │Inventory│
│ │ Service │
│ └─────────┘
│
└────────▶ ┌─────────┐
│Notific- │
│ation │
│ Service │
└─────────┘
// Kafka Producer - publish order events
@Service
public class OrderEventPublisher {
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
public void publishOrderCreated(Order order) {
OrderCreatedEvent event = OrderCreatedEvent.builder()
.orderId(order.getId())
.customerId(order.getCustomerId())
.items(order.getItems())
.totalAmount(order.getTotalAmount())
.timestamp(Instant.now())
.build();
kafkaTemplate.send("orders.created", order.getId(), event)
.whenComplete((result, ex) -> {
if (ex != null) {
log.error("Event publish failed: orderId={}",
order.getId(), ex);
// Compensation logic or retry
}
});
}
}
// Kafka Consumer - payment processing
@Service
public class PaymentEventConsumer {
@KafkaListener(
topics = "orders.created",
groupId = "payment-service",
containerFactory = "kafkaListenerContainerFactory"
)
public void handleOrderCreated(OrderCreatedEvent event) {
log.info("Order event received: orderId={}", event.getOrderId());
paymentService.processPayment(event);
}
}
5.4 Communication Pattern Selection Guide
Need synchronous call?
│
Yes ──▶ For external clients?
│ │
│ Yes ──▶ REST (OpenAPI)
│ │
│ No ──▶ Need high performance? ──Yes──▶ gRPC
│ │
│ No ──▶ REST
│
No ──▶ Need ordering guarantee?
│
Yes ──▶ Kafka (ordering via partition key)
│
No ──▶ Need fan-out?
│
Yes ──▶ SNS + SQS / Kafka topics
│
No ──▶ SQS / RabbitMQ
6. Service Mesh: Istio vs Linkerd
6.1 What is a Service Mesh?
A service mesh is a dedicated infrastructure layer for managing inter-service communication at the infrastructure level.
Without Service Mesh: With Service Mesh:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│Service A│──│Service B│ │Service A│ │Service B│
│ │ │ │ │ ┌─────┐ │ │ ┌─────┐ │
│(retry, │ │(retry, │ │ │Proxy│─┼──┼─│Proxy│ │
│ auth, │ │ auth, │ │ │(side│ │ │ │(side│ │
│ metrics)│ │ metrics)│ │ │car) │ │ │ │car) │ │
└─────────┘ └─────────┘ │ └─────┘ │ │ └─────┘ │
└─────────┘ └─────────┘
Communication logic Cross-cutting concerns
in every service delegated to infrastructure
6.2 Key Features
| Feature | Description |
|---|---|
| Traffic Management | Load balancing, routing, canary deployments |
| Security | Automatic mTLS, authentication/authorization |
| Observability | Distributed tracing, metrics collection, logging |
| Resilience | Retries, circuit breakers, timeouts |
6.3 Istio vs Linkerd Comparison
| Aspect | Istio | Linkerd |
|---|---|---|
| Proxy | Envoy (C++) | linkerd2-proxy (Rust) |
| Resource Usage | High (100-200MB/pod) | Low (20-30MB/pod) |
| Feature Scope | Comprehensive (VM support, etc.) | Core features focused |
| Learning Curve | Steep | Gentle |
| mTLS | Manual configuration needed | Enabled by default |
| CNCF Status | Graduated project | Graduated project |
| Recommended Scale | Large (100+ services) | Small-medium (10-100 services) |
6.4 Istio Traffic Management Example
# VirtualService - Canary Deployment
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
- match:
- headers:
x-canary:
exact: "true"
route:
- destination:
host: order-service
subset: v2
- route:
- destination:
host: order-service
subset: v1
weight: 90
- destination:
host: order-service
subset: v2
weight: 10
---
# DestinationRule - Circuit Breaker
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: order-service
spec:
host: order-service
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
http:
h2UpgradePolicy: DEFAULT
http1MaxPendingRequests: 100
http2MaxRequests: 1000
outlierDetection:
consecutive5xxErrors: 5
interval: 30s
baseEjectionTime: 30s
maxEjectionPercent: 50
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
6.5 When You Don't Need a Service Mesh
- Fewer than 10 services
- Kubernetes built-in features are sufficient
- Your team has no service mesh operational experience
- Systems extremely sensitive to latency (sidecar overhead)
7. Distributed Transactions and the Saga Pattern
7.1 The Problem with Distributed Transactions
In microservices, each service owns its own database, making ACID transactions across multiple services impossible.
Order creation scenario:
1. Order Service: Create order
2. Inventory Service: Reserve stock
3. Payment Service: Process payment
4. Notification Service: Send notification
What if payment fails at step 3?
-> Steps 1 and 2 need to be rolled back!
7.2 Saga Pattern - Choreography
┌─────────┐ OrderCreated ┌─────────┐ StockReserved ┌─────────┐
│ Order │──────────────▶│Inventory│───────────────▶│ Payment │
│ Service │ │ Service │ │ Service │
└─────────┘ └─────────┘ └─────────┘
▲ ▲ │
│ StockRelease │ PaymentFailed │
│ (compensate) │◀──────────────────────────┘
│◀────────────────────────┘
│ OrderCancelled (compensate)
Each service subscribes to events and autonomously triggers the next step.
7.3 Saga Pattern - Orchestration
┌────────────────┐
│ Order Saga │
│ Orchestrator │
└───┬──┬──┬──┬──┘
1.Create │ │ │ │ 4.Notify
Order ┌────┘ │ │ └──────┐
▼ │ │ ▼
┌─────────┐ │ │ ┌──────────┐
│ Order │ │ │ │Notificat-│
│ Service │ │ │ │ion Svc │
└─────────┘ │ │ └──────────┘
2.Reserve │ │ 3.Payment
Stock┌────┘ └─────┐
▼ ▼
┌─────────┐ ┌─────────┐
│Inventory│ │ Payment │
│ Service │ │ Service │
└─────────┘ └─────────┘
// Saga Orchestrator Implementation
@Service
public class OrderSagaOrchestrator {
public Mono<OrderResult> createOrder(CreateOrderCommand command) {
return Mono.just(new SagaState(command))
// Step 1: Create order
.flatMap(state -> orderService.createOrder(state.getCommand())
.map(order -> state.withOrder(order)))
// Step 2: Reserve stock
.flatMap(state -> inventoryService
.reserveStock(state.getOrder())
.map(reservation -> state.withReservation(reservation))
.onErrorResume(e -> compensateOrder(state, e)))
// Step 3: Process payment
.flatMap(state -> paymentService
.processPayment(state.getOrder())
.map(payment -> state.withPayment(payment))
.onErrorResume(e ->
compensateReservation(state, e)))
// Step 4: Complete
.flatMap(state -> {
orderService.confirmOrder(
state.getOrder().getId());
return Mono.just(OrderResult.success(state));
});
}
private Mono<SagaState> compensateReservation(
SagaState state, Throwable e) {
log.warn("Payment failed, starting stock compensation: orderId={}",
state.getOrder().getId());
return inventoryService
.releaseStock(state.getReservation())
.then(compensateOrder(state, e));
}
private Mono<SagaState> compensateOrder(
SagaState state, Throwable e) {
log.warn("Starting order compensation: orderId={}",
state.getOrder().getId());
return orderService
.cancelOrder(state.getOrder().getId())
.then(Mono.error(
new SagaFailedException("Saga failed", e)));
}
}
7.4 Choreography vs Orchestration
| Aspect | Choreography | Orchestration |
|---|---|---|
| Coupling | Loose | Dependent on central coordinator |
| Complexity | Grows with service count | Remains consistent |
| Debugging | Difficult (event tracing) | Relatively easy |
| Single Point of Failure | None | Orchestrator |
| Best For | Simple flows (3-4 steps or fewer) | Complex flows (5+ steps) |
8. Observability: OpenTelemetry
8.1 The Three Pillars of Observability
┌─────────────┐
│ Observability│
└──────┬──────┘
┌──────────┼──────────┐
▼ ▼ ▼
┌─────────┐ ┌──────┐ ┌─────────┐
│ Logs │ │Metrics│ │ Traces │
│(events) │ │(values)│ │(request │
└─────────┘ └──────┘ │ flow) │
What How much └─────────┘
happened? is happening? Where?
8.2 OpenTelemetry Integrated Architecture
┌─────────────────────────────────────────────────────┐
│ Application │
│ ┌─────────────────────────────────────────────┐ │
│ │ OpenTelemetry SDK │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Tracer │ │ Meter │ │ Logger │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └────────────────────┬────────────────────────┘ │
└───────────────────────┼─────────────────────────────┘
│ OTLP
▼
┌─────────────────┐
│ OTel Collector │
│ ┌───────────┐ │
│ │ Receivers │ │
│ │Processors│ │
│ │ Exporters │ │
│ └───────────┘ │
└────┬──┬──┬──────┘
│ │ │
┌────────┘ │ └────────┐
▼ ▼ ▼
┌─────────┐ ┌────────┐ ┌─────────┐
│ Jaeger │ │Promethe│ │ Loki │
│ (Traces)│ │us/Mimir│ │ (Logs) │
│ │ │(Metrics│ │ │
└─────────┘ └────────┘ └─────────┘
│ │ │
└─────┬─────┘───────────┘
▼
┌───────────┐
│ Grafana │
│(Dashboard)│
└───────────┘
8.3 Distributed Tracing Implementation
// Spring Boot + OpenTelemetry auto-configuration
// build.gradle
// implementation 'io.opentelemetry.instrumentation:
// opentelemetry-spring-boot-starter'
// Custom span creation
@Service
public class OrderService {
private final Tracer tracer;
public Order createOrder(CreateOrderRequest request) {
Span span = tracer.spanBuilder("order.create")
.setAttribute("order.customer_id",
request.getCustomerId())
.setAttribute("order.item_count",
request.getItems().size())
.startSpan();
try (Scope scope = span.makeCurrent()) {
// Order creation logic
Order order = processOrder(request);
span.setAttribute("order.id", order.getId());
span.setAttribute("order.total",
order.getTotal().doubleValue());
span.setStatus(StatusCode.OK);
return order;
} catch (Exception e) {
span.setStatus(StatusCode.ERROR, e.getMessage());
span.recordException(e);
throw e;
} finally {
span.end();
}
}
}
8.4 Key Metrics (RED/USE)
RED Method (Service perspective):
┌──────────────────────────────────────┐
│ R - Rate: Requests per second │
│ E - Errors: Error rate (%) │
│ D - Duration: Response time │
│ (p50/p95/p99) │
└──────────────────────────────────────┘
USE Method (Resource perspective):
┌──────────────────────────────────────┐
│ U - Utilization: Resource usage rate │
│ S - Saturation: Queue length │
│ E - Errors: Resource error count│
└──────────────────────────────────────┘
9. API Gateway Pattern
9.1 The Role of API Gateway
┌─────────────────────┐
│ API Gateway │
Client ────────▶│ │
│ - Auth/AuthZ │
│ - Rate Limiting │
│ - Request Routing │
│ - Load Balancing │
│ - Response Caching │
│ - Request/Response │
│ Transformation │
│ - Circuit Breaker │
│ - Logging/Monitoring│
└──┬──────┬──────┬────┘
│ │ │
▼ ▼ ▼
┌─────┐┌─────┐┌─────┐
│Svc A││Svc B││Svc C│
└─────┘└─────┘└─────┘
9.2 BFF (Backend for Frontend) Pattern
┌────────┐ ┌──────────────┐
│ Web │────▶│ Web BFF │──┐
│ Client │ │(GraphQL) │ │
└────────┘ └──────────────┘ │
│ ┌──────────┐
┌────────┐ ┌──────────────┐ ├───▶│ Order │
│ Mobile │────▶│ Mobile BFF │──┤ │ Service │
│ App │ │(REST, light) │ │ └──────────┘
└────────┘ └──────────────┘ │
│ ┌──────────┐
┌────────┐ ┌──────────────┐ ├───▶│ User │
│ IoT │────▶│ IoT BFF │──┤ │ Service │
│ Device │ │(MQTT bridge) │ │ └──────────┘
└────────┘ └──────────────┘ │
│ ┌──────────┐
└───▶│ Product │
│ Service │
└──────────┘
9.3 API Gateway Comparison
| Aspect | Kong | AWS API Gateway | Envoy Gateway | APISIX |
|---|---|---|---|---|
| Foundation | Nginx + Lua | AWS Managed | Envoy Proxy | Nginx + Lua |
| Deployment | Self-hosted/Cloud | AWS only | Kubernetes | Self-hosted |
| Protocols | REST, gRPC, WS | REST, WS, HTTP | REST, gRPC | REST, gRPC, MQTT |
| Plugins | Extensive | Lambda integration | Extensible | Extensive |
| Pricing | Open-source/Enterprise | Per-request billing | Open-source | Open-source |
10. Deployment Strategies
10.1 Blue-Green Deployment
Phase 1: Blue (current) running
┌─────────────┐ ┌──────────┐
│ Load │────▶│ Blue v1 │ (100% traffic)
│ Balancer │ │ (Active) │
└─────────────┘ └──────────┘
┌──────────┐
│ Green v2 │ (idle)
│ (Idle) │
└──────────┘
Phase 2: Switch to Green
┌─────────────┐ ┌──────────┐
│ Load │ │ Blue v1 │ (idle)
│ Balancer │ │ (Idle) │
└─────────────┘ └──────────┘
│ ┌──────────┐
└───────────▶│ Green v2 │ (100% traffic)
│ (Active) │
└──────────┘
10.2 Canary Deployment
# Kubernetes + Argo Rollouts canary deployment
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: order-service
spec:
replicas: 10
strategy:
canary:
steps:
- setWeight: 5
- pause:
duration: 5m
- analysis:
templates:
- templateName: success-rate
args:
- name: service-name
value: order-service
- setWeight: 25
- pause:
duration: 10m
- analysis:
templates:
- templateName: success-rate
- setWeight: 50
- pause:
duration: 15m
- setWeight: 100
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: order-service:v2
ports:
- containerPort: 8080
10.3 Deployment Strategy Comparison
| Strategy | Downtime | Rollback Speed | Resource Cost | Risk Level |
|---|---|---|---|---|
| Rolling Update | None | Moderate | Low | Medium |
| Blue-Green | None | Instant | High (2x) | Low |
| Canary | None | Fast | Medium | Low |
| A/B Testing | None | Fast | Medium | Low |
| Recreate | Yes | Slow | Low | High |
11. Migration: The Strangler Fig Pattern
11.1 Pattern Overview
The Strangler Fig Pattern is a strategy for progressively migrating a legacy monolith to microservices. Just as a strangler fig tree wraps around its host tree, new services gradually replace the legacy system.
Phase 1: Place proxy
┌────────┐ ┌──────────┐ ┌────────────┐
│ Client │────▶│ Proxy │────▶│ Monolith │
└────────┘ │(Facade) │ │(all funcs) │
└──────────┘ └────────────┘
Phase 2: Extract some features
┌────────┐ ┌──────────┐ ┌────────────┐
│ Client │────▶│ Proxy │─┬──▶│ Monolith │
└────────┘ │ │ │ │(reduced) │
└──────────┘ │ └────────────┘
│
└──▶┌────────────┐
│ New Service│
│(extracted) │
└────────────┘
Phase 3: Most features migrated
┌────────┐ ┌──────────┐ ┌────────────┐
│ Client │────▶│ Proxy │─┬──▶│ Monolith │
└────────┘ │ │ │ │(minimal) │
└──────────┘ │ └────────────┘
│
├──▶┌────────────┐
│ │ Service A │
│ └────────────┘
├──▶┌────────────┐
│ │ Service B │
│ └────────────┘
└──▶┌────────────┐
│ Service C │
└────────────┘
Phase 4: Remove monolith
┌────────┐ ┌──────────┐
│ Client │────▶│ API GW │─┬──▶ Service A
└────────┘ └──────────┘ ├──▶ Service B
└──▶ Service C
11.2 Migration Phases
1. Analysis (2-4 weeks)
├── Domain modeling (Event Storming)
├── Dependency analysis (code/data)
└── Priority determination
2. Foundation (4-8 weeks)
├── CI/CD pipeline
├── Container/orchestration environment
├── Observability stack
└── API Gateway placement
3. First service extraction (4-6 weeks)
├── Select most independent domain
├── Implement Anti-Corruption Layer
├── Data migration
└── Dual write / Event bridge
4. Iterative extraction (2-4 weeks per service)
├── Extract next service
├── Remove monolith code
└── Integration testing
5. Monolith removal
├── Migrate remaining features
├── Data cleanup
└── Infrastructure decommission
11.3 Data Migration Strategy
// Dual Write Pattern - write to both during transition
@Service
public class UserServiceMigration {
private final MonolithUserRepository monolithRepo;
private final NewUserServiceClient newServiceClient;
private final FeatureFlag featureFlag;
public User createUser(CreateUserRequest request) {
// Always write to monolith (existing)
User user = monolithRepo.save(toEntity(request));
// Also write to new service (migration)
try {
newServiceClient.createUser(
toNewServiceRequest(user));
} catch (Exception e) {
log.warn("New service sync failed, will retry later", e);
migrationQueue.enqueue(new SyncEvent(user));
}
return user;
}
public User getUser(String userId) {
// Switch reads using Feature Flag
if (featureFlag.isEnabled("read-from-new-service")) {
try {
return newServiceClient.getUser(userId);
} catch (Exception e) {
log.warn("New service read failed, falling back", e);
return monolithRepo.findById(userId);
}
}
return monolithRepo.findById(userId);
}
}
12. Anti-Patterns
12.1 Common MSA Anti-Patterns
| Anti-Pattern | Description | Solution |
|---|---|---|
| Distributed Monolith | Services are split but remain tightly coupled | Redefine Bounded Contexts |
| Nanoservice | Services too small (function-level) | Merge services, group related features |
| Shared DB | Multiple services accessing the same DB | DB per Service principle |
| Synchronous Chain | Sequential sync calls: A to B to C to D | Event-driven asynchronous |
| Golden Hammer | Applying MSA to every problem | Evaluate suitability before choosing architecture |
| Version Hell | API versions proliferate | Maintain backward compatibility, contract testing |
12.2 Distributed Monolith Symptoms
Distributed Monolith Checklist:
[ ] Deploying one service requires deploying others too
[ ] Synchronous call chains span 3+ services
[ ] Multiple services read/write the same database
[ ] Shared library versions must be coordinated across services
[ ] One service failure brings down the entire system
[ ] Service boundaries are drawn by tech layer (front/back/DB)
If 3 or more apply, you likely have a distributed monolith.
12.3 The Synchronous Call Chain Problem
Problem: Synchronous chain
Client -> Order -> Inventory -> Payment -> Shipping
(3 sec)
Total response time: 3 sec + each service processing time
If any service fails, the entire chain fails
Solution: Async events + immediate response
Client -> Order (200 OK, order accepted)
|
+-- Event: OrderCreated
| |
| +-- Inventory (async)
| +-- Payment (async)
| +-- Shipping (async)
|
+-- Status query API provided
13. Decision Framework
13.1 Architecture Selection Matrix
Score the following questions (1-5 points).
| Question | 1 point (Low) | 5 points (High) |
|---|---|---|
| Team size | 5 or fewer | 50+ |
| Domain complexity | Simple CRUD | Complex business logic |
| Independent scaling needs | Uniform load | Vastly different load per service |
| Deployment frequency | Monthly | Dozens per day |
| Tech diversity needs | Single stack | Optimal tech per service needed |
| Team autonomy | Centralized | Independent per team |
| Operational maturity | DevOps beginners | Mature platform team |
Score Interpretation:
- 7-15 points: Monolith (or Modular Monolith)
- 16-25 points: Modular Monolith (or Hybrid)
- 26-35 points: Microservices
13.2 Recommended Tech Stacks
Monolith:
├── Spring Boot + JPA
├── Django + PostgreSQL
├── Rails + PostgreSQL
└── Next.js Full Stack
Modular Monolith:
├── Spring Modulith
├── .NET Aspire
├── Go (module pattern)
└── Rust (Workspace)
Microservices:
├── Communication: gRPC (internal) + REST (external)
├── Messaging: Kafka / RabbitMQ
├── Orchestration: Kubernetes
├── Service Mesh: Istio / Linkerd
├── Observability: OpenTelemetry + Grafana
├── CI/CD: ArgoCD / GitHub Actions
└── API Gateway: Kong / Envoy Gateway
14. Interview Prep Q&A (Top 15)
Q1. Explain five core characteristics of microservices.
- Single Responsibility: Each service handles one business capability
- Independent Deployment: Deployable independently from other services
- Distributed Data: Each service owns its own data store
- Tech Diversity: Each service can choose its optimal tech stack
- Fault Isolation: One service failure does not propagate to the entire system
Q2. Explain the CAP theorem and its relationship with microservices.
According to the CAP theorem, a distributed system can guarantee only 2 out of 3: Consistency, Availability, and Partition Tolerance. Since network partitions are inevitable in microservices, P must always be chosen, leaving the choice between AP (availability-first) or CP (consistency-first). Most MSA systems choose AP with Eventual Consistency.
Q3. What are the two implementation approaches for the Saga pattern?
Choreography: Each service publishes and subscribes to events, autonomously performing the next step. There is no central coordinator, resulting in loose coupling, but event flow tracking becomes difficult as services increase.
Orchestration: A central orchestrator calls each service in sequence and manages compensation logic. The flow is clear, but the orchestrator can become a single point of failure.
Q4. Explain the Sidecar pattern in Service Mesh.
A proxy container (sidecar) is co-located with each service pod. All inbound/outbound traffic from the service passes through the sidecar, handling cross-cutting concerns like authentication (mTLS), routing, retries, and observability without modifying application code. Istio uses Envoy proxy, while Linkerd uses linkerd2-proxy as sidecars.
Q5. Explain the principles of Distributed Tracing.
When a request enters the system, a unique Trace ID is generated. Each service propagates this Trace ID and records its processing unit as a Span. Parent-child Spans form a complete Trace, enabling visualization of the entire request path and timing of each segment. The W3C Trace Context standard header is used.
Q6. What are the benefits and caveats of the Strangler Fig Pattern?
Benefits: Minimized risk through gradual migration, avoids big-bang migration, monolith and new services can coexist, easy rollback
Caveats: Dual-system operational costs during transition, data synchronization complexity, routing rule management burden, difficulty determining transition completion point
Q7. What is the difference between API Gateway and Service Mesh?
API Gateway: Manages external traffic (North-South). Handles edge features like authentication, rate limiting, request transformation, and API aggregation.
Service Mesh: Manages internal inter-service traffic (East-West). Handles mTLS, service discovery, load balancing, circuit breakers, and other inter-service communication concerns.
They are complementary, and large-scale MSA systems use both together.
Q8. Explain the relationship between Event Sourcing and CQRS.
Event Sourcing: Instead of storing state directly, it stores a sequence of state-change events. Current state is reconstructed by replaying events.
CQRS: Separates command (write) and query (read) models. Writes go to the event store; reads come from optimized views (Read Models). Event Sourcing can be used without CQRS, but combining them allows independent optimization of write and read performance.
Q9. How do you ensure data consistency in microservices?
Instead of strong consistency, we accept Eventual Consistency. Specific patterns include Saga Pattern (distributed transactions), Outbox Pattern (guaranteed event publishing), Change Data Capture (DB change detection), and Idempotency guarantees (safe duplicate processing). When strong data consistency is absolutely required, those capabilities should be placed in the same service.
Q10. When is gRPC more advantageous than REST?
gRPC excels in internal inter-service communication requiring high throughput, bidirectional streaming, strongly-typed contracts (proto files), and reduced payload size through binary serialization. REST is more suitable for browser clients, public APIs for third-party developers, and simple CRUD operations.
Q11. Explain the Circuit Breaker pattern.
Like an electrical circuit breaker, it blocks calls to a failing downstream service to prevent fault propagation. It has three states: Closed (normal calls), Open (calls blocked, immediate failure returned), Half-Open (testing recovery with some requests). When consecutive failures exceed a threshold, it transitions to Open, and after a timeout, tests recovery in Half-Open.
Q12. Compare service discovery approaches.
Client-Side Discovery: The client directly queries a service registry (Eureka, Consul) and selects target instances. Load balancing logic resides in the client.
Server-Side Discovery: A load balancer references the service registry for routing. Kubernetes Service is a prime example. Simpler as clients do not need to know about discovery logic.
Q13. When should you transition from Modular Monolith to MSA?
Consider transitioning when: a specific module's scaling requirements significantly differ from others, the team grows beyond 50 people requiring independent deployment, a specific module requires a different technology stack, or there is a business need for mandatory fault isolation.
Q14. What is the Outbox Pattern?
Within a local transaction, business data and events are saved together to an Outbox table. A separate process (Polling Publisher or CDC) reads events from the Outbox table and publishes them to a message broker. This ensures atomicity between business logic and event publishing.
Q15. Explain the microservices Testing Pyramid.
It consists of unit tests (internal service logic), integration tests (DB/external integration), contract tests (inter-service API compatibility using tools like Pact), component tests (single service E2E), and E2E tests (full system). Contract tests are especially important as they automatically verify that provider service changes do not break consumers.
15. Quiz
Q1. Explain 3 key advantages and 2 limitations of Modular Monolith compared to MSA.
Advantages: (1) Module-level separation without MSA complexity (network communication, distributed transactions, service mesh), (2) ACID transactions available for easy data consistency, (3) Single deployment unit for simpler operations/deployment.
Limitations: (1) Cannot independently scale specific services (difficult to horizontally scale individual modules), (2) Tech stack is limited to a single language/framework.
Q2. What happens when a compensating transaction fails in the Saga pattern?
Compensating transaction failure can leave the system in an inconsistent state, making it a critical issue. Solutions: (1) Retry policy (exponential backoff) for repeated compensation attempts, (2) Dead Letter Queue stores failed compensation events for manual/automatic recovery, (3) Design compensating transactions to be idempotent for safe retries, (4) Monitoring/alerting to immediately notify the operations team for manual intervention.
Q3. How can you minimize sidecar proxy overhead when adopting a Service Mesh?
(1) Choose a lightweight proxy (Linkerd's Rust-based proxy uses significantly less memory than Envoy), (2) Set appropriate resource requests/limits for sidecars (e.g., CPU 100m, Memory 50Mi), (3) Enable only the mesh features you need (disable unnecessary telemetry), (4) Consider eBPF-based service mesh (Cilium Service Mesh) which processes at kernel level without sidecars, (5) Exclude latency-critical services from the mesh.
Q4. What criteria should you use to select the first service to extract when applying the Strangler Fig Pattern?
(1) Features with the fewest dependencies on other modules (can operate independently), (2) High business value to motivate the team, (3) Well-defined domain boundaries exist, (4) Areas requiring independent scaling or different technology, (5) Sufficient test coverage for easy behavior verification. Conversely, high-risk areas like core payment or authentication should be extracted later.
Q5. Explain why Context Propagation in OpenTelemetry is important and how it works.
Context Propagation is the mechanism for passing Trace ID and Span ID between services to track a single request across a distributed environment. Its importance: without it, logs/metrics/traces from each service remain isolated, making it impossible to understand request flow. How it works: (1) Trace ID is generated at the first service, (2) Propagated to the next service via HTTP headers (W3C traceparent) or gRPC metadata, (3) In message queue environments, context is included in message headers, (4) Each service creates child Spans based on the received context.
References
- Martin Fowler, "Microservices" - https://martinfowler.com/articles/microservices.html
- Sam Newman, "Building Microservices, 2nd Edition" (O'Reilly, 2021)
- Chris Richardson, "Microservices Patterns" (Manning, 2018)
- Microservices.io - Patterns - https://microservices.io/patterns/index.html
- Amazon Prime Video Tech Blog, "Scaling up the Prime Video monitoring service" (2023)
- DHH, "Even Amazon can't make sense of serverless or microservices" (2023)
- Spring Modulith Documentation - https://docs.spring.io/spring-modulith/reference/
- Shopify Engineering, "Deconstructing the Monolith" - https://shopify.engineering/deconstructing-monolith-designing-software-maximizes-developer-productivity
- Istio Documentation - https://istio.io/latest/docs/
- Linkerd Documentation - https://linkerd.io/2/overview/
- OpenTelemetry Documentation - https://opentelemetry.io/docs/
- gRPC Documentation - https://grpc.io/docs/
- Argo Rollouts - https://argoproj.github.io/rollouts/
- Confluent, "Event-Driven Microservices" - https://developer.confluent.io/patterns/
- Vaughn Vernon, "Implementing Domain-Driven Design" (Addison-Wesley, 2013)
- Eric Evans, "Domain-Driven Design" (Addison-Wesley, 2003)
- CNCF Landscape - Service Mesh - https://landscape.cncf.io/