Guider/Backend/BackendDevGuide0010
Backend#10

BackendDevGuide0010

마이크로서비스 아키텍처 완전 정복

 
 
BackendDevGuide Series · Episode 10
BackendDevGuide0010
마이크로서비스 아키텍처 완전 정복 A-Z
🏗️ 모놀리스에서 MSA로 — 현업 아키텍처의 모든 것
⏱ 예상 학습: 4~6주 🏗️ MSA 설계 패턴 📨 NestJS Microservices 🚪 API Gateway 완전정복
🏢
Netflix
700+ 마이크로서비스
독립 배포
서비스별 개별 배포 가능
📈
무한 확장
서비스별 독립 스케일링
🔧
장애 격리
한 서비스 장애가 전체로 안 퍼짐

📚 학습 목차 — 이 가이드에서 배울 내용

1 모놀리스 vs MSA — 언제 나눠야 하는가?
2 NestJS Microservices 완전 구현
3 API Gateway 패턴 완전 정복
4 메시지 브로커 — RabbitMQ & Kafka
5 서비스 디스커버리 & 로드밸런싱
6 분산 트랜잭션 — Saga 패턴
7 Circuit Breaker — 장애 전파 차단
8 분산 추적 & 중앙화 로깅
9 gRPC — 고성능 서비스 간 통신
10 MSA 보안 — 서비스 간 인증
11 MSA 실전 설계 — e커머스 주문 시스템
12 면접 Q&A + 체크리스트 + 로드맵

🏗️ 1장. 모놀리스 vs MSA — 언제 나눠야 하는가?

아키텍처 선택의 기준과 마이크로서비스의 본질 이해

🍕 피자 가게 비유로 이해하는 MSA

모놀리스 = 1인 피자 가게: 주인 혼자 주문받고, 반죽하고, 굽고, 배달까지 다 함. 작을 때는 효율적.
MSA = 피자 프랜차이즈 본사: 주문팀, 제조팀, 배달팀, 결제팀이 각자의 역할을 독립적으로 수행.
→ 배달팀에 문제 생겨도 주문과 제조는 계속 가능! → 팀별 독립 확장 가능!

핵심: MSA는 기술이 아닌 조직 구조를 반영한 아키텍처. Conway의 법칙: "소프트웨어 구조는 팀 커뮤니케이션 구조를 따른다"

⚖️ 모놀리스 vs MSA 완전 비교

항목 모놀리스 마이크로서비스
배포 ❌ 전체 재배포 ✅ 서비스별 독립 배포
확장 ⚠️ 전체 스케일 아웃 ✅ 병목 서비스만 확장
장애 격리 ❌ 한 모듈 오류 = 전체 장애 ✅ 장애 전파 최소화
기술 스택 ❌ 단일 언어/DB 강제 ✅ 서비스별 최적 기술 선택
초기 개발 속도 ✅ 빠름 (간단한 구조) ❌ 느림 (인프라 복잡)
운영 복잡도 ✅ 낮음 ❌ 높음 (분산 시스템)
팀 규모 소규모 (1~10명) 중대형 (10명 이상)

❌ MSA가 과도한 경우 (모놀리스 유지)

  • 팀 인원 10명 미만
  • 스타트업 MVP 단계
  • 도메인이 아직 불명확
  • 트래픽이 적고 확장 불필요
  • DevOps 전문인력 없음

✅ MSA가 적합한 경우

  • 팀이 성장해 코드 충돌 빈발
  • 특정 기능만 확장 필요 (결제, 검색)
  • 배포 주기가 팀마다 다름
  • 기술 이질성이 필요한 영역 존재
  • 서비스별 SLA(가용성) 요건 다름

💡 마이크로서비스 분리 기준 — 도메인 주도 설계(DDD)

Bounded Context(바운디드 컨텍스트): 동일한 도메인 용어가 같은 의미를 갖는 경계.
예: e커머스에서 "상품"이란 말은 → 카탈로그팀(설명, 이미지), 재고팀(수량), 배송팀(무게, 크기)에서 모두 다름!
→ 각 컨텍스트를 별도 마이크로서비스로 분리하면 자연스러운 경계가 생김.
실전 팁: "이 서비스를 독립적으로 배포할 수 있는가?"를 기준으로 판단.

📨 2장. NestJS Microservices 완전 구현

TCP 트랜스포트부터 이벤트 기반 통신까지

⚙️ 설치 및 마이크로서비스 아키텍처 구성

# 프로젝트 구조
apps/
├── api-gateway/          # API 진입점 (3000포트)
├── user-service/         # 사용자 관리 (3001포트)
├── order-service/        # 주문 처리 (3002포트)
├── product-service/      # 상품 관리 (3003포트)
├── payment-service/      # 결제 처리 (3004포트)
└── notification-service/ # 알림 발송 (3005포트)

