Backend

마이크로서비스 아키텍처 설계와 모니터링 전략

대규모 애플리케이션을 위한 마이크로서비스 아키텍처 설계 원칙, 통신 패턴, 분산 추적, 모니터링 전략을 실전 사례와 함께 상세히 설명합니다.

마이크로서비스 아키텍처 설계와 모니터링 전략

소개

마이크로서비스는 “서비스 하나만 죽어도 전체가 멈춘다”는 모놀리식 문제를 줄여 주지만, 설계·배포·모니터링이 잘못되면 복잡도만 올라갑니다. 리워드·이벤트·분석처럼 도메인을 나눠 서비스를 구성해 본 경험을 바탕으로, 설계와 운영에 필요한 핵심 개념과 실전 전략을 정리했습니다.

마이크로서비스 아키텍처의 핵심 원칙

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-service

3. 데이터베이스 분리

각 서비스는 자신만의 데이터베이스를 가져야 합니다.

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: 3000

API 게이트웨이 패턴

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.0

Canary 배포

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

결론

마이크로서비스 아키텍처를 성공적으로 구현하기 위해서는:

  1. 명확한 서비스 경계: 각 서비스의 책임을 명확히 정의
  2. 적절한 통신 패턴: 동기/비동기 통신을 상황에 맞게 선택
  3. 강력한 모니터링: 분산 추적과 메트릭 수집으로 시스템 가시성 확보
  4. 장애 처리: Circuit Breaker, Retry 등으로 시스템 안정성 향상
  5. 점진적 배포: Blue-Green, Canary 배포로 위험 최소화

이러한 원칙과 패턴을 따르면 확장 가능하고 유지보수하기 쉬운 마이크로서비스 시스템을 구축할 수 있습니다.

참고 자료

공유하기

관련 포스트