🧩 마이크로서비스 아키텍처
📖 정의
마이크로서비스 아키텍처(MSA, Microservices Architecture)는 하나의 큰 애플리케이션을 여러 개의 작고 독립적인 서비스로 나누어 개발하고 배포하는 아키텍처 패턴입니다. 각 서비스는 특정 비즈니스 기능을 담당하며, 독립적으로 배포하고 확장할 수 있습니다. 모놀리식(Monolithic) 아키텍처와 달리, 서비스 간 느슨한 결합을 통해 유연성과 확장성을 제공합니다.
🎯 비유로 이해하기
대기업 vs 스타트업
모놀리식 = 대기업
├─ 모든 부서가 한 건물에
├─ 중앙 집중식 관리
├─ 한 부서 문제 → 전체 영향
├─ 변경 어려움
└─ 느린 의사결정
마 이크로서비스 = 스타트업 연합
├─ 각 팀이 독립적인 사무실
├─ 자율적인 의사결정
├─ 한 팀 문제 → 다른 팀 정상 작동
├─ 빠른 변경
└─ 유연한 확장
레고 vs 점토
모놀리식 = 점토 덩어리
┌──────────────────────────────┐
│ 사용자 │ 상품 │ 주문 │
│ 관리 │ 관리 │ 관리 │
│ 전부 하나로 │
└──────────────────────────────┘
- 전체를 다시 만들어야 함
- 한 부분 수정 → 전체 영향
- 확장 어려움
마이크로서비스 = 레고 블록
┌─────┐ ┌─────┐ ┌─────┐
│사용자│ │ 상품│ │ 주문│
│서비스│ │서비스│ │서비스│
└─────┘ └─────┘ └─────┘
- 블록 교체 쉬움
- 독립적으로 수정
- 필요한 부분만 확장
⚙️ 작동 원리
1. 모놀리식 vs 마이크로서비스
========== 모놀리식 ==========
┌─────────────────────────────────────┐
│ 하나의 애플리케이션 │
│ │
│ ┌─────────────────────────────┐ │
│ │ 사용자 관리 모듈 │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ 상품 관리 모듈 │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ 주문 관리 모듈 │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ 결제 모듈 │ │
│ └─────────────────────────────┘ │
│ │
│ 하나의 데이터베이스 │
│ 하나의 코드베이스 │
│ 하나의 배포 단위 │
└─ ────────────────────────────────────┘
장점:
✅ 개발 초기에 빠름
✅ 테스트 간단
✅ 배포 간단 (하나만)
✅ 디버깅 쉬움
단점:
❌ 규모 커지면 복잡
❌ 배포 시 전체 중단
❌ 부분 확장 불가
❌ 기술 스택 변경 어려움
========== 마이크로서비스 ==========
┌──────────┐ ┌──────────┐ ┌──────────┐
│사용자 │ │상품 │ │주문 │
│서비스 │ │서비스 │ │서비스 │
│ │ │ │ │ │
│Node.js │ │Java │ │Go │
│MongoDB │ │MySQL │ │PostgreSQL│
└──────────┘ └──────────┘ └──────────┘
↓ ↓ ↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│결제 │ │알림 │ │리뷰 │
│서비스 │ │서비스 │ │서비스 │
│ │ │ │ │ │
│Python │ │Node.js │ │Ruby │
│Redis │ │Kafka │ │Cassandra │
└─────── ───┘ └──────────┘ └──────────┘
장점:
✅ 독립적 배포
✅ 기술 스택 자유
✅ 부분 확장 가능
✅ 팀 독립성
✅ 장애 격리
단점:
❌ 초기 복잡도 높음
❌ 네트워크 통신 오버헤드
❌ 분산 트랜잭션 어려움
❌ 테스트 복잡
❌ 운영 비용 증가
2. 서비스 간 통신
========== 동기 통신 (HTTP/REST) ==========
주문 서비스 → 상품 서비스
"상품 123 재고 있나요?"
↓
"네, 5개 있습니다"
↓
주문 서비스 → 결제 서비스
"10,000원 결제해주세요"
↓
"결제 완료"
↓
주문 완료
장점: 간단, 직관적
단점: 한 서비스 장애 시 전체 실패
========== 비동기 통신 (메시지 큐) ==========
주문 서비스 → Message Queue
"주문 생성됨" 메시지 발행
↓
┌──────────────────┐
│ Message Queue │
│ (RabbitMQ, │
│ Kafka 등) │
└──────────────────┘
↓
┌────┴────┬────────┐
↓ ↓ ↓
결제 알림 재고
서비스 서비스 서비스
각자 독립적으로 처리
장점: 느슨한 결합, 장애 격리
단점: 복잡도 증가, 디버깅 어려움
========== API Gateway ==========
클라이언트 (모바일/웹)
↓
┌──────────────────┐
│ API Gateway │
│ - 라우팅 │
│ - 인증 │
│ - 로드밸런싱 │
│ - 로깅 │
└──────────────────┘
┌────┴────┬────────┐
↓ ↓ ↓
서비스A 서비스B 서비스C
역할:
- 단일 진입점
- 클라이언트 간소화
- 공통 기능 처리
3. 데이터 관리
========== 모놀리식: 공유 데이터베이스 ==========
┌─────────────────────────────────────┐
│ 애플리케이션 │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │모듈A│ │모듈B│ │모듈C│ │
│ └──┬──┘ └──┬──┘ └──┬──┘ │
└─────┼────────┼───────┼──────────────┘
└────────┼───────┘
↓
┌──────────────────┐
│ 하나의 데이터베이스│
│ ┌────┬────┬────┐│
│ │TA│TB│TC││
│ └────┴────┴────┘│
└──────────────────┘
장점: JOIN 쉬움, 일관성 보장
단점: 결합도 높음, 확장 어려움
========== 마이크로서비스: DB per Service ==========
┌──────────┐ ┌──────────┐ ┌──────────┐
│서비스 A │ │서비스 B │ │서비스 C │
└────┬─────┘ └────┬─────┘ └────┬─────┘
↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│ DB A │ │ DB B │ │ DB C │
│ (MySQL) │ │(MongoDB)│ │(PostgreSQL)│
└─────────┘ └─────────┘ └─────────┘
장점: 독립성, 기술 선택 자유
단점: JOIN 불가, 일관성 어려움
========== 데이터 일관성 ==========
// Saga 패턴
1. 주문 서비스: 주문 생성
2. 결제 서비스: 결제 처리
3. 재고 서비스: 재고 차감
4. 배송 서비스: 배송 시작
만약 3단계에서 실패?
→ 보상 트랜잭션 (Compensating Transaction)
4. 재고 차감 실패
3. 결제 취소 ← 보상
2. 주문 취소 ← 보상
💡 실제 예시
모놀리식 예시 (Express.js)
// ========== 모놀리식 애플리케이션 ==========
// server.js - 하나의 파일에 모든 기능
const express = require('express');
const app = express();
app.use(express.json());
// 하나의 데이터베이스
const db = require('./database');
// ========== 사용자 관리 ==========
app.post('/api/users', async (req, res) => {
const { username, email, password } = req.body;
const user = await db.users.create({ username, email, password });
res.json(user);
});
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(user);
});
// ========== 상품 관리 ==========
app.post('/api/products', async (req, res) => {
const { name, price, stock } = req.body;
const product = await db.products.create({ name, price, stock });
res.json(product);
});
app.get('/api/products', async (req, res) => {
const products = await db.products.findAll();
res.json(products);
});
// ========== 주문 관리 ==========
app.post('/api/orders', async (req, res) => {
const { userId, productId, quantity } = req.body;
// 트랜잭션으로 일관성 보장
const transaction = await db.sequelize.transaction();
try {
// 1. 재고 확인
const product = await db.products.findById(productId, { transaction });
if (product.stock < quantity) {
throw new Error('재고 부족');
}
// 2. 재고 차감
await product.update(
{ stock: product.stock - quantity },
{ transaction }
);
// 3. 주문 생성
const order = await db.orders.create(
{ userId, productId, quantity, total: product.price * quantity },
{ transaction }
);
// 4. 결제 처리
await processPayment(order.total);
await transaction.commit();
res.json(order);
} catch (error) {
await transaction.rollback();
res.status(400).json({ error: error.message });
}
});
// ========== 결제 처리 ==========
app.post('/api/payments', async (req, res) => {
const { orderId, amount } = req.body;
const payment = await db.payments.create({ orderId, amount });
res.json(payment);
});
// 하나의 서버로 실행
app.listen(3000, () => {
console.log('모놀리식 서버 실행: http://localhost:3000');
});
/*
장점:
- 코드가 한 곳에
- 개발 빠름
- 디버깅 쉬움
- 트랜잭션 간단
단점:
- 규모 커지면 복잡
- 배포 시 전체 재시작
- 부분 확장 불가
- 한 기능 장애 → 전체 영향
*/
마이크로서비스 예시
// ========== 1. 사용자 서비스 (user-service.js) ==========
// 포트: 3001
const express = require('express');
const app = express();
const mongoose = require('mongoose');
app.use(express.json());
// 독립적인 데이터베이스
mongoose.connect('mongodb://localhost/users-db');
const User = mongoose.model('User', {
username: String,
email: String,
password: String
});
// 사용자 생성
app.post('/users', async (req, res) => {
const { username, email, password } = req.body;
try {
const user = new User({ username, email, password });
await user.save();
// 이벤트 발행 (다른 서비스에 알림)
await publishEvent('user.created', { userId: user._id, email });
res.json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// 사용자 조회
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
});
app.listen(3001, () => {
console.log('사용자 서비스: http://localhost:3001');
});
// ========== 2. 상품 서비스 (product-service.js) ==========
// 포트: 3002
const express = require('express');
const app = express();
const { Pool } = require('pg');
app.use(express.json());
// PostgreSQL 사용 (다른 DB!)
const pool = new Pool({
host: 'localhost',
database: 'products-db',
port: 5432
});
// 상품 목록
app.get('/products', async (req, res) => {
const result = await pool.query('SELECT * FROM products');
res.json(result.rows);
});
// 상품 상세
app.get('/products/:id', async (req, res) => {
const result = await pool.query(
'SELECT * FROM products WHERE id = $1',
[req.params.id]
);
res.json(result.rows[0]);
});
// 재고 확인
app.get('/products/:id/stock', async (req, res) => {
const result = await pool.query(
'SELECT stock FROM products WHERE id = $1',
[req.params.id]
);
res.json({ stock: result.rows[0].stock });
});
// 재고 차감
app.post('/products/:id/decrease-stock', async (req, res) => {
const { quantity } = req.body;
const client = await pool.connect();
try {
await client.query('BEGIN');
// 현재 재고 확인
const result = await client.query(
'SELECT stock FROM products WHERE id = $1 FOR UPDATE',
[req.params.id]
);
const currentStock = result.rows[0].stock;
if (currentStock < quantity) {
throw new Error('재고 부족');
}
// 재고 차감
await client.query(
'UPDATE products SET stock = stock - $1 WHERE id = $2',
[quantity, req.params.id]
);
await client.query('COMMIT');
res.json({ success: true });
} catch (error) {
await client.query('ROLLBACK');
res.status(400).json({ error: error.message });
} finally {
client.release();
}
});
app.listen(3002, () => {
console.log('상품 서비스: http://localhost:3002');
});
// ========== 3. 주문 서비스 (order-service.js) ==========
// 포트: 3003
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
const orders = []; // 실제로는 데이터베이스
// 주문 생성
app.post('/orders', async (req, res) => {
const { userId, productId, quantity } = req.body;
try {
// 1. 사용자 확인 (사용자 서비스 호출)
const userResponse = await axios.get(
`http://localhost:3001/users/${userId}`
);
const user = userResponse.data;
if (!user) {
return res.status(404).json({ error: '사용자를 찾을 수 없습니다' });
}
// 2. 상품 정보 조회 (상품 서비스 호출)
const productResponse = await axios.get(
`http://localhost:3002/products/${productId}`
);
const product = productResponse.data;
// 3. 재고 확인
const stockResponse = await axios.get(
`http://localhost:3002/products/${productId}/stock`
);
const { stock } = stockResponse.data;
if (stock < quantity) {
return res.status(400).json({ error: '재고가 부족합니다' });
}
// 4. 재고 차감
await axios.post(
`http://localhost:3002/products/${productId}/decrease-stock`,
{ quantity }
);
// 5. 결제 처리 (결제 서비스 호출)
const total = product.price * quantity;
const paymentResponse = await axios.post(
'http://localhost:3004/payments',
{ userId, amount: total }
);
// 6. 주문 생성
const order = {
id: orders.length + 1,
userId,
productId,
quantity,
total,
status: 'completed',
createdAt: new Date()
};
orders.push(order);
// 7. 이벤트 발행
await publishEvent('order.created', order);
res.json(order);
} catch (error) {
// Saga 패턴: 보상 트랜잭션
console.error('주문 실패:', error.message);
// 재고 복구
try {
await axios.post(
`http://localhost:3002/products/${productId}/increase-stock`,
{ quantity }
);
} catch (rollbackError) {
console.error('재고 복구 실패:', rollbackError.message);
}
res.status(500).json({ error: '주문 처리 실패' });
}
});
// 주문 조회
app.get('/orders/:id', (req, res) => {
const order = orders.find(o => o.id === parseInt(req.params.id));
res.json(order);
});
app.listen(3003, () => {
console.log('주문 서비스: http://localhost:3003');
});
// ========== 4. 결제 서비스 (payment-service.js) ==========
// 포트: 3004
const express = require('express');
const app = express();
app.use(express.json());
const payments = [];
app.post('/payments', async (req, res) => {
const { userId, amount } = req.body;
// 외부 결제 API 호출 (예: Stripe, Toss Payments)
try {
// 실제 결제 처리
const payment = {
id: payments.length + 1,
userId,
amount,
status: 'success',
createdAt: new Date()
};
payments.push(payment);
// 이벤트 발행
await publishEvent('payment.completed', payment);
res.json(payment);
} catch (error) {
res.status(400).json({ error: '결제 실패' });
}
});
app.listen(3004, () => {
console.log('결제 서비스: http://localhost:3004');
});
// ========== 5. API Gateway (gateway.js) ==========
// 포트: 3000
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// 인증 미들웨어
function authenticate(req, res, next) {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: '인증이 필요합니다' });
}
// JWT 검증 등
next();
}
// 로깅 미들웨어
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
});
// 사용자 서비스 프록시
app.use('/api/users', authenticate, createProxyMiddleware({
target: 'http://localhost:3001',
pathRewrite: { '^/api/users': '/users' },
changeOrigin: true
}));
// 상품 서비스 프록시
app.use('/api/products', createProxyMiddleware({
target: 'http://localhost:3002',
pathRewrite: { '^/api/products': '/products' },
changeOrigin: true
}));
// 주문 서비스 프록시
app.use('/api/orders', authenticate, createProxyMiddleware({
target: 'http://localhost:3003',
pathRewrite: { '^/api/orders': '/orders' },
changeOrigin: true
}));
// 결제 서비스 프록시
app.use('/api/payments', authenticate, createProxyMiddleware({
target: 'http://localhost:3004',
pathRewrite: { '^/api/payments': '/payments' },
changeOrigin: true
}));
app.listen(3000, () => {
console.log('API Gateway: http://localhost:3000');
});
// ========== 6. 이벤트 버스 (event-bus.js) ==========
const amqp = require('amqplib');
let connection, channel;
// RabbitMQ 연결
async function connect() {
connection = await amqp.connect('amqp://localhost');
channel = await connection.createChannel();
}
// 이벤트 발행
async function publishEvent(eventType, data) {
await channel.assertQueue(eventType);
channel.sendToQueue(
eventType,
Buffer.from(JSON.stringify(data))
);
console.log(`이벤트 발행: ${eventType}`, data);
}
// 이벤트 구독
async function subscribeEvent(eventType, callback) {
await channel.assertQueue(eventType);
channel.consume(eventType, (msg) => {
const data = JSON.parse(msg.content.toString());
console.log(`이벤트 수신: ${eventType}`, data);
callback(data);
channel.ack(msg);
});
}
connect();
module.exports = { publishEvent, subscribeEvent };
Docker Compose로 마이크로서비스 실행
# docker-compose.yml
version: '3.8'
services:
# API Gateway
gateway:
build: ./gateway
ports:
- "3000:3000"
depends_on:
- user-service
- product-service
- order-service
- payment-service
# 사용자 서비스
user-service:
build: ./user-service
ports:
- "3001:3001"
environment:
- MONGO_URL=mongodb://mongo:27017/users
depends_on:
- mongo
- rabbitmq
# 상품 서비스
product-service:
build: ./product-service
ports:
- "3002:3002"
environment:
- POSTGRES_URL=postgres://postgres:password@postgres:5432/products
depends_on:
- postgres
- rabbitmq
# 주문 서비스
order-service:
build: ./order-service
ports:
- "3003:3003"
environment:
- MYSQL_URL=mysql://root:password@mysql:3306/orders
depends_on:
- mysql
- rabbitmq
# 결제 서비스
payment-service:
build: ./payment-service
ports:
- "3004:3004"
depends_on:
- rabbitmq
# 데이터베이스들
mongo:
image: mongo:6
volumes:
- mongo-data:/data/db
postgres:
image: postgres:15
environment:
- POSTGRES_PASSWORD=password
- POSTGRES_DB=products
volumes:
- postgres-data:/var/lib/postgresql/data
mysql:
image: mysql:8
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_DATABASE=orders
volumes:
- mysql-data:/var/lib/mysql
# 메시지 큐
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672" # 관리 UI
volumes:
mongo-data:
postgres-data:
mysql-data:
Service Mesh (Istio 예시)
# istio-config.yaml
# 서비스 메시 - 서비스 간 통신 관리
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
# 트래픽 분산 (카나리 배포)
- match:
- headers:
user-type:
exact: beta
route:
- destination:
host: order-service
subset: v2 # 새 버전
weight: 20
- destination:
host: order-service
subset: v1 # 기존 버전
weight: 80
# 재시도 정책
- route:
- destination:
host: order-service
retries:
attempts: 3
perTryTimeout: 2s
# 타임아웃
timeout: 10s
---
# Circuit Breaker
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: payment-service
spec:
host: payment-service
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
http:
http1MaxPendingRequests: 50
maxRequestsPerConnection: 2
outlierDetection:
consecutiveErrors: 5
interval: 30s
baseEjectionTime: 30s
maxEjectionPercent: 50
🤔 자주 묻는 질문
Q1. 언제 마이크로서비스를 사용해야 하나요?
A:
✅ 마이크로서비스가 적합한 경우:
1. 규모가 큰 애플리케이션
- 팀: 10명 이상
- 코드: 10만 줄 이상
- 사용자: 수십만 명 이상
2. 빠른 배포 필요
- 하루에 여러 번 배포
- 독립적인 기능 출시
- A/B 테스트 자주 수행
3. 다양한 기술 스택 필요
- 서비스마다 최적 기술 선택
- 레거시 시스템과 통합
4. 독립적인 확장 필요
- 특정 기능만 트래픽 많음
- 서비스별 리소스 요구사항 다름
5. 팀 독립성 중요
- 여러 팀이 동시 개발
- 팀 간 의존성 최소화
예시:
- Netflix: 수백 개의 마이크로서비스
- Amazon: 2-pizza team (팀당 서비스 담당)
- Uber: 지역별, 기능별 서비스 분리
❌ 모놀리식이 적합한 경우:
1. 작은 애플리케이션
- 팀: 5명 이하
- 기능: 명확하고 단순
- 트래픽: 적음
2. 초기 스타트업
- 빠른 MVP 개발 필요
- 요구사항 자주 변경
- 리소스 제한적
3. 단순한 CRUD
- 복잡한 비즈니스 로직 없음
- 서비스 간 경계 불명확
4. 운영 경험 부족
- DevOps 팀 없음
- 분산 시스템 경험 없음
예시:
- 블로그, 포트폴리오
- 소규모 전자상거래
- 사내 도구
📊 의사결정 체크리스트:
□ 팀 규모 10명 이상?
□ 코드베이스 10만 줄 이상?
□ 독립 배포 자주 필요?
□ 부분 확장 필요?
□ DevOps 팀 있음?
□ 분산 시스템 경험 있음?
3개 이상 체크 → 마이크로서비스 고려
2개 이하 → 모놀리식 유지
Q2. 마이크로서비스의 가장 큰 도전 과제는?
A:
// ========== 1. 분산 트랜잭션 ==========
// 모놀리식: 간단한 트랜잭션
await db.transaction(async (t) => {
await createOrder(data, t);
await decreaseStock(productId, t);
await processPayment(amount, t);
// 하나라도 실패하면 전체 롤백
});
// 마이크로서비스: 복잡한 Saga 패턴
async function createOrderSaga(data) {
try {
// 1단계
const order = await orderService.create(data);
// 2단계
await productService.decreaseStock(data.productId);
// 3단계
await paymentService.process(order.total);
return order;
} catch (error) {
// 보상 트랜잭션 (역순으로)
await paymentService.refund(order.total);
await productService.increaseStock(data.productId);
await orderService.cancel(order.id);
throw error;
}
}
// ========== 2. 데이터 일관성 ==========
// 문제: 여러 서비스에 흩어진 데이터
// 사용자 서비스: userId, name
// 주문 서비스: userId, orders
// 결제 서비스: userId, payments
// 해결 1: 이벤트 소싱
eventBus.on('user.updated', async (event) => {
// 사용자 정보가 변경되면 다른 서비스도 업데이트
await orderService.updateUserInfo(event.userId, event.name);
await paymentService.updateUserInfo(event.userId, event.name);
});
// 해결 2: CQRS (Command Query Responsibility Segregation)
// 쓰기와 읽기 분리
// 쓰기: 각 서비스 독립적
// 읽기: 통합된 뷰 (Read Model)
// ========== 3. 네트워크 지연 ==========
// 모놀리식: 함수 호출 (빠름)
const user = getUser(userId); // 1ms
// 마이크로서비스: HTTP 요청 (느림)
const user = await axios.get(`http://user-service/users/${userId}`); // 50ms
// 해결: 캐싱
const redis = require('redis');
const cache = redis.createClient();
async function getUser(userId) {
// 1. 캐시 확인
const cached = await cache.get(`user:${userId}`);
if (cached) return JSON.parse(cached);
// 2. 서비스 호출
const response = await axios.get(`http://user-service/users/${userId}`);
const user = response.data;
// 3. 캐시 저장
await cache.setex(`user:${userId}`, 3600, JSON.stringify(user));
return user;
}
// ========== 4. 서비스 장애 처리 ==========
// Circuit Breaker 패턴
const CircuitBreaker = require('opossum');
const options = {
timeout: 3000, // 3초 타임아웃
errorThresholdPercentage: 50, // 50% 실패 시
resetTimeout: 30000 // 30초 후 재시도
};
const breaker = new CircuitBreaker(async (userId) => {
return await axios.get(`http://user-service/users/${userId}`);
}, options);
breaker.fallback(() => ({
id: userId,
name: '알 수 없음', // 폴백 데이터
cached: true
}));
// 사용
breaker.fire(userId)
.then(console.log)
.catch(console.error);
// ========== 5. 모니터링 및 디버깅 ==========
// 분산 추적 (Distributed Tracing)
// Jaeger, Zipkin 사용
const tracer = require('jaeger-client').initTracer(config);
app.use((req, res, next) => {
const span = tracer.startSpan('http_request');
span.setTag('http.method', req.method);
span.setTag('http.url', req.url);
req.span = span;
next();
});
// 서비스 간 호출 시 추적 ID 전달
await axios.get('http://order-service/orders', {
headers: {
'x-trace-id': req.span.context().toTraceId()
}
});
Q3. API Gateway의 역할은?
A:
// ========== API Gateway 주요 기능 ==========
const express = require('express');
const rateLimit = require('express-rate-limit');
const jwt = require('jsonwebtoken');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// ========== 1. 라우팅 ==========
// 클라이언트는 하나의 엔드포인트만 알면 됨
app.use('/api/users', createProxyMiddleware({
target: 'http://user-service:3001',
changeOrigin: true
}));
app.use('/api/products', createProxyMiddleware({
target: 'http://product-service:3002',
changeOrigin: true
}));
// ========== 2. 인증 및 인가 ==========
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: '토큰이 필요합니다' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: '유효하지 않은 토큰' });
}
}
app.use('/api/orders', authenticate, createProxyMiddleware({
target: 'http://order-service:3003'
}));
// ========== 3. Rate Limiting ==========
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 100 // 최대 100개 요청
});
app.use('/api/', limiter);
// ========== 4. 로드 밸런싱 ==========
const productServiceInstances = [
'http://product-service-1:3002',
'http://product-service-2:3002',
'http://product-service-3:3002'
];
let currentIndex = 0;
app.use('/api/products', createProxyMiddleware({
target: productServiceInstances[currentIndex],
router: () => {
// 라운드 로빈
const target = productServiceInstances[currentIndex];
currentIndex = (currentIndex + 1) % productServiceInstances.length;
return target;
}
}));
// ========== 5. 요청/응답 변환 ==========
app.use('/api/legacy', createProxyMiddleware({
target: 'http://legacy-service:8080',
onProxyReq: (proxyReq, req) => {
// 요청 변환
proxyReq.setHeader('X-API-Version', '2.0');
},
onProxyRes: (proxyRes, req, res) => {
// 응답 변환
proxyRes.headers['X-Custom-Header'] = 'Gateway';
}
}));
// ========== 6. 캐싱 ==========
const redis = require('redis');
const cache = redis.createClient();
app.get('/api/products/:id', async (req, res) => {
const cacheKey = `product:${req.params.id}`;
// 캐시 확인
const cached = await cache.get(cacheKey);
if (cached) {
return res.json(JSON.parse(cached));
}
// 서비스 호출
const response = await axios.get(
`http://product-service:3002/products/${req.params.id}`
);
// 캐시 저장
await cache.setex(cacheKey, 3600, JSON.stringify(response.data));
res.json(response.data);
});
// ========== 7. 로깅 및 모니터링 ==========
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log({
method: req.method,
path: req.path,
status: res.statusCode,
duration: `${duration}ms`,
user: req.user?.id
});
});
next();
});
// ========== 8. 에러 처리 ==========
app.use((err, req, res, next) => {
console.error('Gateway Error:', err);
if (err.code === 'ECONNREFUSED') {
return res.status(503).json({
error: '서비스를 사용할 수 없습니다'
});
}
res.status(500).json({
error: '서버 오류가 발생했습니다'
});
});
// ========== 9. 서비스 디스커버리 ==========
const consul = require('consul')();
async function getServiceUrl(serviceName) {
const result = await consul.health.service({
service: serviceName,
passing: true // 헬스 체크 통과한 인스턴스만
});
if (result.length === 0) {
throw new Error(`${serviceName} 서비스를 찾을 수 없습니다`);
}
// 랜덤하게 선택
const instance = result[Math.floor(Math.random() * result.length)];
return `http://${instance.Service.Address}:${instance.Service.Port}`;
}
app.listen(3000);
Q4. 마이크로서비스 배포 전략은?
A:
# ========== 1. Blue-Green 배포 ==========
# 새 버전(Green)을 배포하고 트래픽을 한 번에 전환
# Blue (현재 버전)
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
version: blue # 현재 트래픽
ports:
- port: 80
---
# Green (새 버전) 배포
kubectl apply -f order-service-green.yaml
# 테스트 후 트래픽 전환
kubectl patch service order-service -p '{"spec":{"selector":{"version":"green"}}}'
# 문제 있으면 즉시 롤백
kubectl patch service order-service -p '{"spec":{"selector":{"version":"blue"}}}'
# ========== 2. 카나리 배포 ==========
# 일부 트래픽만 새 버전으로 보내며 점진적으로 확대
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
- route:
- destination:
host: order-service
subset: v1 # 기존 버전
weight: 90 # 90% 트래픽
- destination:
host: order-service
subset: v2 # 새 버전
weight: 10 # 10% 트래픽
# 점진적으로 증가
# 10% → 25% → 50% → 75% → 100%
# ========== 3. 롤링 업데이트 ==========
# Kubernetes 기본 전략
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 최대 1개 추가 생성
maxUnavailable: 1 # 최대 1개 중단 허용
template:
spec:
containers:
- name: order-service
image: order-service:v2
# 순서:
# 1. 새 Pod 1개 시작
# 2. 헬스 체크 통과하면 기존 Pod 1개 종료
# 3. 반복 (5개 모두 교체될 때까지)
# ========== 4. Docker Compose 배포 ==========
# docker-compose.yml
version: '3.8'
services:
order-service:
image: order-service:latest
deploy:
replicas: 3
update_config:
parallelism: 1 # 한 번에 1개씩
delay: 10s # 10초 간격
failure_action: rollback # 실패 시 롤백
restart_policy:
condition: on-failure
# 배포
docker stack deploy -c docker-compose.yml myapp
# ========== 5. CI/CD 파이프라인 ==========
# .github/workflows/deploy.yml
name: Deploy Microservices
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# 각 서비스 독립적으로 배포
- name: 변경된 서비스 감지
uses: dorny/paths-filter@v2
id: changes
with:
filters: |
user-service:
- 'services/user/**'
product-service:
- 'services/product/**'
- name: 사용자 서비스 배포
if: steps.changes.outputs.user-service == 'true'
run: |
docker build -t user-service:${{ github.sha }} services/user
docker push user-service:${{ github.sha }}
kubectl set image deployment/user-service user-service=user-service:${{ github.sha }}
- name: 상품 서비스 배포
if: steps.changes.outputs.product-service == 'true'
run: |
docker build -t product-service:${{ github.sha }} services/product
docker push product-service:${{ github.sha }}
kubectl set image deployment/product-service product-service=product-service:${{ github.sha }}
Q5. 모놀리식에서 마이크로서비스로 마이그레이션하려면?
A:
// ========== 단계별 마이그레이션 전략 ==========
// ========== 1단계: Strangler Fig 패턴 ==========
// 새 기능은 마이크로서비스로, 기존 기능은 유지
┌─────────────────────────────────┐
│ 모놀리식 애플리케이션 │
│ ┌──────────┬──────────┬──────┐ │
│ │사용자관리 │상품관리 │주문 │ │
│ │ │ │관리 │ │
│ └──────────┴──────────┴──────┘ │
└─────────────────────────────────┘
// Step 1: 새 기능(알림)을 마이크로서비스로
┌─────────────────────┐ ┌──────────────┐
│ 모놀리식 │ │알림 서비스 │
│ 사용자│상품│주문 │ │(새로 만듦) │
└─────────────────────┘ └──────────────┘
// Step 2: 주문 기능 분리
┌─────────────────────┐ ┌──────────────┐
│ 모놀리식 │ │주문 서비스 │
│ 사용자│상품 │ │(분리) │
└─────────────────────┘ └──────────────┘
┌──────────────┐
│알림 서비스 │
└──────────────┘
// Step 3: 모든 기능 분리
┌──────────┐ ┌──────────┐ ┌──────────┐
│사용자 │ │상품 │ │주문 │
│서비스 │ │서비스 │ │서비스 │
└──────────┘ └──────────┘ └──────────┘
┌──────────┐
│알림 서비스│
└──────────┘
// ========== 2단계: API Gateway 도입 ==========
// 기존: 클라이언트가 모놀리식 직접 호출
const response = await fetch('http://monolith/api/orders');
// 변경: API Gateway를 통해 호출
const response = await fetch('http://api-gateway/api/orders');
// API Gateway가 라우팅
if (route === '/api/orders') {
// 새 서비스로 라우팅
proxy('http://order-service/orders');
} else {
// 아직 모놀리식으로
proxy('http://monolith/api');
}
// ========== 3단계: 데이터베이스 분리 ==========
// 문제: 공유 데이터베이스
┌────────────────┐
│ 모놀리식 DB │
│ ┌──────────┐ │
│ │Users │ │
│ │Products │ │
│ │Orders │ │
│ └──────────┘ │
└────────────────┘
// 해결: Database per Service
// 1) 이중 쓰기 (Dual Write)
async function createOrder(data) {
// 모놀리식 DB에도 쓰기
await monolithDB.orders.create(data);
// 새 서비스 DB에도 쓰기
await orderServiceDB.orders.create(data);
}
// 2) Change Data Capture (CDC)
// 모놀리식 DB의 변경사항을 자동으로 동기화
const debezium = require('debezium');
debezium.on('orders.insert', async (change) => {
// 새 서비스 DB에 반영
await orderServiceDB.orders.create(change.data);
});
// 3) 완전 분리
┌──────────┐ ┌──────────┐ ┌──────────┐
│Users DB │ │Products │ │Orders DB │
│(MongoDB) │ │DB(MySQL) │ │(Postgres)│
└──────────┘ └──────────┘ └──────────┘
// ========== 4단계: 점진적 트래픽 전환 ==========
// API Gateway에서 비율 조정
const MIGRATION_PERCENTAGE = 10; // 10%만 새 서비스로
app.use('/api/orders', (req, res, next) => {
if (Math.random() * 100 < MIGRATION_PERCENTAGE) {
// 새 서비스로
proxy('http://order-service/orders')(req, res, next);
} else {
// 모놀리식으로
proxy('http://monolith/api/orders')(req, res, next);
}
});
// 점진적으로 증가
// 10% → 25% → 50% → 75% → 100%
// ========== 5단계: 모니터링 및 롤백 준비 ==========
const NEW_SERVICE_ERROR_THRESHOLD = 0.05; // 5% 에러율
async function monitorNewService() {
const errorRate = await getErrorRate('order-service');
if (errorRate > NEW_SERVICE_ERROR_THRESHOLD) {
// 에러율 높으면 롤백
console.error('에러율 높음! 모놀리식으로 롤백');
MIGRATION_PERCENTAGE = 0;
// 알림
await sendAlert('마이그레이션 롤백 발생');
}
}
// ========== 실전 체크리스트 ==========
마이그레이션 체크리스트:
□ 1. 경계 식별
- 도메인 주도 설계 (DDD)
- 비즈니스 기능별로 나누기
□ 2. 가장 독립적인 기능부터 시작
- 의존성 적은 것
- 비즈니스 영향 적은 것
- 예: 알림, 로깅, 검색
□ 3. API Gateway 도입
- 점진적 트래픽 전환
□ 4. 데이터베이스 분리 전략
- 이중 쓰기 → CDC → 완전 분리
□ 5. 모니터링 강화
- 에러율, 응답 시간, 트래픽
- 롤백 준비
□ 6. 팀 교육
- 마이크로서비스 아키텍처
- DevOps 도구 (Docker, Kubernetes)
□ 7. 문서화
- 서비스 카탈로그
- API 문서
- 배포 가이드
🎓 다음 단계
마이크로서비스 아키텍처를 이해했다면, 다음을 학습해보세요:
- Docker란? (문서 작성 예정) - 컨테이너화
- CI/CD란? - 자동화 배포
- REST API vs GraphQL - API 설계
실습해보기
# ========== 1. 간단한 마이크로서비스 실습 ==========
# 프로젝트 구조
mkdir microservices-demo
cd microservices-demo
# 서비스 생성
mkdir -p services/{user,product,order}
mkdir gateway
# Docker Compose 작성
cat > docker-compose.yml
# 실행
docker-compose up -d
# 로그 확인
docker-compose logs -f
# ========== 2. Kubernetes 배포 ==========
# minikube 설치 (로컬 K8s)
brew install minikube
minikube start
# 배포
kubectl apply -f kubernetes/
# 서비스 확인
kubectl get pods
kubectl get services
# 로그 확인
kubectl logs <pod-name>
# ========== 3. Istio 설치 (Service Mesh) ==========
# Istio 설치
istioctl install --set profile=demo -y
# 서비스에 사이드카 주입
kubectl label namespace default istio-injection=enabled
# Istio 대시보드
istioctl dashboard kiali