# NestJS Monorepo 설정
nest new msa-project --strict
cd msa-project
nest generate app user-service
nest generate app order-service

# 마이크로서비스 패키지 설치
npm install @nestjs/microservices
npm install @nestjs/platform-express
npm install amqplib amqp-connection-manager  # RabbitMQ
npm install kafkajs                           # Kafka

🔌 TCP 트랜스포트 — 동기 요청/응답

// user-service/main.ts — 마이크로서비스로 부트스트랩
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    UserServiceModule,
    {
      transport: Transport.TCP,
      options: { host: '0.0.0.0', port: 3001 },
    }
  );
  await app.listen();
  console.log('✅ User Service is running on port 3001');
}
bootstrap();

// user-service/users.controller.ts — 메시지 핸들러
import { Controller } from '@nestjs/common';
import { MessagePattern, Payload, EventPattern } from '@nestjs/microservices';

@Controller()
export class UsersController {
  constructor(private usersService: UsersService) {}

  // 동기 메시지 패턴 (요청-응답)
  @MessagePattern('get_user')
  async getUser(@Payload() data: { userId: number }) {
    return this.usersService.findOne(data.userId);
  }

  @MessagePattern('create_user')
  async createUser(@Payload() dto: CreateUserDto) {
    return this.usersService.create(dto);
  }

  @MessagePattern('validate_user')
  async validateUser(@Payload() data: { email: string; password: string }) {
    return this.usersService.validate(data.email, data.password);
  }

  // 비동기 이벤트 패턴 (fire-and-forget)
  @EventPattern('user_created')
  handleUserCreated(@Payload() event: UserCreatedEvent) {
    this.usersService.sendWelcomeEmail(event.email);
    console.log(`📨 Welcome email queued for ${event.email}`);
  }
}

// api-gateway/users.module.ts — 클라이언트 등록
import { ClientsModule, Transport } from '@nestjs/microservices';

@Module({
  imports: [
    ClientsModule.registerAsync([
      {
        name: 'USER_SERVICE',
        useFactory: () => ({
          transport: Transport.TCP,
          options: {
            host: process.env.USER_SERVICE_HOST || 'localhost',
            port: +process.env.USER_SERVICE_PORT || 3001,
          },
        }),
      },
    ]),
  ],
  controllers: [UsersController],
})
export class UsersModule {}

// api-gateway/users.controller.ts — 마이크로서비스 호출
@Controller('users')
export class UsersController {
  constructor(
    @Inject('USER_SERVICE') private userClient: ClientProxy,
  ) {}

  @Get(':id')
  async getUser(@Param('id') id: string) {
    // send: 요청-응답 (Observable 반환)
    return this.userClient.send('get_user', { userId: +id }).pipe(
      timeout(5000),  // 5초 타임아웃
      catchError(err => { throw new RpcException(err); }),
    );
  }

  @Post()
  async createUser(@Body() dto: CreateUserDto) {
    const user = await firstValueFrom(
      this.userClient.send('create_user', dto)
    );
    // 이벤트 발행 (비동기, 응답 불필요)
    this.userClient.emit('user_created', { email: user.email, userId: user.id });
    return user;
  }
}

📡 NestJS 지원 트랜스포트 비교

트랜스포트 방식 적합 용도 특징
TCP 요청-응답 내부 서비스 간 가장 간단, 낮은 지연
RabbitMQ 메시지 큐 비동기 이벤트 영속성, 재시도, 라우팅
Kafka 스트리밍 대용량 이벤트 고처리량, 순서 보장, 재생
Redis Pub/Sub 실시간 알림 초저지연, 간단 구성
gRPC RPC 고성능 서비스 간 바이너리, 타입 안전, 스트리밍

🚪 3장. API Gateway 패턴 완전 정복

단일 진입점 설계 + 인증/라우팅/집계/Rate Limiting

🏢 API Gateway란? — 건물 로비 비유

대형 건물(마이크로서비스 시스템)에 들어오는 모든 방문객(클라이언트)은 반드시 로비(API Gateway)를 통해야 합니다.
로비에서: 신분증 확인(인증) → 어느 부서로 갈지 안내(라우팅) → 방문 기록(로깅) → 입장 제한(Rate Limiting)
→ 클라이언트는 내부에 서비스가 몇 개인지 알 필요 없이 Gateway 하나만 알면 됨!

💻 NestJS API Gateway 완전 구현

