2강 40분

MSA 패턴과 Best Practices

마이크로서비스 아키텍처에서 자주 사용되는 핵심 패턴들과 실전 Best Practice를 배웁니다. 데이터베이스, 통신, 배포 패턴을 다룹니다.

MSA 패턴과 Best Practices

학습 목표

  • 마이크로서비스 핵심 패턴 이해
  • 서비스 분리 Best Practice
  • 데이터 관리 전략
  • 장애 대응 패턴

마이크로서비스 핵심 패턴

1. Database per Service 패턴

각 서비스가 독립적인 데이터베이스를 가집니다.

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   Order      │     │   Payment    │     │  Inventory   │
│   Service    │     │   Service    │     │   Service    │
├──────────────┤     ├──────────────┤     ├──────────────┤
│ PostgreSQL   │     │    MySQL     │     │   MongoDB    │
└──────────────┘     └──────────────┘     └──────────────┘

장점:

  • 서비스 간 독립성 보장
  • 기술 스택 선택 자유
  • 스키마 변경 자유로움

단점:

  • 트랜잭션 관리 복잡
  • 데이터 일관성 유지 어려움
  • 조인 쿼리 불가능

2. API Composition 패턴

여러 서비스의 데이터를 조합해서 반환

// API Gateway에서 여러 서비스 호출
async function getOrderDetail(orderId: string) {
  const [order, payment, delivery] = await Promise.all([
    orderService.getOrder(orderId),
    paymentService.getPayment(orderId),
    deliveryService.getDelivery(orderId),
  ]);

  return {
    ...order,
    payment: {
      status: payment.status,
      amount: payment.amount,
    },
    delivery: {
      status: delivery.status,
      trackingNumber: delivery.trackingNumber,
    },
  };
}

주의사항:

  • N+1 쿼리 문제 방지 (DataLoader 사용)
  • 병렬 처리로 성능 최적화
  • Timeout 설정 필수

3. CQRS (Command Query Responsibility Segregation)

읽기와 쓰기를 분리하는 패턴

Write Model (Command)     Read Model (Query)
┌─────────────┐          ┌─────────────┐
│  Order DB   │          │ Read Cache  │
│ (PostgreSQL)│─────────►│   (Redis)   │
└─────────────┘ Sync     └─────────────┘

                    Fast Queries

구현 예제:

// Command: 주문 생성 (Write)
class CreateOrderCommand {
  async execute(orderData: OrderData) {
    // 1. 비즈니스 로직 검증
    await this.validateOrder(orderData);

    // 2. Write DB에 저장
    const order = await Order.create(orderData);

    // 3. 이벤트 발행 (Read Model 동기화)
    await eventBus.publish('order.created', order);

    return order.id;
  }
}

// Query: 주문 조회 (Read)
class GetOrderQuery {
  async execute(orderId: string) {
    // Read Cache에서 조회 (빠름)
    const order = await redis.get(`order:${orderId}`);

    if (!order) {
      // Cache miss: DB에서 조회
      return await this.loadFromDb(orderId);
    }

    return JSON.parse(order);
  }
}

// Event Handler: Read Model 동기화
eventBus.subscribe('order.created', async (event) => {
  await redis.set(
    `order:${event.id}`,
    JSON.stringify(event),
    'EX',
    3600 // 1시간 TTL
  );
});

4. Event Sourcing 패턴

상태가 아닌 이벤트를 저장하는 패턴

// 이벤트 정의
interface OrderEvent {
  id: string;
  timestamp: Date;
  type: string;
  data: any;
}

// 이벤트 저장
class OrderEventStore {
  private events: OrderEvent[] = [];

  append(event: OrderEvent) {
    this.events.push(event);
  }

  getEvents(orderId: string): OrderEvent[] {
    return this.events.filter(e => e.id === orderId);
  }

  // 이벤트로부터 현재 상태 재구성
  reconstruct(orderId: string): Order {
    const events = this.getEvents(orderId);
    const order = new Order();

    for (const event of events) {
      order.apply(event);
    }

    return order;
  }
}

// Order Aggregate
class Order {
  private id: string;
  private items: OrderItem[] = [];
  private status: OrderStatus = 'PENDING';

  apply(event: OrderEvent) {
    switch (event.type) {
      case 'ORDER_CREATED':
        this.id = event.data.id;
        this.items = event.data.items;
        break;

      case 'ITEM_ADDED':
        this.items.push(event.data.item);
        break;

      case 'ORDER_CONFIRMED':
        this.status = 'CONFIRMED';
        break;

      case 'ORDER_CANCELLED':
        this.status = 'CANCELLED';
        break;
    }
  }
}

장점:

  • 완전한 감사 로그
  • 시간 여행 가능 (과거 상태 재현)
  • 디버깅 용이

단점:

  • 복잡도 증가
  • 저장 공간 많이 사용
  • 쿼리 성능 저하 가능

서비스 분리 Best Practice

1. 비즈니스 도메인 기준 분리

잘못된 예: 기술 계층 기반 분리

- UserController Service
- UserService Service
- UserRepository Service

올바른 예: 도메인 기반 분리

- Customer Service (고객 관리)
- Order Service (주문 관리)
- Catalog Service (상품 카탈로그)

