본문으로 건너뛰기

🧩 마이크로서비스 아키텍처

📖 정의

마이크로서비스 아키텍처(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 문서
- 배포 가이드

🎓 다음 단계

마이크로서비스 아키텍처를 이해했다면, 다음을 학습해보세요:

  1. Docker란? (문서 작성 예정) - 컨테이너화
  2. CI/CD란? - 자동화 배포
  3. 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

🎬 마무리

마이크로서비스 아키텍처는 확장성과 유연성을 제공합니다:

  • 독립성: 서비스별 독립적인 개발, 배포, 확장
  • 기술 다양성: 서비스마다 최적의 기술 선택
  • 장애 격리: 한 서비스 장애가 전체에 영향 없음
  • 팀 자율성: 팀별로 서비스 책임

하지만 복잡도가 증가하므로, 프로젝트 규모와 팀 역량을 고려하여 선택하세요! 🧩