// api-gateway/app.module.ts — 여러 서비스 클라이언트 등록
@Module({
  imports: [
    ClientsModule.registerAsync([
      {
        name: 'USER_SERVICE',
        useFactory: () => ({
          transport: Transport.TCP,
          options: { host: process.env.USER_SERVICE_HOST, port: 3001 },
        }),
      },
      {
        name: 'ORDER_SERVICE',
        useFactory: () => ({
          transport: Transport.RMQ,
          options: {
            urls: [process.env.RABBITMQ_URL],
            queue: 'orders_queue',
            queueOptions: { durable: true },
          },
        }),
      },
      {
        name: 'PRODUCT_SERVICE',
        useFactory: () => ({
          transport: Transport.TCP,
          options: { host: process.env.PRODUCT_SERVICE_HOST, port: 3003 },
        }),
      },
    ]),
    ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }]),  // 1분 100요청
  ],
})
export class AppModule {}

// api-gateway/orders.controller.ts — 데이터 집계 (Aggregation)
@Controller('orders')
@UseGuards(JwtAuthGuard, ThrottlerGuard)
export class OrdersController {
  constructor(
    @Inject('ORDER_SERVICE') private orderClient: ClientProxy,
    @Inject('USER_SERVICE') private userClient: ClientProxy,
    @Inject('PRODUCT_SERVICE') private productClient: ClientProxy,
  ) {}

  // 여러 서비스 데이터 집계 — 주문 상세 조회
  @Get(':orderId')
  async getOrderDetail(@Param('orderId') orderId: string) {
    // 주문 정보 먼저 조회
    const order = await firstValueFrom(
      this.orderClient.send('get_order', { orderId })
    );

    // 사용자 + 상품 정보 병렬 조회 (성능 최적화)
    const [user, products] = await Promise.all([
      firstValueFrom(this.userClient.send('get_user', { userId: order.userId })),
      firstValueFrom(this.productClient.send('get_products', { ids: order.productIds })),
    ]);

    // 집계된 응답 반환
    return {
      ...order,
      user: { id: user.id, name: user.name, email: user.email },
      products,
    };
  }

  // 주문 생성 — 이벤트 체인
  @Post()
  async createOrder(@Body() dto: CreateOrderDto, @CurrentUser() user: User) {
    // 재고 확인 (동기)
    const stockCheck = await firstValueFrom(
      this.productClient.send('check_stock', { items: dto.items })
    );
    if (!stockCheck.available) throw new BadRequestException('재고 부족');

    // 주문 생성 (동기)
    const order = await firstValueFrom(
      this.orderClient.send('create_order', { ...dto, userId: user.id })
    );

    // 결제 + 재고 차감 + 알림 이벤트 발행 (비동기)
    this.orderClient.emit('order_created', { orderId: order.id, ...dto });

    return { message: '주문이 접수되었습니다.', orderId: order.id };
  }
}

// ✅ 글로벌 예외 필터 — RPC 예외를 HTTP로 변환
@Catch(RpcException)
export class RpcExceptionFilter implements ExceptionFilter {
  catch(exception: RpcException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse();
    const error = exception.getError() as any;
    res.status(error.statusCode || 500).json({
      statusCode: error.statusCode || 500,
      message: error.message || '서비스 오류',
    });
  }
}

📨 4장. 메시지 브로커 — RabbitMQ & Kafka 완전 정복

비동기 이벤트 기반 아키텍처의 핵심 인프라

📮 메시지 브로커란? — 우체국 비유

메시지 브로커 = 우체국: 발신자(서비스 A)가 편지(메시지)를 우체국(브로커)에 맡기면 → 우체국이 수신자(서비스 B)에게 배달.
서비스 A는 서비스 B가 현재 살아있는지 신경 쓸 필요 없음 → 느슨한 결합(Loose Coupling)!
서비스 B가 잠깐 다운되어도 메시지는 큐에 보관되다가 복구 후 처리 → 신뢰성 보장!

🐰 RabbitMQ 완전 구현

# Docker로 RabbitMQ 실행
docker run -d --name rabbitmq \
  -p 5672:5672 \   # AMQP 포트
  -p 15672:15672 \ # 관리 UI
  -e RABBITMQ_DEFAULT_USER=admin \
  -e RABBITMQ_DEFAULT_PASS=password \
  rabbitmq:3-management

# 관리 UI: http://localhost:15672

// order-service/main.ts — RabbitMQ 마이크로서비스
async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    OrderServiceModule,
    {
      transport: Transport.RMQ,
      options: {
        urls: [process.env.RABBITMQ_URL || 'amqp://admin:password@localhost:5672'],
        queue: 'orders_queue',
        prefetchCount: 10,       # 한번에 10개 메시지 처리
        noAck: false,             # 수동 ACK (처리 후 확인)
        queueOptions: { durable: true }, # 서버 재시작 후에도 큐 유지
      },
    }
  );
  await app.listen();
}

