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) 확보가 운영의 핵심