🏗️ 아키텍처

Microservices

마이크로서비스 아키텍처 (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)으로 디버깅.

🔗 관련 용어

📚 더 배우기