// order-service/orders.controller.ts
@Controller()
export class OrdersController {
  @EventPattern('order_created')
  async handleOrderCreated(
    @Payload() event: OrderCreatedEvent,
    @Ctx() ctx: RmqContext,
  ) {
    const channel = ctx.getChannelRef();
    const originalMsg = ctx.getMessage();
    
    try {
      await this.ordersService.processOrder(event);
      // ✅ 성공 시 ACK — 큐에서 메시지 제거
      channel.ack(originalMsg);
    } catch (error) {
      // ❌ 실패 시 NACK — 재큐잉 (재처리)
      channel.nack(originalMsg, false, true);
    }
  }

  @MessagePattern('get_order')
  async getOrder(@Payload() data: { orderId: string }) {
    return this.ordersService.findOne(data.orderId);
  }
}

// ✅ Dead Letter Queue — 실패 메시지 처리
const channel = connection.createChannel();
await channel.assertQueue('orders_queue', {
  durable: true,
  arguments: {
    'x-dead-letter-exchange': 'dlx',
    'x-dead-letter-routing-key': 'dead_orders',
    'x-message-ttl': 86400000,  // 24시간 후 DLQ로
  },
});

⚡ Kafka 완전 구현 — 대용량 이벤트 스트리밍

# docker-compose.yml — Kafka + Zookeeper
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:latest
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
  kafka:
    image: confluentinc/cp-kafka:latest
    depends_on: [zookeeper]
    ports: ["9092:9092"]
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1

// event-service/main.ts — Kafka 마이크로서비스
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
  EventServiceModule,
  {
    transport: Transport.KAFKA,
    options: {
      client: {
        clientId: 'event-service',
        brokers: [process.env.KAFKA_BROKER || 'localhost:9092'],
      },
      consumer: {
        groupId: 'event-consumer-group',  # 같은 그룹 = 부하 분산
      },
    },
  }
);

// Kafka 이벤트 소비자
@EventPattern('user.order.created')
async handleOrderCreated(@Payload() message: KafkaMessage) {
  const { key, value, partition, offset } = message;
  console.log(`Partition: ${partition}, Offset: ${offset}`);
  await this.analyticsService.recordEvent(value);
}

// Kafka 이벤트 발행자 (Producer)
await this.kafkaClient.emit('user.order.created', {
  key: order.userId.toString(),    # 같은 userId → 같은 파티션 (순서 보장)
  value: JSON.stringify(order),
});

🐰 RabbitMQ vs ⚡ Kafka 선택 가이드

항목 RabbitMQ Kafka
처리량 수만/초 수백만/초
메시지 보존 소비 후 삭제 설정 기간 보존 (재생 가능)
순서 보장 큐 내 순서 파티션 내 순서 보장
라우팅 Exchange로 복잡한 라우팅 토픽 기반 단순 라우팅
적합 사례 작업 큐, RPC, 알림 로그, 이벤트 소싱, 분석

🔄 5장. 분산 트랜잭션 — Saga 패턴 완전 정복

마이크로서비스에서 데이터 일관성을 보장하는 방법

⚠️ 분산 트랜잭션의 문제 — 왜 어려운가?

단일 DB에서는 트랜잭션(ACID)이 자동으로 일관성을 보장합니다.
하지만 MSA에서는 주문 서비스 DB, 재고 서비스 DB, 결제 서비스 DB가 각각 분리되어 있습니다!
→ 주문 생성 ✅, 재고 차감 ✅, 결제 ❌ → 롤백을 어떻게 하나? → Saga 패턴으로 해결!

📐 Saga 패턴 — Choreography vs Orchestration

🎼 Choreography (코레오그래피)

각 서비스가 이벤트를 듣고 직접 반응. 중앙 조정자 없음.

OrderService → event: order_created
InventoryService → 재고 차감 → event: stock_reserved
PaymentService → 결제 → event: payment_processed
OrderService → 주문 완료
✅ 단순, 결합도 낮음
⚠️ 흐름 파악 어려움

🎻 Orchestration (오케스트레이션)

중앙 Saga Orchestrator가 각 서비스에 명령을 보냄.

OrderSaga → reserve_stock (InventoryService)
InventoryService → stock_reserved
OrderSaga → process_payment (PaymentService)
PaymentService → payment_done
OrderSaga → confirm_order (OrderService)
✅ 흐름 명확, 디버깅 쉬움
⚠️ 중앙 집중 위험
// order-saga.service.ts — Orchestration Saga 구현
@Injectable()
export class OrderSaga {
  constructor(
    @Inject('INVENTORY_SERVICE') private inventoryClient: ClientProxy,
    @Inject('PAYMENT_SERVICE') private paymentClient: ClientProxy,
    @Inject('ORDER_SERVICE') private orderClient: ClientProxy,
  ) {}