2. 서비스 크기 가이드

“2 Pizza Rule” (아마존)

  • 2판의 피자로 팀원 전체가 식사 가능한 크기
  • 대략 6-10명 팀

코드 라인 기준

  • 소형: 1,000 ~ 5,000 LOC
  • 중형: 5,000 ~ 10,000 LOC
  • 대형: 10,000+ LOC (분리 검토 필요)

3. 서비스 경계 설정

// 좋은 예: 명확한 경계
interface OrderService {
  createOrder(data: CreateOrderDTO): Promise<Order>;
  getOrder(id: string): Promise<Order>;
  cancelOrder(id: string): Promise<void>;
}

// 나쁜 예: 모호한 경계
interface OrderService {
  createOrder(data: any): Promise<any>;
  updateProduct(id: string, data: any): Promise<any>; // ❌ Product는 다른 서비스
  sendEmail(to: string, body: string): Promise<void>; // ❌ Notification 서비스
}

데이터 관리 전략

1. 데이터 동기화 패턴

Change Data Capture (CDC)

┌─────────────┐
│  Order DB   │
└──────┬──────┘
       │ CDC (Debezium)

┌─────────────┐      ┌──────────────┐
│   Kafka     │─────►│ Analytics DB │
└─────────────┘      └──────────────┘

2. 참조 데이터 관리

서비스 간 참조는 ID만 사용:

// ❌ 나쁜 예: 전체 객체 저장
interface Order {
  id: string;
  customer: Customer; // 다른 서비스의 데이터
  items: Product[];   // 다른 서비스의 데이터
}

// ✅ 좋은 예: ID만 저장
interface Order {
  id: string;
  customerId: string;    // 참조만
  itemIds: string[];     // 참조만
}

// 필요 시 조회
async function getOrderWithDetails(orderId: string) {
  const order = await orderRepo.findById(orderId);
  const customer = await customerService.getCustomer(order.customerId);
  const items = await productService.getProducts(order.itemIds);

  return { order, customer, items };
}

장애 대응 패턴

1. Bulkhead 패턴

서비스 격리로 장애 전파 방지

// 서비스별 스레드 풀 분리
const orderServicePool = new ThreadPool({ size: 10 });
const paymentServicePool = new ThreadPool({ size: 5 });

// Order Service 장애가 Payment Service에 영향 안 줌
app.post('/orders', (req, res) => {
  orderServicePool.execute(() => createOrder(req.body));
});

app.post('/payments', (req, res) => {
  paymentServicePool.execute(() => processPayment(req.body));
});

2. Timeout 설정

// 모든 외부 호출에 Timeout 설정
const order = await fetch('http://order-service/orders', {
  signal: AbortSignal.timeout(3000), // 3초 timeout
});

3. Fallback 전략

async function getProductRecommendations(userId: string) {
  try {
    // AI 추천 서비스 호출
    return await aiService.getRecommendations(userId);
  } catch (error) {
    console.error('AI service failed, using fallback');

    // Fallback: 인기 상품 반환
    return await getPopularProducts();
  }
}

모니터링과 관찰성

1. 분산 추적 (Distributed Tracing)

import { trace } from '@opentelemetry/api';

async function createOrder(orderData) {
  const span = trace.getTracer('order-service').startSpan('create_order');

  try {
    span.setAttribute('order.id', orderData.id);
    span.setAttribute('order.amount', orderData.totalAmount);

    const order = await Order.create(orderData);

    span.setStatus({ code: SpanStatusCode.OK });
    return order;
  } catch (error) {
    span.setStatus({
      code: SpanStatusCode.ERROR,
      message: error.message,
    });
    throw error;
  } finally {
    span.end();
  }
}

2. 헬스 체크

// Kubernetes Liveness Probe
app.get('/health/live', (req, res) => {
  res.json({ status: 'UP' });
});

// Kubernetes Readiness Probe
app.get('/health/ready', async (req, res) => {
  const dbHealthy = await checkDatabase();
  const cacheHealthy = await checkRedis();

  if (dbHealthy && cacheHealthy) {
    res.json({ status: 'UP' });
  } else {
    res.status(503).json({ status: 'DOWN' });
  }
});

실전 체크리스트

설계 단계

  • 도메인 분석 완료 (DDD)
  • 서비스 경계 명확히 정의
  • API 계약 문서화 (OpenAPI)
  • 데이터 모델 설계

개발 단계

  • 표준화된 에러 처리
  • 로깅 및 메트릭 수집
  • Circuit Breaker 구현
  • Rate Limiting 설정

배포 단계

  • 컨테이너화 (Docker)
  • CI/CD 파이프라인
  • 무중단 배포 전략
  • 롤백 계획

운영 단계

  • 모니터링 대시보드
  • 알림 설정
  • 로그 중앙화
  • 정기 백업

핵심 정리

  • Database per Service로 서비스 독립성 확보
  • CQRS로 읽기/쓰기 성능 최적화
  • Saga/Event Sourcing으로 데이터 일관성 관리
  • Circuit Breaker, Bulkhead로 장애 격리
  • 관찰성(Observability) 확보가 운영의 핵심