마이크로서비스 아키텍처 설계와 모니터링 전략
소개
마이크로서비스는 “서비스 하나만 죽어도 전체가 멈춘다”는 모놀리식 문제를 줄여 주지만, 설계·배포·모니터링이 잘못되면 복잡도만 올라갑니다. 리워드·이벤트·분석처럼 도메인을 나눠 서비스를 구성해 본 경험을 바탕으로, 설계와 운영에 필요한 핵심 개념과 실전 전략을 정리했습니다.
마이크로서비스 아키텍처의 핵심 원칙
1. 단일 책임 원칙 (Single Responsibility Principle)
각 마이크로서비스는 하나의 비즈니스 기능에만 집중해야 합니다.
typescript
1// 나쁜 예: 사용자 관리와 주문 관리를 함께 처리
2class UserOrderService {
3 createUser() { }
4 createOrder() { }
5 updateUser() { }
6 updateOrder() { }
7}
8
9// 좋은 예: 각 서비스가 단일 책임을 가짐
10class UserService {
11 createUser() { }
12 updateUser() { }
13 deleteUser() { }
14}
15
16class OrderService {
17 createOrder() { }
18 updateOrder() { }
19 cancelOrder() { }
20}2. 독립적인 배포
각 서비스는 독립적으로 배포 가능해야 합니다.
yaml
1# docker-compose.yml 예시
2version: '3.8'
3services:
4 user-service:
5 build: ./user-service
6 ports:
7 - "3001:3000"
8 environment:
9 - DATABASE_URL=postgresql://user:pass@db:5432/users
10
11 order-service:
12 build: ./order-service
13 ports:
14 - "3002:3000"
15 environment:
16 - DATABASE_URL=postgresql://user:pass@db:5432/orders
17 depends_on:
18 - user-service3. 데이터베이스 분리
각 서비스는 자신만의 데이터베이스를 가져야 합니다.
typescript
1// User Service의 데이터베이스
2interface UserDatabase {
3 users: User[];
4 profiles: Profile[];
5}
6
7// Order Service의 데이터베이스
8interface OrderDatabase {
9 orders: Order[];
10 orderItems: OrderItem[];
11}서비스 간 통신 패턴
동기 통신: REST API
typescript
1// Order Service에서 User Service 호출
2class OrderService {
3 async createOrder(userId: string, items: OrderItem[]) {
4 // 동기 호출
5 const user = await fetch(`http://user-service/api/users/${userId}`);
6 const userData = await user.json();
7
8 if (!userData.active) {
9 throw new Error('User is not active');
10 }
11
12 // 주문 생성 로직
13 return this.orderRepository.create({
14 userId,
15 items,
16 status: 'pending'
17 });
18 }
19}비동기 통신: 메시지 큐
typescript
1// RabbitMQ를 사용한 비동기 통신
2import amqp from 'amqplib';
3
4class NotificationService {
5 async sendOrderConfirmation(orderId: string) {
6 const connection = await amqp.connect('amqp://localhost');
7 const channel = await connection.createChannel();
8
9 const queue = 'order-confirmations';
10 await channel.assertQueue(queue, { durable: true });
11
12 channel.sendToQueue(queue, Buffer.from(JSON.stringify({
13 orderId,
14 timestamp: new Date().toISOString()
15 })), {
16 persistent: true
17 });
18
19 await channel.close();
20 await connection.close();
21 }
22}이벤트 기반 아키텍처
typescript
1// 이벤트 발행
2class OrderService {
3 async createOrder(orderData: OrderData) {
4 const order = await this.orderRepository.create(orderData);
5
6 // 이벤트 발행
7 await this.eventBus.publish('order.created', {
8 orderId: order.id,
9 userId: order.userId,
10 total: order.total,
11 timestamp: new Date()
12 });
13
14 return order;
15 }
16}
17
18// 이벤트 구독
19class InventoryService {
20 async onOrderCreated(event: OrderCreatedEvent) {
21 // 재고 차감
22 for (const item of event.items) {
23 await this.inventoryRepository.decrease(item.productId, item.quantity);
24 }
25 }
26}서비스 디스커버리
Eureka를 사용한 서비스 디스커버리
typescript
1// 서비스 등록
2import { Eureka } from 'eureka-js-client';
3
4const client = new Eureka({
5 instance: {
6 app: 'order-service',
7 hostName: 'localhost',
8 ipAddr: '127.0.0.1',
9 port: {
10 '$': 3002,
11 '@enabled': 'true',
12 },
13 vipAddress: 'order-service',
14 dataCenterInfo: {
15 '@class': 'com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo',
16 name: 'MyOwn',
17 },
18 },
19 eureka: {
20 host: 'localhost',
21 port: 8761,
22 servicePath: '/eureka/apps/',
23 },
24});
25
26client.start();Kubernetes 서비스 디스커버리
yaml
1# user-service.yaml
2apiVersion: v1
3kind: Service
4metadata:
5 name: user-service
6spec:
7 selector:
8 app: user-service
9 ports:
10 - protocol: TCP
11 port: 80
12 targetPort: 3000
13 type: ClusterIP
14---
15apiVersion: apps/v1
16kind: Deployment
17metadata:
18 name: user-service
19spec:
20 replicas: 3
21 selector:
22 matchLabels:
23 app: user-service
24 template:
25 metadata:
26 labels:
27 app: user-service
28 spec:
29 containers:
30 - name: user-service
31 image: user-service:latest
32 ports:
33 - containerPort: 3000API 게이트웨이 패턴
typescript
1// API Gateway 구현
2import express from 'express';
3import { createProxyMiddleware } from 'http-proxy-middleware';
4
5const app = express();
6
7// 라우팅 규칙
8app.use('/api/users', createProxyMiddleware({
9 target: 'http://user-service:3000',
10 changeOrigin: true,
11 pathRewrite: {
12 '^/api/users': '/api'
13 }
14}));
15
16app.use('/api/orders', createProxyMiddleware({
17 target: 'http://order-service:3000',
18 changeOrigin: true,
19 pathRewrite: {
20 '^/api/orders': '/api'
21 }
22}));
23
24// 인증 미들웨어
25app.use((req, res, next) => {
26 const token = req.headers.authorization;
27 if (!token) {
28 return res.status(401).json({ error: 'Unauthorized' });
29 }
30 // 토큰 검증 로직
31 next();
32});
33
34app.listen(8080);분산 추적 (Distributed Tracing)
OpenTelemetry를 사용한 추적
typescript
1import { NodeTracerProvider } from '@opentelemetry/node';
2import { SimpleSpanProcessor } from '@opentelemetry/tracing';
3import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
4import { Resource } from '@opentelemetry/resources';
5import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
6
7const provider = new NodeTracerProvider({
8 resource: new Resource({
9 [SemanticResourceAttributes.SERVICE_NAME]: 'order-service',
10 }),
11});
12
13provider.addSpanProcessor(
14 new SimpleSpanProcessor(
15 new JaegerExporter({
16 endpoint: 'http://localhost:14268/api/traces',
17 })
18 )
19);
20
21provider.register();
22
23// 추적 사용
24import { trace } from '@opentelemetry/api';
25
26const tracer = trace.getTracer('order-service');
27
28async function createOrder(orderData: OrderData) {
29 const span = tracer.startSpan('createOrder');
30
31 try {
32 span.setAttribute('order.userId', orderData.userId);
33 span.setAttribute('order.items', orderData.items.length);
34
35 const order = await processOrder(orderData);
36
37 span.setStatus({ code: SpanStatusCode.OK });
38 return order;
39 } catch (error) {
40 span.setStatus({
41 code: SpanStatusCode.ERROR,
42 message: error.message
43 });
44 throw error;
45 } finally {
46 span.end();
47 }
48}모니터링 전략
메트릭 수집
typescript
1// Prometheus 메트릭 수집
2import { Registry, Counter, Histogram } from 'prom-client';
3
4const register = new Registry();
5
6const httpRequestDuration = new Histogram({
7 name: 'http_request_duration_seconds',
8 help: 'Duration of HTTP requests in seconds',
9 labelNames: ['method', 'route', 'status'],
10 buckets: [0.1, 0.5, 1, 2, 5]
11});
12
13const httpRequestTotal = new Counter({
14 name: 'http_requests_total',
15 help: 'Total number of HTTP requests',
16 labelNames: ['method', 'route', 'status']
17});
18
19register.registerMetric(httpRequestDuration);
20register.registerMetric(httpRequestTotal);
21
22// 미들웨어에서 메트릭 수집
23app.use((req, res, next) => {
24 const start = Date.now();
25
26 res.on('finish', () => {
27 const duration = (Date.now() - start) / 1000;
28 httpRequestDuration
29 .labels(req.method, req.route?.path || req.path, res.statusCode)
30 .observe(duration);
31 httpRequestTotal
32 .labels(req.method, req.route?.path || req.path, res.statusCode)
33 .inc();
34 });
35
36 next();
37});로그 집계
typescript
1// Winston을 사용한 구조화된 로깅
2import winston from 'winston';
3
4const logger = winston.createLogger({
5 format: winston.format.combine(
6 winston.format.timestamp(),
7 winston.format.errors({ stack: true }),
8 winston.format.json()
9 ),
10 defaultMeta: {
11 service: 'order-service',
12 environment: process.env.NODE_ENV
13 },
14 transports: [
15 new winston.transports.File({ filename: 'error.log', level: 'error' }),
16 new winston.transports.File({ filename: 'combined.log' }),
17 new winston.transports.Console({
18 format: winston.format.simple()
19 })
20 ]
21});
22
23// 사용 예시
24logger.info('Order created', {
25 orderId: order.id,
26 userId: order.userId,
27 total: order.total,
28 traceId: span.context().traceId
29});장애 처리 패턴
Circuit Breaker 패턴
typescript
1import CircuitBreaker from 'opossum';
2
3const options = {
4 timeout: 3000,
5 errorThresholdPercentage: 50,
6 resetTimeout: 30000
7};
8
9const breaker = new CircuitBreaker(callUserService, options);
10
11breaker.on('open', () => {
12 console.log('Circuit breaker opened');
13});
14
15breaker.on('halfOpen', () => {
16 console.log('Circuit breaker half-open');
17});
18
19breaker.on('close', () => {
20 console.log('Circuit breaker closed');
21});
22
23async function callUserService(userId: string) {
24 const response = await fetch(`http://user-service/api/users/${userId}`);
25 if (!response.ok) {
26 throw new Error('User service error');
27 }
28 return response.json();
29}
30
31// 사용
32try {
33 const user = await breaker.fire(userId);
34} catch (error) {
35 // 폴백 처리
36 return getCachedUser(userId);
37}Retry 패턴
typescript
1import retry from 'async-retry';
2
3async function callExternalService(data: any) {
4 return await retry(
5 async (bail) => {
6 const response = await fetch('http://external-service/api', {
7 method: 'POST',
8 body: JSON.stringify(data)
9 });
10
11 if (response.status === 404) {
12 bail(new Error('Resource not found'));
13 return;
14 }
15
16 if (!response.ok) {
17 throw new Error(`HTTP ${response.status}`);
18 }
19
20 return response.json();
21 },
22 {
23 retries: 3,
24 minTimeout: 1000,
25 maxTimeout: 5000,
26 factor: 2
27 }
28 );
29}실전 배포 전략
Blue-Green 배포
yaml
1# Kubernetes Blue-Green 배포
2apiVersion: v1
3kind: Service
4metadata:
5 name: user-service
6spec:
7 selector:
8 version: blue
9 ports:
10 - port: 80
11---
12apiVersion: apps/v1
13kind: Deployment
14metadata:
15 name: user-service-blue
16spec:
17 replicas: 3
18 selector:
19 matchLabels:
20 app: user-service
21 version: blue
22 template:
23 metadata:
24 labels:
25 app: user-service
26 version: blue
27 spec:
28 containers:
29 - name: user-service
30 image: user-service:v1.0.0
31---
32apiVersion: apps/v1
33kind: Deployment
34metadata:
35 name: user-service-green
36spec:
37 replicas: 3
38 selector:
39 matchLabels:
40 app: user-service
41 version: green
42 template:
43 metadata:
44 labels:
45 app: user-service
46 version: green
47 spec:
48 containers:
49 - name: user-service
50 image: user-service:v1.1.0Canary 배포
yaml
1# Istio를 사용한 Canary 배포
2apiVersion: networking.istio.io/v1alpha3
3kind: VirtualService
4metadata:
5 name: user-service
6spec:
7 hosts:
8 - user-service
9 http:
10 - match:
11 - headers:
12 canary:
13 exact: "true"
14 route:
15 - destination:
16 host: user-service
17 subset: v2
18 weight: 100
19 - route:
20 - destination:
21 host: user-service
22 subset: v1
23 weight: 90
24 - destination:
25 host: user-service
26 subset: v2
27 weight: 10결론
마이크로서비스 아키텍처를 성공적으로 구현하기 위해서는:
- 명확한 서비스 경계: 각 서비스의 책임을 명확히 정의
- 적절한 통신 패턴: 동기/비동기 통신을 상황에 맞게 선택
- 강력한 모니터링: 분산 추적과 메트릭 수집으로 시스템 가시성 확보
- 장애 처리: Circuit Breaker, Retry 등으로 시스템 안정성 향상
- 점진적 배포: Blue-Green, Canary 배포로 위험 최소화
이러한 원칙과 패턴을 따르면 확장 가능하고 유지보수하기 쉬운 마이크로서비스 시스템을 구축할 수 있습니다.