9강 50분

데이터베이스 패턴 - Database per Service

마이크로서비스 환경에서 데이터베이스를 설계하는 핵심 패턴들을 배웁니다. Database per Service, Shared Database, CQRS 패턴을 비교합니다.

데이터베이스 패턴 - Database per Service

학습 목표

  • Database per Service 패턴의 장단점
  • 데이터 일관성 문제와 해결책
  • Polyglot Persistence
  • 데이터 동기화 전략

Database per Service 패턴

핵심 원칙

각 서비스가 자신만의 데이터베이스를 소유

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   Order      │     │   Product    │     │   User       │
│   Service    │     │   Service    │     │   Service    │
├──────────────┤     ├──────────────┤     ├──────────────┤
│ PostgreSQL   │     │   MongoDB    │     │    MySQL     │
└──────────────┘     └──────────────┘     └──────────────┘

❌ Order Service가 직접 Product DB 접근 불가
✅ Product Service API 호출 필요

장점

독립적 배포: DB 스키마 변경이 다른 서비스에 영향 없음 ✅ 기술 선택 자유: 서비스별 최적 DB 선택 가능 ✅ 확장성: 서비스별로 독립적으로 스케일 ✅ 장애 격리: 한 DB 장애가 다른 서비스에 전파 안 됨

단점

데이터 일관성: 트랜잭션 관리 복잡 ❌ 조인 불가: 여러 서비스 데이터 조회 어려움 ❌ 데이터 중복: 참조 데이터 복제 필요 ❌ 운영 복잡도: 여러 DB 관리

Anti-Pattern: Shared Database

문제점

┌──────────────┐     ┌──────────────┐
│   Order      │     │   Product    │
│   Service    │     │   Service    │
└──────┬───────┘     └──────┬───────┘
       │                    │
       └────────┬───────────┘

         ┌──────▼──────┐
         │   Shared    │
         │   Database  │
         └─────────────┘

강한 결합: 스키마 변경 시 모든 서비스 영향 ❌ 배포 어려움: 동시 배포 필요 ❌ 확장 제한: DB 병목 발생 ❌ 팀 자율성 저해: DB 변경 시 협의 필요

예외: 읽기 전용 DB

Read Replica는 공유 가능:

┌──────────────┐     ┌──────────────┐
│  Analytics   │     │  Reporting   │
│   Service    │     │   Service    │
└──────┬───────┘     └──────┬───────┘
       │                    │
       └────────┬───────────┘

         ┌──────▼──────┐
         │ Read Replica│
         │  (분석용)    │
         └─────────────┘

Polyglot Persistence

서비스별 최적 DB 선택

Order Service → PostgreSQL

-- ACID 트랜잭션 필요
CREATE TABLE orders (
  id UUID PRIMARY KEY,
  customer_id UUID NOT NULL,
  total_amount DECIMAL(10,2),
  status VARCHAR(20),
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE order_items (
  id UUID PRIMARY KEY,
  order_id UUID REFERENCES orders(id),
  product_id UUID,
  quantity INT,
  unit_price DECIMAL(10,2)
);

Product Catalog → MongoDB

// 유연한 스키마, 중첩 데이터
{
  "_id": "prod-123",
  "name": "Laptop",
  "description": "...",
  "specs": {
    "cpu": "Intel i7",
    "ram": "16GB",
    "storage": "512GB SSD"
  },
  "images": [
    "https://...",
    "https://..."
  ],
  "tags": ["electronics", "computer", "work"]
}

Session Store → Redis

# 빠른 읽기/쓰기, TTL 지원
SET session:abc123 "{\"userId\": \"user-456\", \"role\": \"admin\"}" EX 3600
GET session:abc123

Analytics → ClickHouse

-- 대용량 로그 분석
CREATE TABLE page_views (
  timestamp DateTime,
  user_id String,
  page_url String,
  referrer String,
  country String
) ENGINE = MergeTree()
ORDER BY timestamp;

데이터 동기화 패턴

1. API Composition

여러 서비스 API 호출 후 조합:

async function getOrderDetails(orderId: string) {
  // 1. Order 조회
  const order = await orderService.getOrder(orderId);

  // 2. 병렬로 상세 정보 조회
  const [customer, products] = await Promise.all([
    customerService.getCustomer(order.customerId),
    Promise.all(
      order.items.map(item =>
        productService.getProduct(item.productId)
      )
    ),
  ]);

  // 3. 조합
  return {
    order,
    customer: {
      name: customer.name,
      email: customer.email,
    },
    items: order.items.map((item, index) => ({
      ...item,
      productName: products[index].name,
      productImage: products[index].images[0],
    })),
  };
}

장점:

  • 구현 간단
  • 데이터 항상 최신

단점:

  • 네트워크 오버헤드
  • 성능 저하
  • 서비스 장애 전파

2. CQRS (Command Query Responsibility Segregation)

읽기와 쓰기 분리:

Write Side (Command)          Read Side (Query)
┌──────────────┐             ┌──────────────┐
│ Order Service│             │  Query DB    │
│  (PostgreSQL)│─Event──────►│  (Denorm)    │
└──────────────┘             └──────────────┘

                              ┌──────▼──────┐
                              │ Order Details│
                              │ View (빠름)  │
                              └─────────────┘

구현 예제:

// Command: 주문 생성
class CreateOrderCommand {
  async execute(orderData: OrderData) {
    // Write DB에 저장
    const order = await Order.create(orderData);

    // Event 발행
    await eventBus.publish('order.created', {
      orderId: order.id,
      customerId: order.customerId,
      items: order.items,
    });

    return order.id;
  }
}

// Event Handler: Read Model 업데이트
eventBus.subscribe('order.created', async (event) => {
  // 고객 정보 조회
  const customer = await customerService.getCustomer(event.customerId);

  // 상품 정보 조회
  const products = await productService.getProducts(
    event.items.map(i => i.productId)
  );

  // Denormalized View 생성
  await queryDb.orderDetails.insert({
    orderId: event.orderId,
    customerName: customer.name,
    customerEmail: customer.email,
    items: event.items.map((item, i) => ({
      productId: item.productId,
      productName: products[i].name,
      productImage: products[i].images[0],
      quantity: item.quantity,
      unitPrice: item.unitPrice,
    })),
    totalAmount: event.items.reduce(
      (sum, item) => sum + item.quantity * item.unitPrice,
      0
    ),
    createdAt: new Date(),
  });
});

// Query: 주문 상세 조회 (빠름)
class GetOrderDetailsQuery {
  async execute(orderId: string) {
    // Read DB에서 바로 조회 (JOIN 없음)
    return await queryDb.orderDetails.findById(orderId);
  }
}

3. Change Data Capture (CDC)

DB 변경 사항을 자동으로 캡처:

┌──────────────┐
│ Order DB     │
│ (PostgreSQL) │
└──────┬───────┘
       │ Binlog

┌──────────────┐
│  Debezium    │ (CDC Tool)
└──────┬───────┘
       │ Event

┌──────────────┐
│    Kafka     │
└──────┬───────┘

       ├─────────────► [Search Service] → Elasticsearch

       └─────────────► [Analytics] → Data Warehouse

Debezium 설정:

{
  "name": "order-connector",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "database.hostname": "postgres",
    "database.port": "5432",
    "database.user": "debezium",
    "database.password": "dbz",
    "database.dbname": "orderdb",
    "database.server.name": "order-db",
    "table.include.list": "public.orders,public.order_items"
  }
}

