데이터베이스 패턴 - 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로 중복 실행 방지
- 데이터 중복은 허용하되 동기화 전략 필요