13.microservices

Episode 13: "마이크로서비스: 모놀리스의 해체"

거대한 시스템을 작은 조각으로 나누다


프롤로그: 두 개의 극단

케이스 1: 모놀리스의 악몽

2015년, 한 스타트업의 이야기입니다.

ruby
# 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의 함정

같은 시기, 다른 스타트업의 이야기입니다.

bash
# "모놀리스는 나쁘다더라. 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 배송이 중단되었죠.

bash
# Netflix Architecture (2008)

┌─────────────────────────────────────┐
│     Monolithic Application          │
│                                      │
│  - DVD Queue Management              │
│  - User Account                      │
│  - Billing                           │
│  - Shipping                          │
│  - Recommendations                   │
│  - Everything...                     │
│                                      │
└─────────────┬───────────────────────┘
              │
    ┌─────────▼─────────┐
    │  Single Database   │
    │  (Oracle)          │
    └────────────────────┘

# 데이터베이스 하나가 죽으면?
# → 전체 서비스 다운
# → DVD 배송도 멈춤
# → 고객 이탈

넷플릭스의 경영진은 결정을 내립니다: 클라우드로 이동하자.

"우리는 더 이상 데이터센터를 운영하지 않기로 했습니다. 대신 애플리케이션을 작은 서비스들로 쪼개서, 각각 독립적으로 실패하고 복구할 수 있게 만들기로 했습니다."

Adrian CockcroftNetflix Cloud Architect (2010)

2009-2015, 7년간의 대이동

넷플릭스는 7년에 걸쳐 모놀리스를 마이크로서비스로 전환했습니다:

1

2009: 클라우드 이동 시작

가장 중요하지 않은 서비스부터 AWS로

2

2010: API 분리

모놀리스에서 API 레이어 분리

3

2012: 핵심 서비스 분할

사용자 계정, 추천, 검색 등을 독립 서비스로

4

2015: 이동 완료

모든 서비스가 AWS에서 마이크로서비스로 운영

5

2016: 700개 이상의 마이크로서비스

각 서비스는 독립적으로 배포 가능

bash
# 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의 핵심 원칙 (넷플릭스가 정립)

typescript
// 마이크로서비스의 핵심 특징

// 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를 도입했습니다:

1

Amazon (2000년대 초부터)

실은 넷플릭스보다 먼저. CEO 명령으로 SOA 도입

2

Uber (2012~)

700개 이상의 마이크로서비스

3

Airbnb (2015~)

모놀리스에서 점진적 전환

4

Spotify (2014~)

Squad 모델과 결합한 MSA

하지만 모두가 성공한 것은 아니었습니다.


Chapter 2: 분산 시스템의 복잡성 - MSA의 함정들

함정 1: 분산 모놀리스 (Distributed Monolith)

가장 흔한 실패 패턴입니다.

typescript
// 분산 모놀리스의 전형적인 예

// 모놀리스를 그냥 쪼개기만 함

// 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에 의존
  • 동시 배포 필요

조율된 배포:

  • 여러 서비스를 순서대로 배포해야 함
  • 하나만 배포하면 시스템 고장
typescript
// 올바른 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: 잘못된 경계 설정

typescript
// ❌ 나쁜 예: 기술 계층으로 분리

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: 네트워크는 믿을 수 없다

typescript
// 모놀리스에서는 간단했던 것

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)
  1. 네트워크는 신뢰할 수 있다 (X)
  2. 지연시간은 0이다 (X)
  3. 대역폭은 무한하다 (X)
  4. 네트워크는 안전하다 (X)
  5. 토폴로지는 변하지 않는다 (X)
  6. 관리자는 한 명이다 (X)
  7. 전송 비용은 0이다 (X)
  8. 네트워크는 동질적이다 (X)

모두 거짓입니다. MSA 설계 시 항상 고려해야 합니다.

함정 4: 관찰 가능성의 악몽