장점:

  • Application 코드 변경 불필요
  • 모든 변경 사항 캡처
  • 실시간 동기화

4. Event Sourcing

모든 변경을 이벤트로 저장:

// Event Store
interface OrderEvent {
  id: string;
  aggregateId: string;
  type: string;
  data: any;
  timestamp: Date;
}

const events: OrderEvent[] = [
  {
    id: '1',
    aggregateId: 'order-123',
    type: 'ORDER_CREATED',
    data: { customerId: 'user-1', items: [...] },
    timestamp: new Date('2024-12-01T10:00:00Z'),
  },
  {
    id: '2',
    aggregateId: 'order-123',
    type: 'PAYMENT_COMPLETED',
    data: { paymentId: 'pay-456', amount: 100 },
    timestamp: new Date('2024-12-01T10:05:00Z'),
  },
  {
    id: '3',
    aggregateId: 'order-123',
    type: 'ORDER_SHIPPED',
    data: { trackingNumber: 'TRACK-789' },
    timestamp: new Date('2024-12-01T12:00:00Z'),
  },
];

// 현재 상태 재구성
function reconstructOrder(orderId: string): Order {
  const orderEvents = events.filter(e => e.aggregateId === orderId);
  const order = new Order();

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

  return order;
}

데이터 일관성 전략

1. Saga 패턴 (복습)

// Orchestration Saga
class OrderSaga {
  async execute(orderData) {
    try {
      const order = await orderService.createOrder(orderData);
      const payment = await paymentService.charge(order);
      await inventoryService.reserve(order.items);
      await orderService.confirm(order.id);
    } catch (error) {
      // 보상 트랜잭션
      await this.compensate();
    }
  }
}

2. Eventual Consistency (최종 일관성)

일시적 불일치 허용:

// 주문 생성 후 재고는 나중에 차감
await orderService.createOrder(orderData);
// → 주문 상태: PENDING

// 몇 초 후 이벤트 처리
eventBus.subscribe('order.created', async (event) => {
  await inventoryService.decreaseStock(event.items);
  await orderService.updateStatus(event.orderId, 'CONFIRMED');
});
// → 주문 상태: CONFIRMED

3. Idempotency (멱등성)

중복 실행 방지:

class PaymentService {
  async charge(orderId: string, amount: number) {
    // 이미 결제됐는지 확인
    const existing = await paymentRepo.findByOrderId(orderId);

    if (existing) {
      console.log('Payment already processed');
      return existing; // 중복 실행 방지
    }

    // 결제 처리
    const payment = await this.processPayment(orderId, amount);
    await paymentRepo.save(payment);

    return payment;
  }
}

데이터 중복 관리

Materialized View 패턴

자주 사용하는 조인 결과를 미리 저장:

// Event Handler: Materialized View 업데이트
eventBus.subscribe('product.price_changed', async (event) => {
  // Order에 캐시된 상품 가격 업데이트
  await orderViewRepo.updateMany(
    { 'items.productId': event.productId },
    { $set: { 'items.$.currentPrice': event.newPrice } }
  );
});

Reference Data 복제

자주 변경되지 않는 데이터는 복제 허용:

// Order Service에 Product 기본 정보 복제
interface OrderItem {
  productId: string;
  productName: string;     // 복제
  productImage: string;    // 복제
  quantity: number;
  unitPrice: number;       // 주문 시점 가격
}

// Product 변경 시 동기화
eventBus.subscribe('product.updated', async (event) => {
  // 영향받는 주문이 많지 않으면 업데이트
  // 또는 그냥 두고 history 유지
});

핵심 정리

  • Database per Service로 서비스 독립성 확보
  • Polyglot Persistence: 서비스별 최적 DB 선택
  • API Composition vs CQRS: 성능과 일관성 트레이드오프
  • CDC로 자동 데이터 동기화
  • Saga, Eventual Consistency로 분산 트랜잭션 해결
  • Idempotency로 중복 실행 방지
  • 데이터 중복은 허용하되 동기화 전략 필요