  async execute(createOrderDto: CreateOrderDto): Promise<Order> {
    let orderId: string;
    let stockReserved = false;

    try {
      // Step 1: 주문 생성 (PENDING 상태)
      const order = await firstValueFrom(
        this.orderClient.send('create_order_pending', createOrderDto)
      );
      orderId = order.id;

      // Step 2: 재고 예약
      await firstValueFrom(
        this.inventoryClient.send('reserve_stock', { orderId, items: createOrderDto.items })
      );
      stockReserved = true;

      // Step 3: 결제 처리
      await firstValueFrom(
        this.paymentClient.send('process_payment', {
          orderId, amount: createOrderDto.totalAmount, userId: createOrderDto.userId,
        })
      );

      // Step 4: 주문 확정 (CONFIRMED)
      return firstValueFrom(
        this.orderClient.send('confirm_order', { orderId })
      );

    } catch (error) {
      // ❌ 보상 트랜잭션 (Compensating Transaction)
      console.error(`Saga failed at step, rolling back... ${error.message}`);

      if (stockReserved) {
        // 재고 복원
        await firstValueFrom(
          this.inventoryClient.send('release_stock', { orderId })
        ).catch(e => console.error('Stock rollback failed', e));
      }
      if (orderId) {
        // 주문 취소
        await firstValueFrom(
          this.orderClient.send('cancel_order', { orderId, reason: error.message })
        ).catch(e => console.error('Order cancel failed', e));
      }
      throw error;
    }
  }
}

⚡ 6장. Circuit Breaker + gRPC + 분산 추적

장애 전파 차단 + 고성능 통신 + 전체 요청 흐름 추적

⚡ Circuit Breaker란? — 전기 차단기 비유

집에서 전기가 과부하되면 차단기(Circuit Breaker)가 자동으로 전기를 차단합니다. → 화재 방지!
MSA에서 결제 서비스가 느려지면 → API Gateway가 계속 결제 서비스를 호출 → 전체 시스템 마비!
Circuit Breaker: 일정 횟수 이상 실패하면 자동으로 해당 서비스 호출을 차단하고 fallback 응답을 반환.

🔌 Circuit Breaker 3가지 상태

CLOSED (정상)
요청을 정상 전달
실패율 모니터링 중
🔴
OPEN (차단)
즉시 Fallback 반환
서비스 회복 시간 확보
⚠️
HALF-OPEN (탐색)
제한적 요청 허용
회복 여부 테스트
# Circuit Breaker 설치
npm install opossum  # Node.js Circuit Breaker 라이브러리
npm install @nestjs/axios axios  # HTTP 클라이언트

// circuit-breaker.service.ts
import CircuitBreaker from 'opossum';

@Injectable()
export class PaymentCircuitBreakerService {
  private breaker: CircuitBreaker;

  constructor(private httpService: HttpService) {
    const options = {
      timeout: 3000,          // 3초 이내 응답 없으면 실패
      errorThresholdPercentage: 50, // 50% 실패율에서 OPEN
      resetTimeout: 30000,    // 30초 후 HALF-OPEN 시도
      volumeThreshold: 5,    // 최소 5개 요청 후 판단
    };

    this.breaker = new CircuitBreaker(
      (data: ProcessPaymentDto) => this.callPaymentService(data),
      options
    );

    // 이벤트 핸들러
    this.breaker.on('open', () => console.error('🔴 Payment Circuit OPEN!'));
    this.breaker.on('halfOpen', () => console.warn('⚠️ Payment Circuit HALF-OPEN'));
    this.breaker.on('close', () => console.log('✅ Payment Circuit CLOSED'));
    this.breaker.fallback(() => ({ status: 'queued', message: '결제 서비스 복구 중. 나중에 재시도됩니다.' }));
  }

  async processPayment(data: ProcessPaymentDto) {
    return this.breaker.fire(data);  // Circuit Breaker 통해 호출
  }

  private async callPaymentService(data: ProcessPaymentDto) {
    const { data: result } = await firstValueFrom(
      this.httpService.post(`${process.env.PAYMENT_SERVICE_URL}/payments`, data)
    );
    return result;
  }
}

// ✅ gRPC — 고성능 서비스 간 통신
# 설치
npm install @grpc/grpc-js @grpc/proto-loader

# user.proto — Protocol Buffers 정의
syntax = "proto3";
package user;

service UserService {
  rpc GetUser (GetUserRequest) returns (UserResponse);
  rpc CreateUser (CreateUserRequest) returns (UserResponse);
  rpc StreamUsers (StreamRequest) returns (stream UserResponse);  # 스트리밍!
}