bash
# 모놀리스에서 디버깅

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를 도입하면 안 되는 경우

typescript
// 스타트업 시작 단계

// ❌ 나쁜 선택
// "우리도 넷플릭스처럼 MSA로!"

// 상황:
// - 팀: 3명
// - 사용자: 0명 (아직 출시 전)
// - 도메인: 아직 불명확
// - 기술: MSA 경험 없음

// 결과:
// - 개발 속도 느림
// - 운영 부담 과중
// - 실패 확률 높음

// ✅ 올바른 선택
// "일단 모놀리스로 빠르게 만들자"

class App {
  // 모든 기능이 하나의 앱에
  // - 빠른 개발
  // - 간단한 배포
  // - 쉬운 디버깅
  // - 낮은 운영 비용
}

// MVP 출시 → 고객 피드백 → 빠른 반복
// 도메인이 명확해진 후 필요시 분리
MSA를 피해야 하는 신호들

팀 크기:

  • 개발자 10명 미만
  • MSA 운영 경험 없음
  • DevOps 전담 인력 없음

비즈니스 단계:

  • Product-Market Fit 이전
  • 도메인 경계 불명확
  • 빠른 변화와 실험 필요

기술 성숙도:

  • 분산 시스템 경험 없음
  • CI/CD 파이프라인 없음
  • 모니터링 인프라 없음

트래픽/규모:

  • 사용자 수천 명 미만
  • 트래픽 예측 가능
  • 단일 서버로 충분

MSA를 고려해야 하는 경우

typescript
// 성숙한 서비스

// ✅ 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

typescript
// 모놀리스 → 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 │
└─────────┘  └─────────┘  └─────────┘  └─────────┘

// 장점:
// - 위험 최소화
// - 학습하며 진행
// - 언제든 되돌릴 수 있음
// - 비즈니스 중단 없음

"마이크로서비스 아키텍처의 첫 번째 규칙은: 마이크로서비스로 시작하지 마라. 모놀리스로 시작하고, 문제가 생겼을 때만 마이크로서비스로 전환하라."

Martin Fowler

Chapter 4: MSA 인프라의 진화

API Gateway: 단일 진입점

MSA에는 수많은 서비스가 있습니다. 클라이언트가 각 서비스를 직접 호출하면 혼란스러워집니다.

typescript
// 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: 서비스 간 통신 관리

서비스가 수백 개가 되면, 서비스 간 통신 관리가 복잡해집니다.

yaml
# 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
        
# 모든 통신 설정이 인프라 레벨에서!
1

Istio

가장 인기있는 Service Mesh. Kubernetes 기반

2

Linkerd

경량 Service Mesh. Rust로 작성되어 빠름

3

Consul

HashiCorp의 Service Mesh. Multi-cloud 지원

Service Mesh의 기능

트래픽 관리:

  • 로드 밸런싱
  • Retry, Timeout, Circuit Breaker
  • Canary 배포, Blue-Green 배포

보안:

  • mTLS (상호 TLS 인증)
  • 서비스 간 암호화 통신
  • 인가 정책

관찰 가능성:

  • 자동 메트릭 수집
  • 분산 추적
  • 서비스 토폴로지 시각화

트래픽 제어:

  • A/B 테스트
  • 점진적 롤아웃
  • 트래픽 미러링

Saga Pattern: 분산 트랜잭션

MSA에서 가장 어려운 문제: 여러 서비스에 걸친 트랜잭션

typescript
// 모놀리스에서는 간단했던 것

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를 한다"고 하지만, 실제로는...

typescript
// "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가 필요한가요?

typescript
// 질문에 솔직하게 답해보세요

const questions = [
  "개발자가 50명 이상인가?",
  "하루에 10번 이상 배포하나?",
  "서로 다른 기술 스택이 필요한가?",
  "스케일링이 불균등한가?",
  "팀이 독립적으로 일할 수 있나?",
  "분산 시스템 경험이 풍부한가?",
  "DevOps 문화가 성숙했나?",
  "운영 복잡도를 감당할 수 있나?"
];

