Microservices
마이크로서비스 아키텍처 (MSA)
애플리케이션을 작은 독립적인 서비스들로 분해하는 아키텍처 스타일입니다. 각 서비스는 단일 비즈니스 기능을 담당하며, 독립적으로 배포, 확장, 개발될 수 있고 API를 통해 서로 통신합니다.
마이크로서비스 아키텍처 (MSA)
애플리케이션을 작은 독립적인 서비스들로 분해하는 아키텍처 스타일입니다. 각 서비스는 단일 비즈니스 기능을 담당하며, 독립적으로 배포, 확장, 개발될 수 있고 API를 통해 서로 통신합니다.
Microservices 아키텍처는 전통적인 모놀리식(Monolithic) 아키텍처의 한계를 극복하기 위해 등장했습니다. 모놀리식은 하나의 거대한 코드베이스로 구성되어 배포와 확장이 어렵고, 한 부분의 장애가 전체 시스템에 영향을 미칩니다. 반면 마이크로서비스는 각 서비스가 독립적으로 운영되어 이러한 문제를 해결합니다.
핵심 원칙으로는 단일 책임 원칙(Single Responsibility), 느슨한 결합(Loose Coupling), 높은 응집도(High Cohesion)가 있습니다. 각 서비스는 자체 데이터베이스를 가지며(Database per Service), 다른 서비스의 데이터에 직접 접근하지 않고 API를 통해서만 통신합니다.
서비스 간 통신은 동기(Synchronous)와 비동기(Asynchronous) 방식으로 나뉩니다. 동기 방식은 REST API나 gRPC를 사용하며, 비동기 방식은 메시지 큐(RabbitMQ, Kafka)를 통해 이벤트 기반으로 통신합니다. 비동기 방식이 서비스 간 결합도를 더 낮출 수 있습니다.
API Gateway는 클라이언트와 마이크로서비스 사이의 단일 진입점 역할을 합니다. 인증, 라우팅, 로드 밸런싱, 속도 제한 등의 횡단 관심사를 처리합니다. Kong, AWS API Gateway, Netflix Zuul 등이 대표적입니다.
Service Mesh는 서비스 간 통신을 관리하는 인프라 계층입니다. Istio, Linkerd 같은 도구가 트래픽 관리, 보안, 관찰성을 제공합니다. 각 서비스에 사이드카 프록시를 배치하여 네트워크 로직을 애플리케이션 코드에서 분리합니다.
# 마이크로서비스 예제 - 주문 서비스 (Order Service)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional
import httpx
import asyncio
app = FastAPI(title="Order Service", version="1.0.0")
# 다른 서비스 URL (환경변수로 관리)
USER_SERVICE_URL = "http://user-service:8001"
PRODUCT_SERVICE_URL = "http://product-service:8002"
PAYMENT_SERVICE_URL = "http://payment-service:8003"
class OrderRequest(BaseModel):
user_id: str
product_id: str
quantity: int
payment_method: str
class Order(BaseModel):
id: str
user_id: str
product_id: str
quantity: int
total_amount: float
status: str
# 서비스 간 통신 - HTTP Client
async def call_service(url: str, timeout: float = 5.0):
"""다른 마이크로서비스 호출 (Circuit Breaker 패턴 적용)"""
async with httpx.AsyncClient(timeout=timeout) as client:
try:
response = await client.get(url)
response.raise_for_status()
return response.json()
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="Service timeout")
except httpx.HTTPStatusError as e:
raise HTTPException(status_code=e.response.status_code)
@app.post("/orders", response_model=Order)
async def create_order(request: OrderRequest):
"""
주문 생성 - 여러 마이크로서비스와 협력
1. 사용자 검증 (User Service)
2. 상품 정보 조회 (Product Service)
3. 결제 처리 (Payment Service)
"""
# 1. 사용자 검증 (User Service 호출)
user = await call_service(f"{USER_SERVICE_URL}/users/{request.user_id}")
if not user.get("active"):
raise HTTPException(status_code=400, detail="User is not active")
# 2. 상품 정보 조회 (Product Service 호출)
product = await call_service(f"{PRODUCT_SERVICE_URL}/products/{request.product_id}")
if product.get("stock", 0) < request.quantity:
raise HTTPException(status_code=400, detail="Insufficient stock")
total_amount = product["price"] * request.quantity
# 3. 결제 처리 (Payment Service 호출)
async with httpx.AsyncClient() as client:
payment_response = await client.post(
f"{PAYMENT_SERVICE_URL}/payments",
json={
"user_id": request.user_id,
"amount": total_amount,
"method": request.payment_method
}
)
if payment_response.status_code != 200:
raise HTTPException(status_code=400, detail="Payment failed")
# 4. 주문 생성 (로컬 DB에 저장)
order = Order(
id=f"ORD-{asyncio.get_event_loop().time()}",
user_id=request.user_id,
product_id=request.product_id,
quantity=request.quantity,
total_amount=total_amount,
status="CONFIRMED"
)
# 5. 이벤트 발행 (비동기 처리를 위해)
# await publish_event("order.created", order.dict())
return order
@app.get("/health")
async def health_check():
"""헬스 체크 - Kubernetes liveness/readiness probe용"""
return {"status": "healthy", "service": "order-service"}
# Circuit Breaker 패턴 구현
class CircuitBreaker:
def __init__(self, failure_threshold: int = 5, reset_timeout: int = 30):
self.failure_count = 0
self.failure_threshold = failure_threshold
self.reset_timeout = reset_timeout
self.state = "CLOSED" # CLOSED, OPEN, HALF_OPEN
async def call(self, func, *args, **kwargs):
if self.state == "OPEN":
raise HTTPException(status_code=503, detail="Circuit breaker is open")
try:
result = await func(*args, **kwargs)
self.failure_count = 0
self.state = "CLOSED"
return result
except Exception as e:
self.failure_count += 1
if self.failure_count >= self.failure_threshold:
self.state = "OPEN"
raise e
// 마이크로서비스 예제 - 상품 서비스 (Product Service)
const express = require('express');
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
const app = express();
app.use(express.json());
// 서비스 레지스트리 (실제로는 Consul, Eureka 등 사용)
const SERVICE_REGISTRY = {
inventory: 'http://inventory-service:8004',
pricing: 'http://pricing-service:8005'
};
// 인메모리 데이터 (실제로는 각 서비스별 DB 사용)
const products = new Map();
/**
* 상품 등록 API
*/
app.post('/products', async (req, res) => {
try {
const { name, description, category, basePrice } = req.body;
const product = {
id: uuidv4(),
name,
description,
category,
basePrice,
createdAt: new Date().toISOString()
};
products.set(product.id, product);
// 이벤트 발행 (다른 서비스에 알림)
await publishEvent('product.created', product);
res.status(201).json(product);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
/**
* 상품 조회 API - 여러 서비스 데이터 조합
*/
app.get('/products/:id', async (req, res) => {
try {
const product = products.get(req.params.id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
// 여러 서비스에서 데이터 병렬 조회 (API Composition)
const [inventory, pricing] = await Promise.all([
callService(`${SERVICE_REGISTRY.inventory}/stock/${product.id}`),
callService(`${SERVICE_REGISTRY.pricing}/price/${product.id}`)
]);
// 데이터 조합하여 반환
res.json({
...product,
stock: inventory?.quantity || 0,
price: pricing?.currentPrice || product.basePrice,
discount: pricing?.discount || 0
});
} catch (error) {
// Fallback: 기본 상품 정보만 반환
const product = products.get(req.params.id);
res.json({ ...product, stock: 'unavailable', price: product?.basePrice });
}
});
/**
* 서비스 간 HTTP 통신 (타임아웃 및 재시도 포함)
*/
async function callService(url, options = {}) {
const { retries = 3, timeout = 3000 } = options;
for (let i = 0; i < retries; i++) {
try {
const response = await axios.get(url, { timeout });
return response.data;
} catch (error) {
if (i === retries - 1) throw error;
// 지수 백오프 (Exponential Backoff)
await new Promise(r => setTimeout(r, Math.pow(2, i) * 100));
}
}
}
/**
* 이벤트 발행 (메시지 큐로 전송)
*/
async function publishEvent(eventType, data) {
// 실제로는 Kafka, RabbitMQ 등 사용
console.log(`[Event Published] ${eventType}:`, data);
// await kafkaProducer.send({ topic: eventType, messages: [{ value: JSON.stringify(data) }] });
}
/**
* 헬스 체크 엔드포인트
*/
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
service: 'product-service',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
/**
* Readiness 체크 (의존 서비스 확인)
*/
app.get('/ready', async (req, res) => {
try {
// 의존 서비스 헬스 체크
await Promise.all([
axios.get(`${SERVICE_REGISTRY.inventory}/health`, { timeout: 1000 }),
axios.get(`${SERVICE_REGISTRY.pricing}/health`, { timeout: 1000 })
]);
res.json({ status: 'ready' });
} catch (error) {
res.status(503).json({ status: 'not ready', error: error.message });
}
});
const PORT = process.env.PORT || 8002;
app.listen(PORT, () => {
console.log(`Product Service running on port ${PORT}`);
});
┌─────────────────────────────────────────────────────────────────────────┐
│ Microservices Architecture │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────┐
│ Client │
│ (Web/App) │
└──────┬──────┘
│
┌──────▼──────┐
│ API Gateway │ ← 인증, 라우팅, 속도제한
│ (Kong/Zuul) │
└──────┬──────┘
│
┌─────────────────────────┼─────────────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ User │ │ Order │ │ Product │
│ Service │◄────────►│ Service │◄────────►│ Service │
└──────┬──────┘ REST └──────┬──────┘ REST └──────┬──────┘
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ User DB │ │ Order DB │ │ Product DB │
│ (PostgreSQL)│ │ (PostgreSQL)│ │ (MongoDB) │
└─────────────┘ └─────────────┘ └─────────────┘
│
│ Events (Kafka)
▼
┌─────────────────────────────────────────────────────┐
│ Message Queue │
│ (Apache Kafka) │
└──────────┬────────────────────────────┬─────────────┘
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Payment │ │ Notification│
│ Service │ │ Service │
└──────┬──────┘ └──────┬──────┘
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Payment DB │ │ Redis │
└─────────────┘ │ (Cache) │
└─────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
├─────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Kubernetes │ │ Istio │ │ Prometheus │ │ Jaeger │ │
│ │ (Container │ │ (Service │ │ (Metrics) │ │ (Tracing) │ │
│ │ Orchestr.) │ │ Mesh) │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
핵심 패턴:
• API Gateway: 단일 진입점, 횡단 관심사 처리
• Database per Service: 서비스별 독립 데이터베이스
• Event-Driven: 비동기 통신으로 느슨한 결합
• Circuit Breaker: 장애 전파 방지
• Service Discovery: 동적 서비스 위치 탐색
• Distributed Tracing: 요청 흐름 추적
"주문 도메인이 복잡해지면서 모놀리식의 한계가 드러나고 있어요. 마이크로서비스로 전환하면 주문, 결제, 배송 팀이 각자 독립적으로 배포할 수 있고, 트래픽이 몰리는 서비스만 선택적으로 스케일아웃 할 수 있습니다."
"서비스 간 동기 통신은 REST보다 gRPC가 성능이 좋지만, 외부 공개 API는 REST가 호환성이 높아요. 내부 서비스 간엔 Kafka로 이벤트 기반 비동기 통신을 쓰면 결합도를 더 낮출 수 있습니다."
"결제 서비스가 다운됐을 때 주문 서비스까지 멈춘 건 Circuit Breaker가 없었기 때문입니다. Hystrix나 Resilience4j로 장애 전파를 막고, 타임아웃과 fallback 로직을 추가해야 합니다."
여러 서비스에 걸친 트랜잭션은 ACID를 보장하기 어렵습니다. Saga 패턴으로 보상 트랜잭션을 구현하거나, 최종 일관성(Eventual Consistency)을 수용해야 합니다.
도메인 경계가 불명확한 초기 단계에서 마이크로서비스는 과도한 복잡성을 유발합니다. 모놀리식으로 시작해서 도메인이 명확해진 후 분리하세요.
서비스 간 데이터베이스 공유는 강한 결합을 만듭니다. 각 서비스는 자체 DB를 가지고, 데이터가 필요하면 API나 이벤트로 동기화하세요.
도메인 주도 설계(DDD)로 서비스 경계 정의, 컨테이너(Docker/K8s)로 배포, 중앙 집중식 로깅/모니터링(ELK, Prometheus), 분산 추적(Jaeger)으로 디버깅.