📚 학습 목차 — 이 가이드에서 배울 내용
🏗️ 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 (코레오그래피)
각 서비스가 이벤트를 듣고 직접 반응. 중앙 조정자 없음.
InventoryService → 재고 차감 → event: stock_reserved
PaymentService → 결제 → event: payment_processed
OrderService → 주문 완료
🎻 Orchestration (오케스트레이션)
중앙 Saga Orchestrator가 각 서비스에 명령을 보냄.
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가지 상태
실패율 모니터링 중
서비스 회복 시간 확보
회복 여부 테스트
# 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) — 인프라 레벨 보안
- 각 서비스가 직접 TLS 설정
- 재시도/타임아웃 코드에 직접 구현
- 트래픽 제어 코드가 비즈니스에 혼재
- 서비스 간 인증 각자 구현
- 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
JWT 검증
재고 확인
결제 처리
이벤트 발행
이메일 발송
비동기 이벤트: order_confirmed 이벤트 → ⑤ RabbitMQ → ⑥ 이메일 발송 (백그라운드)
장애 시: ④ 결제 실패 → Saga 보상 트랜잭션 → ③ 재고 복원 → ③ 주문 취소
🎯 9장. 면접 Q&A + 체크리스트 + 로드맵
현업에서 실제로 묻는 MSA 심층 질문들
💬 면접 질문 & 모범 답변
프로젝트 규모와 팀 상황에 따라 다릅니다. 초기 스타트업이나 소규모 팀에서는 모놀리스가 적합합니다. 복잡성이 낮고 빠르게 개발할 수 있기 때문입니다.
MSA로 전환을 고려할 시점은: ① 팀이 10명 이상으로 코드 충돌이 빈번해질 때, ② 특정 기능만 독립적으로 확장해야 할 때, ③ 서비스별로 배포 주기가 달라질 때입니다.
Strangler Fig Pattern: 기존 모놀리스를 한 번에 바꾸지 않고, 새 기능은 마이크로서비스로 추가하고 점진적으로 이전하는 방식을 추천합니다.
MSA에서는 분산 트랜잭션의 어려움 때문에 최종 일관성(Eventual Consistency)을 수용하는 설계를 합니다.
구체적 방법: Saga 패턴으로 여러 서비스에 걸친 비즈니스 트랜잭션을 처리합니다. 각 단계 실패 시 보상 트랜잭션(Compensating Transaction)으로 이전 상태를 복원합니다.
Outbox Pattern: DB 저장과 이벤트 발행을 같은 트랜잭션으로 처리하여 메시지 유실을 방지합니다. DB에 outbox 테이블에 먼저 저장 후 메시지 브로커로 발행합니다.
API Gateway 역할: 단일 진입점, 인증/인가, 라우팅, 부하분산, Rate Limiting, 로깅, CORS 처리.
주의점: Gateway가 너무 많은 비즈니스 로직을 가지면 "Smart Gateway, Dumb Services" 반패턴. Gateway는 라우팅/인증 등 크로스 커팅 관심사만 처리하고, 비즈니스 로직은 각 서비스에 있어야 합니다.
SPOF(단일 장애점) 방지: Gateway를 여러 인스턴스로 운영하고 로드밸런서 앞에 배치.
RabbitMQ 선택: 복잡한 라우팅 규칙이 필요하거나, 작업 큐(Task Queue) 패턴, 이메일/알림 같은 일반적인 비동기 작업에 적합. 운영이 상대적으로 간단합니다.
Kafka 선택: 초당 수백만 메시지, 메시지 이력 보존(재생)이 필요할 때, 이벤트 소싱 패턴, 실시간 로그 분석. 운영 복잡도가 높지만 처리량이 압도적입니다.
현업에서는 일반적인 비동기 처리는 RabbitMQ, 이벤트 스트리밍/분석 파이프라인은 Kafka를 사용합니다.
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주)
NestJS TCP 마이크로서비스
API Gateway 구현
Docker Compose 구성
Saga 패턴 구현
Circuit Breaker 적용
분산 추적(Jaeger) 설정
gRPC 서비스 간 통신
MSA 보안 강화
e커머스 실전 프로젝트