13.microservices
Episode 13: "마이크로서비스: 모놀리스의 해체"
거대한 시스템을 작은 조각으로 나누다
프롤로그: 두 개의 극단
케이스 1: 모놀리스의 악몽
2015년, 한 스타트업의 이야기입니다.
# monolith_app.rb
# 처음엔 간단했습니다
class Application
def handle_request(path)
case path
when '/users' then UserController.new
when '/products' then ProductController.new
when '/orders' then OrderController.new
end
end
end
# 3년 후...
# 500개의 컨트롤러
# 100만 줄의 코드
# 200명의 개발자가 같은 저장소에서 작업
# 문제들:
# - 빌드 시간: 30분
# - 테스트 시간: 2시간
# - 배포: 주말에만 (위험해서)
# - 한 줄 고치려면 전체 재배포
# - "결제팀이 배포했더니 검색이 죽었어요"
# - 신입이 실수로 DB 연결 코드 건드려서 서비스 전체 다운
거대한 모놀리스의 문제들
개발 속도 저하:
- 코드베이스 이해 어려움
- 빌드/테스트 시간 증가
- 배포 두려움 (전체에 영향)
스케일링 비효율:
- 일부만 부하 높아도 전체 확장
- 자원 낭비
기술 부채:
- 레거시 코드 제거 어려움
- 새 기술 도입 어려움
- 리팩토링 위험
팀 협업 충돌:
- 같은 코드 동시 수정
- 병합 충돌
- 배포 대기열
케이스 2: 과도한 MSA의 함정
같은 시기, 다른 스타트업의 이야기입니다.
# "모놀리스는 나쁘다더라. MSA로 가자!"
# 서비스 수: 50개
# 개발자 수: 5명
# 사용자 수: 100명 (...)
services/
├── user-service/
├── user-authentication-service/
├── user-profile-service/
├── user-preference-service/
├── product-service/
├── product-search-service/
├── product-recommendation-service/
├── order-service/
├── order-payment-service/
├── order-shipping-service/
├── notification-email-service/
├── notification-sms-service/
├── notification-push-service/
... (37개 더)
# 문제들:
# - 서비스 하나 고치려면 5개 서비스 수정 필요
# - 로그가 50개 서비스에 흩어짐
# - 로컬 개발 환경 구축에 3일 소요
# - "API 호출 체인이 10단계인데 어디서 느린지 모르겠어요"
# - 배포 파이프라인 50개 관리
# - 5명이 50개 서비스 온콜 (주말 없음)
과도한 MSA의 문제들
운영 복잡도 폭증:
- 관리할 서비스 너무 많음
- 모니터링/로깅 복잡
- 네트워크 장애 추적 어려움
개발 생산성 저하:
- 로컬 개발 환경 복잡
- 여러 서비스 동시 수정
- API 버전 호환성 관리
과도한 인프라 비용:
- 서비스당 최소 리소스
- 작은 서비스도 전체 인프라 필요
- 낭비 심각
조직 미스매치:
- 5명이 50개 서비스 관리 불가
- 전문성 분산
- 온콜 지옥
두 케이스 모두 잘못된 선택입니다.
마이크로서비스는 은탄환이 아닙니다. 때로는 모놀리스가 답이고, 때로는 마이크로서비스가 답입니다. 중요한 건 언제, 왜, 어떻게 사용하는지 아는 것입니다.
이 글에서 다룰 내용
- 넷플릭스의 실험: 모놀리스에서 마이크로서비스로
- 분산 시스템의 복잡성: CAP 정리의 현실
- API Gateway, Service Mesh: 인프라의 진화
- "분산 모놀리스"라는 새로운 안티패턴
Chapter 1: 넷플릭스의 실험 - MSA의 탄생
2008년, 재앙의 시작
2008년 8월, 넷플릭스의 데이터센터에서 대규모 장애가 발생했습니다. 데이터베이스 손상으로 3일간 DVD 배송이 중단되었죠.
# Netflix Architecture (2008)
┌─────────────────────────────────────┐
│ Monolithic Application │
│ │
│ - DVD Queue Management │
│ - User Account │
│ - Billing │
│ - Shipping │
│ - Recommendations │
│ - Everything... │
│ │
└─────────────┬───────────────────────┘
│
┌─────────▼─────────┐
│ Single Database │
│ (Oracle) │
└────────────────────┘
# 데이터베이스 하나가 죽으면?
# → 전체 서비스 다운
# → DVD 배송도 멈춤
# → 고객 이탈
넷플릭스의 경영진은 결정을 내립니다: 클라우드로 이동하자.
"우리는 더 이상 데이터센터를 운영하지 않기로 했습니다. 대신 애플리케이션을 작은 서비스들로 쪼개서, 각각 독립적으로 실패하고 복구할 수 있게 만들기로 했습니다."
2009-2015, 7년간의 대이동
넷플릭스는 7년에 걸쳐 모놀리스를 마이크로서비스로 전환했습니다:
2009: 클라우드 이동 시작
가장 중요하지 않은 서비스부터 AWS로
2010: API 분리
모놀리스에서 API 레이어 분리
2012: 핵심 서비스 분할
사용자 계정, 추천, 검색 등을 독립 서비스로
2015: 이동 완료
모든 서비스가 AWS에서 마이크로서비스로 운영
2016: 700개 이상의 마이크로서비스
각 서비스는 독립적으로 배포 가능
# Netflix Architecture (2016)
User Request
↓
API Gateway (Zuul)
↓
┌───────────────────────────────────────────────────┐
│ Microservices (700+) │
├───────────────┬───────────────┬───────────────────┤
│ User Service │ Auth Service │ Billing Service │
│ Profile Svc │ Recommendation│ Playback Service │
│ Search Svc │ Encoding Svc │ Content Metadata │
│ Device Svc │ A/B Test Svc │ Analytics Service │
│ ... │ ... │ ... │
└───────────────┴───────────────┴───────────────────┘
↓ ↓ ↓
Multiple Databases (각 서비스마다)
넷플릭스의 MSA 전환 효과
실제 결과
배포 속도:
- Before: 주 1회 배포 (위험해서)
- After: 하루 수천 번 배포 (자동화)
가용성:
- Before: 한 곳 장애 = 전체 다운
- After: 일부 서비스 장애 = 부분 기능 저하
개발 속도:
- Before: 큰 팀이 하나의 코드베이스
- After: 작은 팀이 독립적인 서비스
스케일링:
- Before: 전체 애플리케이션 확장
- After: 필요한 서비스만 확장 (추천 엔진만 10배 확장 등)
혁신:
- 각 팀이 독립적으로 기술 선택
- Python, Node.js, Go, Java 혼용
- 실험과 실패의 비용 감소
MSA의 핵심 원칙 (넷플릭스가 정립)
// 마이크로서비스의 핵심 특징
// 1. 단일 책임
// 각 서비스는 하나의 비즈니스 기능만
// user-service (O)
interface UserService {
createUser(data: UserData): User;
getUser(id: string): User;
updateUser(id: string, data: Partial<UserData>): User;
}
// god-service (X)
interface GodService {
createUser(...): User;
processPayment(...): Payment;
sendEmail(...): void;
generateReport(...): Report;
// 너무 많은 책임!
}
// 2. 독립적 배포
// 다른 서비스에 영향 없이 배포 가능
// 3. 데이터베이스 분리
// 각 서비스가 자신의 DB 소유
// User Service DB
users_db/
├── users
├── profiles
└── preferences
// Order Service DB
orders_db/
├── orders
├── order_items
└── shipments
// 절대 직접 다른 서비스의 DB에 접근하지 않음!
// API로만 통신
// 4. 독립적 확장
// 트래픽 높은 서비스만 확장
kubectl scale deployment user-service --replicas=10
kubectl scale deployment billing-service --replicas=2
// 5. 장애 격리
// 한 서비스 장애가 전체에 영향 최소화
// recommendation-service가 다운되어도
// → 사용자는 여전히 로그인 가능
// → 비디오 재생 가능
// → 단지 추천 기능만 안 됨
다른 회사들도 뒤따르다
넷플릭스의 성공 이후, 많은 회사들이 MSA를 도입했습니다:
Amazon (2000년대 초부터)
실은 넷플릭스보다 먼저. CEO 명령으로 SOA 도입
Uber (2012~)
700개 이상의 마이크로서비스
Airbnb (2015~)
모놀리스에서 점진적 전환
Spotify (2014~)
Squad 모델과 결합한 MSA
하지만 모두가 성공한 것은 아니었습니다.
Chapter 2: 분산 시스템의 복잡성 - MSA의 함정들
함정 1: 분산 모놀리스 (Distributed Monolith)
가장 흔한 실패 패턴입니다.
// 분산 모놀리스의 전형적인 예
// 모놀리스를 그냥 쪼개기만 함
// user-service
class UserService {
async createUser(data: UserData) {
// 1. user-profile-service 호출 (동기)
const profile = await http.post('http://profile-service/profiles', data);
// 2. user-preference-service 호출 (동기)
const preferences = await http.post('http://preference-service/prefs', data);
// 3. notification-service 호출 (동기)
await http.post('http://notification-service/welcome-email', { userId: profile.id });
// 4. analytics-service 호출 (동기)
await http.post('http://analytics-service/track', { event: 'user_created' });
return profile;
}
}
// 문제:
// - user-service는 4개 서비스에 의존
// - 하나라도 느리면 전체 느림
// - 하나라도 죽으면 회원가입 실패
// - 모놀리스보다 더 나쁨!
분산 모놀리스의 특징
강한 결합:
- 서비스 간 동기 호출 체인
- 한 서비스가 여러 서비스에 의존
- 독립적 배포 불가능
공유 데이터베이스:
- 여러 서비스가 같은 DB 테이블 접근
- 스키마 변경이 여러 서비스에 영향
버전 종속성:
- A 서비스 버전 2는 B 서비스 버전 3에 의존
- 동시 배포 필요
조율된 배포:
- 여러 서비스를 순서대로 배포해야 함
- 하나만 배포하면 시스템 고장
// 올바른 MSA: 느슨한 결합
class UserService {
async createUser(data: UserData) {
// 1. 사용자만 생성 (동기)
const user = await db.users.create(data);
// 2. 이벤트 발행 (비동기)
await eventBus.publish('user.created', { userId: user.id, ...data });
return user;
// 빠르게 반환!
}
}
// profile-service (독립적으로 실행)
eventBus.subscribe('user.created', async (event) => {
await createProfile(event.userId, event.data);
});
// notification-service (독립적으로 실행)
eventBus.subscribe('user.created', async (event) => {
await sendWelcomeEmail(event.userId);
});
// 장점:
// - user-service는 빠르게 응답
// - 다른 서비스가 죽어도 회원가입은 성공
// - 각 서비스 독립적 배포
// - 나중에 새 구독자 추가 쉬움
함정 2: 잘못된 경계 설정
// ❌ 나쁜 예: 기술 계층으로 분리
services/
├── frontend-service/ // UI만
├── business-logic-service/ // 모든 비즈니스 로직
└── database-service/ // DB 접근만
// 문제:
// - 작은 기능 하나 추가해도 3개 서비스 모두 수정
// - 팀 간 조율 필수
// - 배포 조율 필수
// - 모놀리스와 다를 바 없음
// ✅ 좋은 예: 비즈니스 도메인으로 분리
services/
├── user-service/ // 사용자 관리 전체
├── order-service/ // 주문 처리 전체
├── product-service/ // 상품 카탈로그 전체
└── payment-service/ // 결제 처리 전체
// 각 서비스는 자신의 UI + 로직 + DB 포함
// 독립적으로 개발, 배포, 확장 가능
서비스 경계 설정의 원칙
Domain-Driven Design
비즈니스 기능 기준:
- "사용자 관리", "주문 처리", "결제"
- 기술이 아닌 도메인으로
팀 구조 일치:
- Conway's Law: 시스템 구조는 조직 구조를 따름
- 한 팀이 한 서비스 담당
데이터 응집도:
- 함께 변경되는 데이터는 같은 서비스에
- 자주 조인되는 테이블은 같은 서비스에
트랜잭션 경계:
- 강한 일관성 필요한 부분은 같은 서비스에
- 분산 트랜잭션 최소화
함정 3: 네트워크는 믿을 수 없다
// 모놀리스에서는 간단했던 것
class MonolithService {
async createOrder(userId: string, productId: string) {
const user = await this.userRepo.findById(userId);
const product = await this.productRepo.findById(productId);
if (user.balance < product.price) {
throw new Error('Insufficient balance');
}
const order = await this.orderRepo.create({ userId, productId });
await this.userRepo.update(userId, { balance: user.balance - product.price });
return order;
}
// 간단, 빠름, ACID 트랜잭션 보장
}
// MSA에서는...
class OrderService {
async createOrder(userId: string, productId: string) {
// 1. User Service 호출 (네트워크!)
const user = await http.get(`http://user-service/users/${userId}`);
// 만약 네트워크 지연 500ms?
// 만약 타임아웃?
// 만약 서비스 다운?
// 2. Product Service 호출 (네트워크!)
const product = await http.get(`http://product-service/products/${productId}`);
// 또 네트워크 문제 가능
if (user.balance < product.price) {
return { error: 'Insufficient balance' };
}
// 3. Order 생성
const order = await db.orders.create({ userId, productId });
// 4. User Service에 잔액 차감 요청 (네트워크!)
await http.post(`http://user-service/users/${userId}/deduct`, {
amount: product.price
});
// 만약 여기서 실패하면?
// Order는 이미 생성됨!
// 롤백 어떻게?
return order;
}
// 복잡, 느림, 일관성 보장 어려움, 에러 케이스 폭증
}
분산 시스템의 8가지 함정 (Fallacies of Distributed Computing)
- 네트워크는 신뢰할 수 있다 (X)
- 지연시간은 0이다 (X)
- 대역폭은 무한하다 (X)
- 네트워크는 안전하다 (X)
- 토폴로지는 변하지 않는다 (X)
- 관리자는 한 명이다 (X)
- 전송 비용은 0이다 (X)
- 네트워크는 동질적이다 (X)
모두 거짓입니다. MSA 설계 시 항상 고려해야 합니다.
함정 4: 관찰 가능성의 악몽
# 모놀리스에서 디버깅
User: "주문이 실패했어요"
$ grep "order_id=12345" /var/log/app.log
[ERROR] Order creation failed: Insufficient balance
at OrderService.create (order_service.rb:45)
at OrderController.create (order_controller.rb:23)
# 5분 만에 원인 파악!
# MSA에서 디버깅
User: "주문이 실패했어요"
# 어느 서비스에서 실패했을까?
$ kubectl logs -f order-service-7d4f9c8b-x4k2m
[INFO] Received order request: order_id=12345
$ kubectl logs -f user-service-6c8a7b9d-m3p1k
[INFO] User balance check: user_id=789, balance=100
$ kubectl logs -f product-service-5b7e6a8c-n2q4j
[INFO] Product price: product_id=456, price=150
$ kubectl logs -f payment-service-4a6d5b7c-k1r3m
[ERROR] Payment processing timeout
$ kubectl logs -f notification-service-3e5c4d6b-j0s2n
[WARN] Order notification failed: order not found
# 어? 결제 타임아웃인가? 주문 생성 안 된 건가?
# 로그가 5개 서비스에 흩어짐
# 시간순 정렬도 안 됨
# Correlation ID도 없음
# 3시간 후에야 원인 파악...
MSA에 필요한 관찰 가능성 도구들
복잡도 증가
분산 추적 (Distributed Tracing):
- Jaeger, Zipkin, OpenTelemetry
- 요청이 여러 서비스를 거치는 경로 추적
- Trace ID로 전체 흐름 파악
중앙 로깅:
- ELK Stack, Loki, CloudWatch
- 모든 서비스 로그를 한 곳에
- Correlation ID로 연관 로그 검색
메트릭 모니터링:
- Prometheus, Grafana, DataDog
- 각 서비스의 CPU, 메모리, 요청 수 등
- 이상 탐지 및 알람
서비스 메시:
- Istio, Linkerd, Consul
- 서비스 간 통신 가시성
- 트래픽 제어, 보안
APM (Application Performance Monitoring):
- New Relic, Dynatrace, AppDynamics
- 전체 애플리케이션 성능 통합 뷰
모놀리스에서는 필요 없던 수많은 도구와 인프라가 필요합니다.
Chapter 3: 언제 MSA가 필요한가?
MSA를 도입하면 안 되는 경우
// 스타트업 시작 단계
// ❌ 나쁜 선택
// "우리도 넷플릭스처럼 MSA로!"
// 상황:
// - 팀: 3명
// - 사용자: 0명 (아직 출시 전)
// - 도메인: 아직 불명확
// - 기술: MSA 경험 없음
// 결과:
// - 개발 속도 느림
// - 운영 부담 과중
// - 실패 확률 높음
// ✅ 올바른 선택
// "일단 모놀리스로 빠르게 만들자"
class App {
// 모든 기능이 하나의 앱에
// - 빠른 개발
// - 간단한 배포
// - 쉬운 디버깅
// - 낮은 운영 비용
}
// MVP 출시 → 고객 피드백 → 빠른 반복
// 도메인이 명확해진 후 필요시 분리
MSA를 피해야 하는 신호들
팀 크기:
- 개발자 10명 미만
- MSA 운영 경험 없음
- DevOps 전담 인력 없음
비즈니스 단계:
- Product-Market Fit 이전
- 도메인 경계 불명확
- 빠른 변화와 실험 필요
기술 성숙도:
- 분산 시스템 경험 없음
- CI/CD 파이프라인 없음
- 모니터링 인프라 없음
트래픽/규모:
- 사용자 수천 명 미만
- 트래픽 예측 가능
- 단일 서버로 충분
MSA를 고려해야 하는 경우
// 성숙한 서비스
// ✅ MSA 고려 신호들
// 1. 팀 규모
// - 개발자 30명 이상
// - 여러 팀으로 나뉨
// - Conway's Law를 활용
// 2. 배포 충돌
// - 팀 A 배포가 팀 B에 영향
// - 배포 대기열 길어짐
// - 한 팀 실수가 전체 영향
// 3. 스케일링 불균형
// - 추천 엔진만 CPU 집약적
// - 검색만 트래픽 10배
// - 전체 스케일링은 낭비
// 4. 기술 다양성 필요
// - ML 모델은 Python
// - 실시간 채팅은 Go
// - 레거시는 Java
// - 모놀리스에선 불가능
// 5. 독립적 배포 필요
// - 하루 수십 번 배포
// - A/B 테스트 많음
// - 빠른 롤백 필요
MSA 도입 체크리스트
준비가 되었나요?
조직:
- 개발팀 20명 이상
- DevOps/SRE 전담 팀 있음
- 각 팀이 독립적으로 일할 수 있음
기술:
- 컨테이너/오케스트레이션 경험 (Docker, Kubernetes)
- CI/CD 자동화되어 있음
- 모니터링/로깅 인프라 있음
- API 설계 경험 풍부
비즈니스:
- 도메인 경계 명확
- 독립적인 비즈니스 기능들
- 장기적 로드맵 있음
문화:
- 실패를 학습 기회로 봄
- 자동화 문화
- DevOps 문화 성숙
5개 이하 체크: 모놀리스 유지 권장 6-10개 체크: 점진적 전환 고려 11개 이상 체크: MSA 도입 가능
점진적 전환: Strangler Fig Pattern
// 모놀리스 → MSA 전환은 점진적으로!
// Phase 1: 모놀리스 (현재)
┌────────────────────────────┐
│ Monolith App │
│ - Users │
│ - Products │
│ - Orders │
│ - Payments │
│ - Everything │
└────────────────────────────┘
// Phase 2: 첫 번째 서비스 분리 (가장 독립적인 것부터)
┌────────────────────────────┐ ┌──────────────┐
│ Monolith App │◄────►│ Payment │
│ - Users │ │ Service │
│ - Products │ └──────────────┘
│ - Orders │
│ - (Payments → delegated) │
└────────────────────────────┘
// Phase 3: 두 번째 서비스 분리
┌────────────────────────────┐ ┌──────────────┐
│ Monolith App │◄────►│ Payment │
│ - Users │ │ Service │
│ - Products │ └──────────────┘
│ - (Orders → delegated) │ ▲
└────────────────────────────┘ │
▲ │
└──────┬────────────────────┘
│
┌──────▼────────┐
│ Order Service│
└───────────────┘
// Phase N: 완전 전환
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ User │ │ Product │ │ Order │ │ Payment │
│ Service │ │ Service │ │ Service │ │ Service │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
// 장점:
// - 위험 최소화
// - 학습하며 진행
// - 언제든 되돌릴 수 있음
// - 비즈니스 중단 없음
"마이크로서비스 아키텍처의 첫 번째 규칙은: 마이크로서비스로 시작하지 마라. 모놀리스로 시작하고, 문제가 생겼을 때만 마이크로서비스로 전환하라."
Chapter 4: MSA 인프라의 진화
API Gateway: 단일 진입점
MSA에는 수많은 서비스가 있습니다. 클라이언트가 각 서비스를 직접 호출하면 혼란스러워집니다.
// API Gateway 없이 (클라이언트가 복잡해짐)
// 모바일 앱에서
const user = await fetch('http://user-service:3001/users/123');
const orders = await fetch('http://order-service:3002/orders?userId=123');
const products = await fetch('http://product-service:3003/products');
const recommendations = await fetch('http://recommendation-service:3004/recommend?userId=123');
// 문제:
// - 클라이언트가 모든 서비스 주소 알아야 함
// - 인증을 각 요청마다 처리
// - 네트워크 요청 많음
// - 서비스 변경 시 모든 클라이언트 수정
// API Gateway 사용
// 모바일 앱에서
const data = await fetch('https://api.myapp.com/v1/dashboard', {
headers: { 'Authorization': 'Bearer token' }
});
// 한 번의 요청으로 필요한 모든 데이터 받음
// API Gateway에서
app.get('/v1/dashboard', async (req, res) => {
// 1. 인증 (한 곳에서)
const user = await authenticate(req.headers.authorization);
// 2. 병렬로 여러 서비스 호출
const [orders, products, recommendations] = await Promise.all([
fetch(`http://order-service/orders?userId=${user.id}`),
fetch(`http://product-service/products`),
fetch(`http://recommendation-service/recommend?userId=${user.id}`)
]);
// 3. 데이터 조합 후 반환
res.json({
user,
orders: orders.data,
products: products.data,
recommendations: recommendations.data
});
});
API Gateway의 역할
MSA의 관문
라우팅:
- 클라이언트 요청을 적절한 서비스로 전달
- URL 기반, 헤더 기반 라우팅
인증/인가:
- 한 곳에서 통합 인증
- JWT 검증, OAuth 처리
요청 조합 (BFF Pattern):
- 여러 서비스 호출 결과 조합
- 모바일/웹별 최적화된 응답
Rate Limiting:
- API 사용량 제한
- DDoS 방어
캐싱:
- 자주 요청되는 데이터 캐시
- 백엔드 부하 감소
모니터링:
- 모든 요청 로깅
- 메트릭 수집
인기 솔루션:
- Kong, Nginx, Traefik (오픈소스)
- AWS API Gateway, Azure API Management (클라우드)
Service Mesh: 서비스 간 통신 관리
서비스가 수백 개가 되면, 서비스 간 통신 관리가 복잡해집니다.
# Service Mesh 없이: 각 서비스에서 처리
# user-service 코드
app.post('/api/users', async (req, res) => {
// Retry 로직
let retries = 3;
while (retries > 0) {
try {
await callNotificationService();
break;
} catch (error) {
retries--;
await sleep(1000);
}
}
// Circuit Breaker 로직
if (errorRate > 0.5) {
// 빠른 실패
}
// Timeout 설정
// Logging
// Metrics
// ...
// 비즈니스 로직보다 인프라 코드가 더 많음!
});
# order-service 코드에도 똑같은 코드 복붙...
# payment-service 코드에도 똑같은 코드 복붙...
# 모든 서비스에 중복!
# Service Mesh 사용: 인프라가 처리
# user-service 코드 (깔끔!)
app.post('/api/users', async (req, res) => {
// 비즈니스 로직만!
const user = await db.users.create(req.body);
await callNotificationService(user);
res.json(user);
});
# Istio 설정 (YAML)
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: notification-service
spec:
http:
- timeout: 5s
retries:
attempts: 3
perTryTimeout: 2s
fault:
delay:
percentage:
value: 0.1
fixedDelay: 5s
# 모든 통신 설정이 인프라 레벨에서!
Istio
가장 인기있는 Service Mesh. Kubernetes 기반
Linkerd
경량 Service Mesh. Rust로 작성되어 빠름
Consul
HashiCorp의 Service Mesh. Multi-cloud 지원
Service Mesh의 기능
트래픽 관리:
- 로드 밸런싱
- Retry, Timeout, Circuit Breaker
- Canary 배포, Blue-Green 배포
보안:
- mTLS (상호 TLS 인증)
- 서비스 간 암호화 통신
- 인가 정책
관찰 가능성:
- 자동 메트릭 수집
- 분산 추적
- 서비스 토폴로지 시각화
트래픽 제어:
- A/B 테스트
- 점진적 롤아웃
- 트래픽 미러링
Saga Pattern: 분산 트랜잭션
MSA에서 가장 어려운 문제: 여러 서비스에 걸친 트랜잭션
// 모놀리스에서는 간단했던 것
async function createOrder(userId, productId) {
const transaction = await db.transaction();
try {
// 1. 재고 차감
await transaction('inventory')
.where('product_id', productId)
.decrement('quantity', 1);
// 2. 결제
await transaction('payments').insert({
user_id: userId,
amount: product.price
});
// 3. 주문 생성
await transaction('orders').insert({
user_id: userId,
product_id: productId
});
await transaction.commit();
// 모두 성공 or 모두 롤백
} catch (error) {
await transaction.rollback();
}
}
// MSA에서는: 분산 트랜잭션 불가능!
// Saga Pattern: Choreography (이벤트 기반)
// Order Service
async function createOrder(userId, productId) {
const order = await db.orders.create({
user_id: userId,
product_id: productId,
status: 'PENDING'
});
// 이벤트 발행
await eventBus.publish('OrderCreated', {
orderId: order.id,
userId,
productId
});
return order;
}
// Inventory Service (구독자)
eventBus.subscribe('OrderCreated', async (event) => {
try {
await decrementInventory(event.productId);
await eventBus.publish('InventoryReserved', event);
} catch (error) {
// 보상 트랜잭션
await eventBus.publish('InventoryReservationFailed', event);
}
});
// Payment Service (구독자)
eventBus.subscribe('InventoryReserved', async (event) => {
try {
await processPayment(event.userId, event.amount);
await eventBus.publish('PaymentCompleted', event);
} catch (error) {
// 보상 트랜잭션
await eventBus.publish('PaymentFailed', event);
}
});
// Order Service (구독자 - 완료 처리)
eventBus.subscribe('PaymentCompleted', async (event) => {
await db.orders.update(event.orderId, { status: 'COMPLETED' });
});
// Order Service (구독자 - 실패 처리)
eventBus.subscribe('PaymentFailed', async (event) => {
// 보상 트랜잭션: 재고 복구
await eventBus.publish('CancelInventoryReservation', event);
await db.orders.update(event.orderId, { status: 'FAILED' });
});
// Inventory Service (구독자 - 보상)
eventBus.subscribe('CancelInventoryReservation', async (event) => {
await incrementInventory(event.productId);
});
// 복잡하지만 이것이 MSA의 현실!
분산 트랜잭션의 어려움
일관성 vs 가용성:
- CAP 정리: 일관성, 가용성, 분할 내성 중 2개만 선택 가능
- MSA는 보통 가용성과 분할 내성 선택 → 최종 일관성
복잡성:
- 보상 트랜잭션 구현 복잡
- 에러 케이스 많음
- 테스트 어려움
디버깅:
- 여러 서비스에 걸친 흐름
- 시간 순서 추적 어려움
대안:
- 트랜잭션 필요한 부분은 같은 서비스에
- 최종 일관성으로 타협
- 2PC (Two-Phase Commit) 피하기
에필로그: MSA는 은탄환이 아니다
진짜 MSA를 하고 있나요?
많은 회사들이 "MSA를 한다"고 하지만, 실제로는...
// "MSA"라고 부르지만 실제로는...
// 1. 분산 모놀리스
// - 서비스 간 강한 결합
// - 동기 호출 체인
// - 공유 데이터베이스
// = MSA가 아님
// 2. 마이크로 모놀리스
// - 서비스는 작은데
// - 모두 함께 배포
// - 버전 의존성 강함
// = MSA가 아님
// 3. 나노서비스
// - 서비스가 너무 작음
// - 함수 하나가 서비스 하나
// - 운영 비용 폭증
// = 과도한 MSA
// 진짜 MSA:
// - 독립적 배포
// - 느슨한 결합
// - 자율적 팀
// - 비즈니스 기능 단위
// - 장애 격리
MSA 성숙도 자가 진단
우리는 어느 단계인가?
Level 0: 모놀리스
- 하나의 애플리케이션
- 괜찮음! 많은 경우 충분함
Level 1: 모듈형 모놀리스
- 코드는 모듈로 분리
- 하나의 배포 단위
- MSA로 가기 전 좋은 단계
Level 2: 서비스 지향
- 일부 서비스 분리
- 여전히 공유 DB
- 전환기
Level 3: 마이크로서비스
- 서비스마다 독립 DB
- 독립적 배포
- 팀 자율성
- 진짜 MSA
Level 4: 엔터프라이즈 MSA
- Service Mesh, API Gateway
- 고급 관찰 가능성
- 자동화된 배포
- 넷플릭스, 우버 수준
진짜 MSA가 필요한가요?
// 질문에 솔직하게 답해보세요
const questions = [
"개발자가 50명 이상인가?",
"하루에 10번 이상 배포하나?",
"서로 다른 기술 스택이 필요한가?",
"스케일링이 불균등한가?",
"팀이 독립적으로 일할 수 있나?",
"분산 시스템 경험이 풍부한가?",
"DevOps 문화가 성숙했나?",
"운영 복잡도를 감당할 수 있나?"
];
let yesCount = 0;
// 5개 이상 Yes? → MSA 고려
// 3개 이하 Yes? → 모놀리스가 나음
// 중요한 것:
// - 비즈니스 가치
// - 팀 역량
// - 유지보수 비용
"마이크로서비스는 복잡성의 대가를 치르고 자율성을 얻는 것입니다. 자율성이 필요 없다면, 그 대가를 치를 이유가 없습니다."
모놀리스 퍼스트 (Monolith First)
// 권장 진행 순서
// Phase 1: 모놀리스로 시작
// - 빠른 개발
// - 단순한 아키텍처
// - 도메인 이해하며 학습
// Phase 2: 모듈형 모놀리스
// - 코드를 명확한 모듈로 분리
// - 경계 정의
// - 준비 단계
// Phase 3: 필요할 때 분리
// - 진짜 문제가 생겼을 때
// - 가장 독립적인 부분부터
// - 점진적으로
// Phase 4: 계속 진화
// - 필요한 만큼만
// - 과도하게 분리하지 말 것
// - 언제든 다시 합칠 수 있음
성공적인 MSA 도입 사례의 공통점
점진적 전환:
- 한 번에 바꾸지 않음
- 학습하며 진행
- 실패해도 복구 가능
명확한 이유:
- "유행이라서"가 아님
- 실제 문제 해결 위해
- ROI 명확
조직 준비:
- 팀 구조 정비
- DevOps 문화
- 자동화 인프라
현실적 기대:
- 마법의 해결책 아님
- 복잡도 증가 인정
- 트레이드오프 이해
MSA의 본질
마이크로서비스는 기술이 아니라 조직 전략입니다.
# MSA는 이런 것들을 가능하게 합니다:
✅ 팀 자율성
→ 각 팀이 독립적으로 결정
✅ 빠른 배포
→ 다른 팀 기다릴 필요 없음
✅ 기술 다양성
→ 문제에 맞는 도구 선택
✅ 점진적 확장
→ 필요한 부분만 확장
# 하지만 이런 대가가 있습니다:
❌ 운영 복잡도 증가
❌ 분산 시스템의 어려움
❌ 높은 초기 투자
❌ 전문성 요구
# 트레이드오프를 이해하고 선택해야 합니다
"추상화는 단지 복잡성을 숨기는 것이 아니라, 다른 곳으로 옮기는 것입니다. 마이크로서비스도 마찬가지입니다. 애플리케이션 복잡도를 인프라 복잡도로 옮기는 것입니다."
2024년, MSA의 현주소
주류 기술로 자리잡음
Netflix, Uber, Amazon 등 대규모 서비스의 표준
하지만 과도한 도입 반성
많은 회사들이 "모놀리스로 회귀" 사례도 증가
중도 해법 등장
모듈형 모놀리스, 서비스 지향 등
성숙한 도구 생태계
Kubernetes, Istio, Kafka 등 인프라 성숙
MSA는 필요할 때, 제대로 이해하고 도입해야 하는 아키텍처입니다.
"우리도 MSA 해야 해"가 아니라, "우리에게 MSA가 필요한가?"를 먼저 물어야 합니다.
그리고 대부분의 경우, 답은 "아직은 아니다"일 수 있습니다. 그리고 그것은 완전히 괜찮습니다.
참고자료
Building Microservices
Sam Newman의 마이크로서비스 설계 가이드
Microservices Patterns
Chris Richardson의 MSA 패턴 카탈로그
Martin Fowler - Microservices
마이크로서비스 아키텍처 정의와 특징
Martin Fowler - MonolithFirst
모놀리스 우선 접근법
Netflix Tech Blog
넷플릭스의 MSA 전환 경험
Istio Documentation
Service Mesh 공식 문서
CAP Theorem
분산 시스템의 CAP 정리
The Eight Fallacies of Distributed Computing
분산 시스템의 8가지 오해
다음 에피소드에서는 "GitHub Copilot: AI 페어 프로그래머의 등장"을 다룰 예정입니다. AI와 개발자의 협업이 코딩의 미래를 어떻게 바꾸고 있는지 살펴보겠습니다.