let yesCount = 0;
// 5개 이상 Yes? → MSA 고려
// 3개 이하 Yes? → 모놀리스가 나음

// 중요한 것:
// - 비즈니스 가치
// - 팀 역량
// - 유지보수 비용

"마이크로서비스는 복잡성의 대가를 치르고 자율성을 얻는 것입니다. 자율성이 필요 없다면, 그 대가를 치를 이유가 없습니다."

Sam NewmanBuilding Microservices 저자

모놀리스 퍼스트 (Monolith First)

typescript
// 권장 진행 순서

// Phase 1: 모놀리스로 시작
// - 빠른 개발
// - 단순한 아키텍처
// - 도메인 이해하며 학습

// Phase 2: 모듈형 모놀리스
// - 코드를 명확한 모듈로 분리
// - 경계 정의
// - 준비 단계

// Phase 3: 필요할 때 분리
// - 진짜 문제가 생겼을 때
// - 가장 독립적인 부분부터
// - 점진적으로

// Phase 4: 계속 진화
// - 필요한 만큼만
// - 과도하게 분리하지 말 것
// - 언제든 다시 합칠 수 있음
성공적인 MSA 도입 사례의 공통점

점진적 전환:

  • 한 번에 바꾸지 않음
  • 학습하며 진행
  • 실패해도 복구 가능

명확한 이유:

  • "유행이라서"가 아님
  • 실제 문제 해결 위해
  • ROI 명확

조직 준비:

  • 팀 구조 정비
  • DevOps 문화
  • 자동화 인프라

현실적 기대:

  • 마법의 해결책 아님
  • 복잡도 증가 인정
  • 트레이드오프 이해

MSA의 본질

마이크로서비스는 기술이 아니라 조직 전략입니다.

bash
# MSA는 이런 것들을 가능하게 합니다:

✅ 팀 자율성
   → 각 팀이 독립적으로 결정
   
✅ 빠른 배포
   → 다른 팀 기다릴 필요 없음
   
✅ 기술 다양성
   → 문제에 맞는 도구 선택
   
✅ 점진적 확장
   → 필요한 부분만 확장

# 하지만 이런 대가가 있습니다:

❌ 운영 복잡도 증가
❌ 분산 시스템의 어려움
❌ 높은 초기 투자
❌ 전문성 요구

# 트레이드오프를 이해하고 선택해야 합니다

"추상화는 단지 복잡성을 숨기는 것이 아니라, 다른 곳으로 옮기는 것입니다. 마이크로서비스도 마찬가지입니다. 애플리케이션 복잡도를 인프라 복잡도로 옮기는 것입니다."

Dan AbramovReact Core Team

2024년, MSA의 현주소

1

주류 기술로 자리잡음

Netflix, Uber, Amazon 등 대규모 서비스의 표준

2

하지만 과도한 도입 반성

많은 회사들이 "모놀리스로 회귀" 사례도 증가

3

중도 해법 등장

모듈형 모놀리스, 서비스 지향 등

4

성숙한 도구 생태계

Kubernetes, Istio, Kafka 등 인프라 성숙

MSA는 필요할 때, 제대로 이해하고 도입해야 하는 아키텍처입니다.

"우리도 MSA 해야 해"가 아니라, "우리에게 MSA가 필요한가?"를 먼저 물어야 합니다.

그리고 대부분의 경우, 답은 "아직은 아니다"일 수 있습니다. 그리고 그것은 완전히 괜찮습니다.



다음 에피소드에서는 "GitHub Copilot: AI 페어 프로그래머의 등장"을 다룰 예정입니다. AI와 개발자의 협업이 코딩의 미래를 어떻게 바꾸고 있는지 살펴보겠습니다.