message GetUserRequest { int32 id = 1; }
message UserResponse {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

// gRPC 마이크로서비스 설정
const app = await NestFactory.createMicroservice({
  transport: Transport.GRPC,
  options: {
    package: 'user',
    protoPath: join(__dirname, '../user.proto'),
    url: '0.0.0.0:5001',
  },
});

🔍 분산 추적 — Jaeger + OpenTelemetry

# OpenTelemetry 설치
npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node
npm install @opentelemetry/exporter-jaeger

// tracer.ts — OpenTelemetry 초기화 (main.ts보다 먼저 임포트)
import { NodeSDK } from '@opentelemetry/sdk-node';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';

const sdk = new NodeSDK({
  serviceName: 'order-service',
  traceExporter: new JaegerExporter({
    endpoint: 'http://jaeger:14268/api/traces',
  }),
  instrumentations: [getNodeAutoInstrumentations()],  // 자동 계측!
});
sdk.start();
// 이후 HTTP, DB 쿼리, 외부 API 호출이 자동으로 추적됨!

# Jaeger UI 접속: http://localhost:16686
# → 요청이 어느 서비스를 거쳤는지, 각 단계 소요 시간 시각화!

🔐 7장. MSA 보안 + 서비스 메시 (Service Mesh)

서비스 간 인증, mTLS, Istio 개요

🛡️ MSA 보안의 두 가지 문제

문제 1 — 클라이언트 인증: 사용자가 API Gateway에서 JWT로 인증. Gateway는 내부 서비스 호출 시 사용자 정보를 전달해야 함.
문제 2 — 서비스 간 인증: Order Service가 Payment Service를 호출할 때, 진짜 Order Service인지 확인해야 함. (악의적인 서비스가 Payment API를 직접 호출할 수 있음)
해결: JWT 전파 + mTLS(상호 TLS) + Service Account

// ✅ JWT 전파 패턴 — Gateway에서 내부 서비스로 사용자 정보 전달

// api-gateway — 요청 헤더에 사용자 정보 추가
@UseGuards(JwtAuthGuard)
@Get('orders')
async getOrders(@CurrentUser() user: User) {
  // 내부 서비스에 JWT 그대로 전달하거나 사용자 정보를 서명해서 전달
  const internalToken = this.jwtService.sign(
    { userId: user.id, email: user.email, role: user.role },
    { secret: process.env.INTERNAL_JWT_SECRET, expiresIn: '30s' }  // 짧은 만료
  );
  
  return firstValueFrom(
    this.orderClient.send('get_user_orders', {
      userId: user.id,
      _internalToken: internalToken,  // 내부 서비스용 토큰 전달
    })
  );
}

// order-service — 내부 토큰 검증
@MessagePattern('get_user_orders')
async getUserOrders(@Payload() data: { userId: number; _internalToken: string }) {
  // 내부 JWT 검증 (외부 요청 차단)
  try {
    this.jwtService.verify(data._internalToken, { secret: process.env.INTERNAL_JWT_SECRET });
  } catch {
    throw new RpcException('Unauthorized internal request');
  }
  return this.ordersService.findByUser(data.userId);
}

// ✅ 서비스 간 API Key 인증 (간단한 방법)
@MessagePattern('payment_webhook')
async handleWebhook(@Payload() data: any, @Headers() headers: any) {
  const apiKey = headers['x-service-api-key'];
  if (apiKey !== process.env.PAYMENT_SERVICE_API_KEY) {
    throw new RpcException('Unauthorized');
  }
  return this.paymentService.processWebhook(data);
}

// ✅ 환경변수 — .env (각 서비스별)
INTERNAL_JWT_SECRET=super-secret-internal-key-min-32-chars
PAYMENT_SERVICE_API_KEY=payment-internal-api-key-xyz
ORDER_SERVICE_API_KEY=order-internal-api-key-abc

🕸️ Service Mesh (Istio) — 인프라 레벨 보안

Service Mesh 없을 때
  • 각 서비스가 직접 TLS 설정
  • 재시도/타임아웃 코드에 직접 구현
  • 트래픽 제어 코드가 비즈니스에 혼재
  • 서비스 간 인증 각자 구현
Istio Service Mesh 사용 시
  • mTLS 자동 설정 (코드 없음)
  • 재시도/타임아웃 YAML로 선언
  • 트래픽 관리 인프라에서 처리
  • 서비스 간 인증 인프라 레벨에서 자동

📊 MSA 보안 레이어별 구현 전략

레이어 방법 도구
외부 → Gateway JWT 인증 + HTTPS JWT, Let's Encrypt
Gateway → 내부 서비스 내부 JWT + API Key 짧은 TTL JWT, 환경변수
서비스 ↔ 서비스 mTLS 상호 인증 Istio, cert-manager
시크릿 관리 중앙 시크릿 저장소 HashiCorp Vault, K8s Secrets

🛒 8장. MSA 실전 설계 — e커머스 주문 시스템

도메인 분리부터 docker-compose 구성까지

🏗️ 실전 MSA 아키텍처 설계

e커머스 시스템을 마이크로서비스로 분리하는 실전 예제입니다.
6개 핵심 서비스 + API Gateway + 메시지 브로커 + 분산 추적으로 구성합니다.

# docker-compose.yml — 전체 MSA 시스템
version: '3.8'
services:

  # ========== 인프라 ==========
  rabbitmq:
    image: rabbitmq:3-management
    ports: ["5672:5672", "15672:15672"]
    environment:
      RABBITMQ_DEFAULT_USER: admin
      RABBITMQ_DEFAULT_PASS: password

  redis:
    image: redis:7-alpine
    ports: ["6379:6379"]
    command: redis-server --requirepass password

  jaeger:
    image: jaegertracing/all-in-one:latest
    ports: ["16686:16686", "14268:14268"]

  # ========== 데이터베이스 (서비스별 독립 DB!) ==========
  user-db:
    image: postgres:15
    environment:
      POSTGRES_DB: users
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password

  order-db:
    image: postgres:15
    environment:
      POSTGRES_DB: orders
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password

  product-db:
    image: postgres:15
    environment:
      POSTGRES_DB: products
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password

  # ========== 마이크로서비스 ==========
  api-gateway:
    build: ./apps/api-gateway
    ports: ["3000:3000"]
    environment:
      USER_SERVICE_HOST: user-service
      ORDER_SERVICE_HOST: order-service
      PRODUCT_SERVICE_HOST: product-service
      RABBITMQ_URL: amqp://admin:password@rabbitmq:5672
      REDIS_URL: redis://:password@redis:6379
      JWT_SECRET: your-jwt-secret
    depends_on: [user-service, order-service, product-service, rabbitmq, redis]

  user-service:
    build: ./apps/user-service
    environment:
      DATABASE_URL: postgresql://user:password@user-db:5432/users
      INTERNAL_JWT_SECRET: internal-secret
    depends_on: [user-db]

  order-service:
    build: ./apps/order-service
    environment:
      DATABASE_URL: postgresql://user:password@order-db:5432/orders
      RABBITMQ_URL: amqp://admin:password@rabbitmq:5672
      JAEGER_URL: http://jaeger:14268/api/traces
    depends_on: [order-db, rabbitmq]

  product-service:
    build: ./apps/product-service
    environment:
      DATABASE_URL: postgresql://user:password@product-db:5432/products
      REDIS_URL: redis://:password@redis:6379  # 상품 목록 캐싱
    depends_on: [product-db, redis]

  payment-service:
    build: ./apps/payment-service
    environment:
      RABBITMQ_URL: amqp://admin:password@rabbitmq:5672
      STRIPE_SECRET_KEY: sk_test_...
    depends_on: [rabbitmq]
    # 결제 서비스는 외부 노출 없음 (Gateway 통해서만)

  notification-service:
    build: ./apps/notification-service
    environment:
      RABBITMQ_URL: amqp://admin:password@rabbitmq:5672
      EMAIL_HOST: smtp.gmail.com
    depends_on: [rabbitmq]

📊 주문 처리 전체 흐름

① 클라이언트
POST /orders
② API Gateway
JWT 검증
③ Order Saga
재고 확인
④ Payment
결제 처리
⑤ RabbitMQ
이벤트 발행
⑥ Notification
이메일 발송
동기 체인: ① → ② → ③ (재고 확인) → ④ (결제) → 주문 확정 응답
비동기 이벤트: order_confirmed 이벤트 → ⑤ RabbitMQ → ⑥ 이메일 발송 (백그라운드)
장애 시: ④ 결제 실패 → Saga 보상 트랜잭션 → ③ 재고 복원 → ③ 주문 취소

🎯 9장. 면접 Q&A + 체크리스트 + 로드맵

현업에서 실제로 묻는 MSA 심층 질문들

💬 면접 질문 & 모범 답변

Q1. 마이크로서비스와 모놀리스 중 어떤 것을 선택하겠습니까?

프로젝트 규모와 팀 상황에 따라 다릅니다. 초기 스타트업이나 소규모 팀에서는 모놀리스가 적합합니다. 복잡성이 낮고 빠르게 개발할 수 있기 때문입니다.

MSA로 전환을 고려할 시점은: ① 팀이 10명 이상으로 코드 충돌이 빈번해질 때, ② 특정 기능만 독립적으로 확장해야 할 때, ③ 서비스별로 배포 주기가 달라질 때입니다.

Strangler Fig Pattern: 기존 모놀리스를 한 번에 바꾸지 않고, 새 기능은 마이크로서비스로 추가하고 점진적으로 이전하는 방식을 추천합니다.

Q2. MSA에서 데이터 일관성을 어떻게 보장하나요?

MSA에서는 분산 트랜잭션의 어려움 때문에 최종 일관성(Eventual Consistency)을 수용하는 설계를 합니다.

구체적 방법: Saga 패턴으로 여러 서비스에 걸친 비즈니스 트랜잭션을 처리합니다. 각 단계 실패 시 보상 트랜잭션(Compensating Transaction)으로 이전 상태를 복원합니다.
Outbox Pattern: DB 저장과 이벤트 발행을 같은 트랜잭션으로 처리하여 메시지 유실을 방지합니다. DB에 outbox 테이블에 먼저 저장 후 메시지 브로커로 발행합니다.

Q3. API Gateway의 역할과 주의점은 무엇인가요?

API Gateway 역할: 단일 진입점, 인증/인가, 라우팅, 부하분산, Rate Limiting, 로깅, CORS 처리.

주의점: Gateway가 너무 많은 비즈니스 로직을 가지면 "Smart Gateway, Dumb Services" 반패턴. Gateway는 라우팅/인증 등 크로스 커팅 관심사만 처리하고, 비즈니스 로직은 각 서비스에 있어야 합니다.
SPOF(단일 장애점) 방지: Gateway를 여러 인스턴스로 운영하고 로드밸런서 앞에 배치.

Q4. RabbitMQ와 Kafka 중 어떤 것을 선택하겠습니까?

RabbitMQ 선택: 복잡한 라우팅 규칙이 필요하거나, 작업 큐(Task Queue) 패턴, 이메일/알림 같은 일반적인 비동기 작업에 적합. 운영이 상대적으로 간단합니다.

Kafka 선택: 초당 수백만 메시지, 메시지 이력 보존(재생)이 필요할 때, 이벤트 소싱 패턴, 실시간 로그 분석. 운영 복잡도가 높지만 처리량이 압도적입니다.
현업에서는 일반적인 비동기 처리는 RabbitMQ, 이벤트 스트리밍/분석 파이프라인은 Kafka를 사용합니다.

Q5. Circuit Breaker는 왜 필요한가요?

MSA에서 서비스 A가 서비스 B를 호출하는데, B가 느려지면 A의 스레드(또는 커넥션)가 쌓이고 결국 A도 느려지고, A를 호출하는 C도 느려지는 Cascade Failure(연쇄 장애)가 발생합니다.

Circuit Breaker는 B의 실패율이 임계치(예: 50%)를 넘으면 즉시 B로의 요청을 차단하고 Fallback 응답을 반환합니다. B가 회복될 시간을 주고, A의 리소스 소진을 막습니다.
구현 라이브러리: opossum(Node.js), resilience4j(Java), Polly(.NET)

✅ MSA 레벨별 체크리스트

🌱 초급
  • 모놀리스 vs MSA 차이 설명
  • NestJS TCP 마이크로서비스
  • MessagePattern/EventPattern
  • API Gateway 기본 라우팅
  • docker-compose 구성
🚀 중급
  • RabbitMQ 연동 및 ACK 처리
  • Saga 패턴 구현
  • Circuit Breaker 적용
  • JWT 내부 서비스 전파
  • 분산 추적(Jaeger) 설정
🏆 고급
  • Kafka 이벤트 스트리밍
  • Outbox Pattern 구현
  • gRPC 서비스 구현
  • Istio Service Mesh
  • MSA Kubernetes 배포

🗺️ MSA 학습 로드맵 (6주)

📅 1~2주차
MSA 개념 + DDD 기초
NestJS TCP 마이크로서비스
API Gateway 구현
Docker Compose 구성
📅 3~4주차
RabbitMQ 메시지 큐
Saga 패턴 구현
Circuit Breaker 적용
분산 추적(Jaeger) 설정
📅 5~6주차
Kafka 스트리밍
gRPC 서비스 간 통신
MSA 보안 강화
e커머스 실전 프로젝트
🏗️
BackendDevGuide0010 완료!
모놀리스부터 Kafka까지 — 이제 엔터프라이즈급 마이크로서비스를 설계할 수 있습니다!
다음: BackendDevGuide0011 - GraphQL과 REST API 고급 설계 완전 정복 A-Z
🏗️ MSA 아키텍처 📨 RabbitMQ/Kafka 🔄 Saga 패턴 ⚡ Circuit Breaker
반응형