MSA Patterns
분류: Layer 9 - 아키텍처 & 설계 패턴
MSA 패턴 (Microservices Architecture Patterns)
섹션 제목: “MSA 패턴 (Microservices Architecture Patterns)”1. 한 줄 정의
섹션 제목: “1. 한 줄 정의”MSA(마이크로서비스 아키텍처)는 하나의 큰 애플리케이션을 독립적으로 배포·확장 가능한 작은 서비스들로 나누어 운영하는 아키텍처 패턴이며, API Gateway·Service Mesh·Edge Computing 같은 주변 패턴들과 함께 동작한다.
프론트엔드 브릿지: 프론트에서 Webpack Module Federation(마이크로 프론트엔드)을 알고 있다면 MSA의 서비스 분리와 동일한 원리다. 마이크로 프론트엔드가 UI를 독립 번들로 나눠 팀별로 배포하듯, MSA는 백엔드 비즈니스 로직을 독립 서비스로 나눠 팀별로 배포한다. “각 팀이 자신의 영역을 독립적으로 빌드·배포한다”는 핵심 철학이 프론트엔드와 백엔드 양쪽에서 동일하게 적용된다.
2. 왜 중요한가
섹션 제목: “2. 왜 중요한가”서비스가 성장할수록 단일 코드베이스(Monolith)는 배포 위험, 확장 비용, 팀 간 충돌이라는 세 가지 문제를 동시에 일으킨다. MSA는 이 문제를 해결하지만 동시에 분산 시스템 특유의 복잡성을 도입한다. BackOps 엔지니어 입장에서는 다음 이유로 필수 지식이다.
- 배포 독립성: 특정 서비스만 배포할 수 있어 릴리스 주기가 빨라진다.
- 장애 격리: 결제 서비스가 죽어도 상품 조회 서비스는 살아있다.
- 팀 자율성: 서비스마다 독립 팀이 기술 스택까지 선택할 수 있다.
- NestJS와의 연관성: NestJS는 마이크로서비스 모듈(
@nestjs/microservices)을 내장하여 MSA 전환을 직접 지원한다.
3. 핵심 개념
섹션 제목: “3. 핵심 개념”3.1 Monolithic vs Microservices
섹션 제목: “3.1 Monolithic vs Microservices”모놀리식 = 백화점 한 건물 안에 식품, 의류, 가전이 모두 있다. 관리가 편하고 손님이 한 번에 모든 것을 해결할 수 있지만, 식품 코너 화재 시 건물 전체가 영업을 중단해야 한다. 식품 코너만 리모델링하려 해도 전체 건물 공사가 필요하다.
MSA = 전문 매장 거리(스트리트몰) 각 매장(서비스)은 독립 건물이다. 카페가 불이 나도 옆 서점은 영업 중이다. 각 매장은 자체 인테리어·운영 방식(기술 스택)을 선택한다. 단, 손님이 여러 매장을 돌아다녀야 하고 매장 간 재고 조율(데이터 일관성)이 복잡해진다.
모놀리식 구조
[클라이언트] │ ▼[단일 애플리케이션] ├── 사용자 모듈 ├── 주문 모듈 ├── 결제 모듈 └── 배송 모듈 │ ▼[단일 데이터베이스]모든 모듈이 같은 프로세스에서 실행된다. 함수 호출이 곧 모듈 간 통신이다. 배포 시 전체를 빌드·재시작해야 한다.
MSA 구조
[클라이언트] │ ▼[API Gateway] ├──▶ [사용자 서비스] ──▶ [사용자 DB] ├──▶ [주문 서비스] ──▶ [주문 DB] ├──▶ [결제 서비스] ──▶ [결제 DB] └──▶ [배송 서비스] ──▶ [배송 DB]각 서비스는 독립 프로세스(컨테이너)로 실행된다. 서비스 간 통신은 네트워크(HTTP REST, gRPC, 메시지 큐)를 경유한다. 각 서비스는 자체 데이터베이스를 소유한다(Database per Service 패턴).
장단점 비교표
섹션 제목: “장단점 비교표”| 항목 | 모놀리식 | MSA |
|---|---|---|
| 배포 | 전체 재배포 필요, 위험 높음 | 서비스 단위 독립 배포 가능 |
| 확장 | 전체 복제(비용 낭비) | 특정 서비스만 수평 확장 가능 |
| 장애 격리 | 단일 장애점(SPOF) | 서비스별 독립 장애 격리 |
| 개발 속도 | 초반 빠름, 규모 커지면 느려짐 | 초반 셋업 오래 걸림, 이후 팀별 병렬 개발 |
| 복잡성 | 낮음(단순 구조) | 높음(분산 시스템, 네트워크 장애 등) |
| 기술 스택 | 단일 스택 | 서비스별 다른 스택 선택 가능 |
| 테스트 | 통합 테스트 쉬움 | 서비스 간 통합 테스트 어려움 |
| 데이터 일관성 | 트랜잭션으로 보장 | 분산 트랜잭션 필요(Saga 패턴 등) |
NestJS 마이크로서비스 코드 예시
섹션 제목: “NestJS 마이크로서비스 코드 예시”// main.ts - 마이크로서비스로 부트스트랩import { NestFactory } from "@nestjs/core";import { MicroserviceOptions, Transport } from "@nestjs/microservices";import { AppModule } from "./app.module";
async function bootstrap() { const app = await NestFactory.createMicroservice<MicroserviceOptions>( AppModule, { transport: Transport.TCP, options: { host: "0.0.0.0", port: 3001, // 주문 서비스 포트 }, }, ); await app.listen(); console.log("주문 서비스가 포트 3001에서 실행 중입니다.");}bootstrap();
// order.controller.ts - 메시지 패턴으로 통신import { Controller } from "@nestjs/common";import { MessagePattern, Payload } from "@nestjs/microservices";
@Controller()export class OrderController { @MessagePattern("create_order") // 이벤트 이름으로 라우팅 async createOrder(@Payload() data: { userId: string; items: any[] }) { console.log("주문 생성 요청 수신:", data); // 주문 생성 로직 return { orderId: "ORD-001", status: "created" }; }}// 예상 출력주문 서비스가 포트 3001에서 실행 중입니다.주문 생성 요청 수신: { userId: 'user-123', items: [...] }”왜 Database per Service가 필수인가” — 데이터 독립성의 원리
섹션 제목: “”왜 Database per Service가 필수인가” — 데이터 독립성의 원리”MSA에서 각 서비스가 자체 DB를 소유하는 패턴(Database per Service)은 선택이 아니라 핵심 전제 조건이다. 공유 DB를 사용하면 MSA의 모든 장점이 무너진다.
공유 DB의 문제:
주문 서비스 ──┐ ├──▶ [공유 DB] ← users 테이블 스키마 변경사용자 서비스 ──┘
1. 사용자 서비스가 users 테이블에 컬럼 추가2. 주문 서비스의 SQL 쿼리가 깨짐 → 배포 실패3. "독립 배포"가 불가능해짐 → MSA의 핵심 가치 상실
Database per Service:
주문 서비스 ──▶ [주문 DB] ← 독립적 스키마 관리사용자 서비스 ──▶ [사용자 DB] ← 독립적 스키마 관리
1. 사용자 서비스가 스키마를 자유롭게 변경2. 주문 서비스는 API를 통해서만 사용자 데이터에 접근3. 인터페이스(API)만 유지되면 내부 변경은 자유단점은 서비스 간 데이터 조인이 불가능해진다는 것이다. “주문 + 사용자 정보”를 한 번에 조회하려면 API 호출로 데이터를 모아야 한다. 이 비용을 감수할 수 없는 규모라면 모놀리식이 더 적합하다.
📖 더 보기: Microservices.io — Database per Service Pattern — Database per Service의 장단점과 CQRS/Saga 연계 패턴 (중급)
“우리 팀 규모에서 MSA가 맞는가” 판단 기준
섹션 제목: ““우리 팀 규모에서 MSA가 맞는가” 판단 기준”MSA는 만능 해결책이 아니다. Twilio의 Segment 팀은 실제로 MSA에서 모놀리식으로 역행한 사례로 유명하다. 다음 기준으로 판단하라.
| 조건 | 모놀리식 권장 | MSA 권장 |
|---|---|---|
| 팀 규모 | ~10명 이하 | 30명 이상, 팀 분리 가능 |
| 도메인 경계 | 불명확함 | 명확히 정의됨 |
| 배포 주기 | 주 1회 이하 | 일 단위, 팀별 독립 배포 필요 |
| 트래픽 패턴 | 균일 | 특정 서비스에 집중 |
| DevOps 성숙도 | 낮음 | 높음(컨테이너, CI/CD, 모니터링 완비) |
실무 조언: “모놀리식 먼저, 경계가 보이면 분리”가 안전한 접근법이다. 처음부터 MSA를 도입하면 아직 불명확한 도메인 경계 때문에 서비스를 나중에 합쳐야 하는 역설이 발생한다.
모놀리스 → MSA 마이그레이션 체크리스트
섹션 제목: “모놀리스 → MSA 마이그레이션 체크리스트”MSA로 전환하기 전에 다음 항목을 점검하라. 체크되지 않은 항목이 많다면 모놀리식을 유지하는 것이 낫다.
- 도메인 경계가 명확히 식별되었는가? (주문/결제/사용자 등 경계가 DDD Event Storming 등으로 검증되었는가)
- 팀이 30명 이상이고 독립적으로 배포를 원하는가? (소규모 팀에서 MSA는 오버엔지니어링이다)
- CI/CD 파이프라인이 서비스별로 독립 구성 가능한가? (배포 자동화 없는 MSA는 운영 지옥이다)
- 각 서비스가 독립 데이터 스토어를 가질 수 있는가? (DB를 공유하면 MSA의 핵심 가치(독립 배포)가 사라진다)
- 분산 트랜잭션 실패 처리 전략(Saga 패턴 등)이 설계되었는가? (데이터 일관성 문제가 모놀리식보다 훨씬 복잡해진다)
- 서비스 간 통신 실패·지연에 대한 Circuit Breaker가 준비되었는가? (한 서비스 장애가 전체로 전파되는 카스케이딩 장애를 막아야 한다)
- 중앙 집중식 모니터링·분산 추적(OpenTelemetry 등)이 구축되었는가? (로그가 여러 서비스에 분산되면 장애 원인 추적이 극도로 어려워진다)
3.2 API Gateway
섹션 제목: “3.2 API Gateway”API Gateway = 건물 로비의 안내 데스크 건물(MSA 시스템)에는 여러 층(서비스)이 있다. 방문객(클라이언트)은 안내 데스크에 먼저 들른다. 데스크는 방문 목적을 확인(인증)하고, 해당 층으로 안내(라우팅)하며, 1분에 최대 10명까지만 입장 허용(Rate Limiting)한다. 클라이언트는 내부 구조를 알 필요가 없다.
API Gateway가 없다면 클라이언트는 각 마이크로서비스의 주소를 직접 알아야 한다. 서비스가 10개라면 클라이언트 코드에 10개의 엔드포인트가 하드코딩된다. 서비스 주소가 바뀌면 클라이언트도 수정해야 한다.
API Gateway는 단일 진입점(Single Entry Point) 을 제공한다.
클라이언트 앱들 ├── Web Browser ├── iOS App └── Android App │ ▼ [API Gateway] ← 여기서 모든 공통 처리 ├── 라우팅: /orders → 주문 서비스:3001 ├── 인증: JWT 검증 (모든 서비스 공통) ├── Rate Limiting: 초당 100 요청까지 ├── 요청 변환: XML → JSON └── 응답 변환: 여러 서비스 응답 병합 │ ┌────┼────────────┐ ▼ ▼ ▼주문 사용자 결제서비스 서비스 서비스API Gateway 주요 기능
섹션 제목: “API Gateway 주요 기능”1. 라우팅 (Routing)
GET /api/v1/orders → 주문 서비스 (port 3001)GET /api/v1/users → 사용자 서비스 (port 3002)POST /api/v1/payments → 결제 서비스 (port 3003)2. 인증·인가 (Authentication & Authorization) 각 마이크로서비스가 JWT 검증 로직을 중복 구현하는 대신, API Gateway에서 일괄 처리한다.
3. Rate Limiting 클라이언트가 API를 과도하게 호출하는 것을 방지한다.
4. 요청/응답 변환 모바일 클라이언트는 데이터를 최소화하고, 웹 클라이언트는 상세 데이터가 필요할 때, Gateway에서 응답을 필터링·변환한다.
AWS API Gateway 설정 예시
섹션 제목: “AWS API Gateway 설정 예시”# AWS CLI로 API Gateway 생성aws apigateway create-rest-api \ --name "MyServiceGateway" \ --description "주문 서비스 API Gateway"
# 예상 출력{ "id": "abc123def4", "name": "MyServiceGateway", "description": "주문 서비스 API Gateway", "createdDate": "2024-01-15T10:30:00+00:00", "apiKeySource": "HEADER", "endpointConfiguration": { "types": ["EDGE"] }}AWS 콘솔 경로: API Gateway → Create API → REST API → Routes 설정 → Integration (Lambda 또는 HTTP 엔드포인트 연결)
Kong API Gateway (오픈소스) 설정 예시
섹션 제목: “Kong API Gateway (오픈소스) 설정 예시”# kong.yml - 선언형 설정_format_version: "3.0"
services: - name: order-service url: http://order-service:3001 routes: - name: order-routes paths: - /api/orders
- name: user-service url: http://user-service:3002 routes: - name: user-routes paths: - /api/users
plugins: - name: rate-limiting # Rate Limiting 플러그인 config: minute: 100 # 분당 100회 제한 policy: local
- name: jwt # JWT 인증 플러그인 config: secret_is_base64: falseNestJS 자체 API Gateway 패턴
섹션 제목: “NestJS 자체 API Gateway 패턴”NestJS로 API Gateway 역할을 하는 서비스를 직접 구현할 수 있다.
import { Controller, Get, Post, Body, Param, Inject } from "@nestjs/common";import { ClientProxy } from "@nestjs/microservices";import { firstValueFrom } from "rxjs";
@Controller("api")export class AppController { constructor( @Inject("ORDER_SERVICE") private orderClient: ClientProxy, @Inject("USER_SERVICE") private userClient: ClientProxy, ) {}
@Get("orders/:id") async getOrder(@Param("id") id: string) { // 주문 서비스에 메시지 전송 후 응답 대기 return firstValueFrom(this.orderClient.send("get_order", { orderId: id })); }
@Get("dashboard/:userId") async getDashboard(@Param("userId") userId: string) { // 여러 서비스에 병렬 요청 후 응답 병합 (BFF 패턴) const [user, orders] = await Promise.all([ firstValueFrom(this.userClient.send("get_user", { userId })), firstValueFrom(this.orderClient.send("get_user_orders", { userId })), ]);
return { user, orders, timestamp: new Date().toISOString() }; }}BFF (Backend for Frontend) 패턴
섹션 제목: “BFF (Backend for Frontend) 패턴”API Gateway가 하나의 공통 게이트웨이라면, BFF는 클라이언트 유형별 전용 게이트웨이다.
문제 상황: 모바일 앱은 데이터를 최소화해야 배터리·데이터를 아끼지만, 웹 대시보드는 풍부한 데이터가 필요하다. 단일 API Gateway는 양쪽을 모두 만족시키다 보면 점점 복잡해진다.
BFF 해결책:
[iOS App] [Android App] [Web Browser] [Admin Panel] │ │ │ │ ▼ ▼ ▼ ▼[Mobile BFF] [Mobile BFF] [Web BFF] [Admin BFF] │ │ └────────────┬───────────────┘ ▼ [공통 마이크로서비스들] 주문 / 사용자 / 결제모바일 BFF는 응답을 압축·최소화하고, 웹 BFF는 여러 서비스 데이터를 집계하여 반환한다.
3.3 Service Mesh (Istio)
섹션 제목: “3.3 Service Mesh (Istio)”Service Mesh = 도로 위의 교통 관제 시스템 마이크로서비스들은 도시의 차량(서비스)이고, 서비스 간 통신은 도로(네트워크)다. Service Mesh는 각 교차로에 신호등과 CCTV(Sidecar Proxy)를 설치하는 것과 같다. 운전자(서비스 코드)는 신호등 존재를 모른다. 그냥 운전할 뿐이다. 하지만 교통량 통계, 신호 제어, 사고 차단(circuit breaker)은 신호등이 자동으로 처리한다.
원리: Sidecar Proxy 패턴
섹션 제목: “원리: Sidecar Proxy 패턴”핵심은 서비스 코드를 수정하지 않고 네트워크 레벨에서 기능을 주입하는 것이다.
기존 방식 (서비스 코드 수정 필요):[서비스 A] ─── 재시도 로직 직접 구현[서비스 A] ─── mTLS 핸드쉐이크 직접 구현[서비스 A] ─── 메트릭 수집 코드 직접 삽입
Sidecar 방식 (서비스 코드 무수정):┌─────────────────────────┐│ Pod (Kubernetes) ││ ┌──────────────────┐ ││ │ 서비스 A 컨테이너 │ ││ └────────┬─────────┘ ││ │ localhost ││ ┌────────▼─────────┐ ││ │ Envoy Proxy │ │ ← Sidecar│ │ (istio-proxy) │ ││ └──────────────────┘ │└─────────────────────────┘ │ 외부 네트워크 ▼ [다른 서비스 Pod]Kubernetes Pod 내에서 서비스 컨테이너와 Envoy 프록시 컨테이너가 나란히 실행된다. 서비스의 모든 인바운드/아웃바운드 트래픽은 Envoy를 경유한다. 서비스 A는 localhost로 Envoy와 통신한다고 착각하고, 실제 제어는 Envoy가 담당한다.
Istio 구조: Control Plane vs Data Plane
섹션 제목: “Istio 구조: Control Plane vs Data Plane”Control Plane (Istiod)┌─────────────────────────────────┐│ Istiod ││ ├── Pilot: 라우팅 규칙 관리 ││ ├── Citadel: 인증서 관리(mTLS) ││ └── Galley: 설정 검증 │└────────────┬────────────────────┘ │ xDS 프로토콜로 설정 배포 ▼Data Plane (Envoy Proxies)┌──────────┐ ┌──────────┐ ┌──────────┐│ Service │ │ Service │ │ Service ││ A │ │ B │ │ C ││ [Envoy] │ │ [Envoy] │ │ [Envoy] │└──────────┘ └──────────┘ └──────────┘Istiod(Control Plane)는 각 Envoy에게 “어떻게 트래픽을 처리할지” 규칙을 xDS 프로토콜로 전달한다. Envoy들(Data Plane)은 실제 트래픽을 처리한다.
Istio 주요 기능 (MSA 패턴 관점)
섹션 제목: “Istio 주요 기능 (MSA 패턴 관점)”Service Mesh가 MSA 패턴에 제공하는 핵심 가치는 세 가지다.
1. 서비스 간 통신 보안 (mTLS) 서비스 코드 수정 없이 모든 서비스 간 통신을 자동으로 TLS 암호화한다. Istiod(Control Plane)가 인증서를 자동 발급·갱신하고 각 Envoy에 배포한다.
2. 트래픽 관리 카나리 배포(신 버전에 10% 트래픽), A/B 테스트, 서킷 브레이킹, 재시도 정책을 서비스 코드 수정 없이 Control Plane 설정만으로 제어한다.
3. 관찰 가능성 (Observability) 모든 서비스 간 트래픽의 메트릭(지연, 오류율, 처리량)을 자동 수집한다. Prometheus(메트릭), Jaeger(분산 추적), Kiali(서비스 맵 시각화)와 연동된다. mTLS, 분산 추적의 상세 운영 설정은 L6(운영 심화 - 관측성) 을 참조하세요.
”왜 Sidecar 방식을 쓰는가” — 라이브러리 방식의 한계
섹션 제목: “”왜 Sidecar 방식을 쓰는가” — 라이브러리 방식의 한계”Service Mesh 이전에는 서비스 간 통신 제어를 **라이브러리(SDK)**로 해결했다. 재시도, Circuit Breaker, mTLS를 각 서비스 코드에 직접 구현했다. 이 방식의 문제점은 다음과 같다.
라이브러리 방식의 문제:
1. 언어 종속성: Java용 Hystrix, Node.js용 opossum, Go용 별도 라이브러리... → 서비스마다 기술 스택이 다르면 동일한 기능을 N번 구현해야 함
2. 업데이트 지옥: Circuit Breaker 정책을 변경하려면 → 모든 서비스의 라이브러리 버전을 업데이트 → 모든 서비스를 재배포 → 서비스가 30개면 30번 배포
3. 관심사 침투: 비즈니스 로직 코드에 인프라 코드가 섞임 → 코드 리뷰 시 "이게 비즈니스 로직인가 인프라 설정인가" 혼란
Sidecar 방식의 해결:
1. 언어 무관: Envoy(C++)가 모든 언어의 서비스 옆에서 동작2. 중앙 업데이트: Istiod에서 정책 변경 → 모든 Envoy에 자동 전파 (재배포 불필요)3. 관심사 분리: 서비스 코드에 인프라 코드가 0줄📖 더 보기: Istio Architecture — Istio 공식 문서 — Control Plane과 Data Plane의 상세 동작 원리 (중급)
AWS App Mesh vs Istio 비교
섹션 제목: “AWS App Mesh vs Istio 비교”| 항목 | AWS App Mesh | Istio |
|---|---|---|
| 설치 복잡도 | 낮음(AWS 관리형) | 높음(직접 설치·관리) |
| AWS 통합 | 네이티브(CloudWatch, X-Ray) | 별도 설정 필요 |
| 기능 범위 | 기본 트래픽 관리 | mTLS, 정책, 관찰성 등 풍부 |
| 비용 | 사용량 과금 | 오픈소스(무료, 인프라 비용 별도) |
| 커뮤니티 | AWS 한정 | CNCF 표준, 광범위 |
”지금 당장 필요한가” 판단 기준
섹션 제목: “”지금 당장 필요한가” 판단 기준”Service Mesh는 강력하지만 소규모 팀에는 오버엔지니어링이 될 수 있다.
Service Mesh 도입 체크리스트:
✅ 필요한 상황: - 서비스 수 10개 이상 - 서비스 간 보안(mTLS) 요구사항 존재 - 카나리 배포, A/B 테스트 필요 - 분산 추적 없이 장애 원인 찾기 불가능한 수준 - Kubernetes 운영 경험 있는 DevOps 팀 보유
❌ 아직 이른 상황: - 서비스 수 5개 미만 - 팀이 Kubernetes 자체를 처음 배우는 중 - 모놀리식에서 막 분리한 직후 - 운영 인력이 부족한 소규모 스타트업대안: 소규모 팀은 AWS ALB + 서비스별 로깅 + X-Ray 분산 추적으로 시작하고, 서비스가 10개를 넘어설 때 Service Mesh를 검토하는 것이 현실적이다.
3.4 Edge Computing (개요)
섹션 제목: “3.4 Edge Computing (개요)”Edge Computing = 편의점 서울 본사(Origin 서버)에서만 재고를 관리한다면 전국 고객이 서울까지 와야 한다. 대신 각 동네에 편의점(Edge 노드)을 두면 가까운 곳에서 빠르게 처리할 수 있다. CDN이 정적 파일을 배포하듯, Edge Computing은 로직(코드)까지 엣지에서 실행한다.
MSA에서의 역할
섹션 제목: “MSA에서의 역할”MSA 아키텍처에서 Edge Computing은 클라이언트와 API Gateway 사이에 위치하는 전처리 계층이다. 서비스 자체의 비즈니스 로직을 건드리지 않으면서도 공통 관심사(인증 토큰 검증, 지역 라우팅, A/B 테스트 트래픽 분기)를 엣지 레벨에서 처리한다.
[사용자(부산)] ──▶ [Edge 노드(부산/대구)] → 응답 (지연: ~5ms) │ 공통 처리(인증·라우팅·헤더) 후 └── 필요한 경우만 [API Gateway → 마이크로서비스]로 전달이 구조 덕분에 각 마이크로서비스는 엣지 수준의 공통 처리를 중복 구현하지 않아도 된다.
AWS에서의 구현 옵션
섹션 제목: “AWS에서의 구현 옵션”AWS에서는 CloudFront와 두 가지 실행 환경을 제공한다.
| 항목 | CloudFront Functions | Lambda@Edge |
|---|---|---|
| 실행 위치 | 600+ 엣지 로케이션 | ~13 리전 |
| 실행 시간 | < 1ms | 5~200ms |
| 네트워크 접근 | 불가 | 가능 |
| 주요 용도 | URL 재작성, 헤더 조작 | 복잡한 로직, DB 조회 |
주요 활용 사례: A/B 테스트 트래픽 분기, 지역별 리다이렉트, JWT 토큰 엣지 검증, 보안 헤더 자동 추가.
상세 구현 코드(CloudFront Functions A/B 테스트, Lambda@Edge JWT 검증)는 L3(AWS 인프라) 또는 L7(네트워크 심화) 를 참조하세요. 이 문서에서는 MSA 패턴 관점의 역할과 위치만 다룹니다.
3.5 Circuit Breaker 패턴 (회로 차단기)
섹션 제목: “3.5 Circuit Breaker 패턴 (회로 차단기)”비유: 전기 회로 차단기
섹션 제목: “비유: 전기 회로 차단기”집에서 과전류가 흐르면 차단기(두꺼비집)가 내려간다. 차단기가 없으면 전선이 타거나 화재가 발생한다. 마이크로서비스의 Circuit Breaker도 동일하다. 한 서비스가 응답 불능이 될 때, 무한 대기하는 대신 **빠르게 실패(Fail Fast)**하여 전체 시스템이 멈추지 않도록 보호한다.
원리: 3가지 상태
섹션 제목: “원리: 3가지 상태”Circuit Breaker는 세 가지 상태를 전환하며 동작한다.
Closed (정상) │ │ 일정 횟수 이상 실패 발생 ▼Open (차단됨) ──── 모든 요청 즉시 실패 반환 │ │ 일정 시간(cooldown) 경과 ▼Half-Open (탐지 중) ──── 일부 요청만 통과시켜 테스트 │ │ │ 성공 │ 실패 ▼ ▼Closed (복구) Open (재차단)왜 이렇게 설계되었는가?
단순히 “실패하면 에러 반환”이 아니라 복구 시점을 탐지해야 하기 때문이다. Open 상태에서 영구적으로 차단하면 서비스가 복구되어도 트래픽을 받지 못한다. Half-Open은 “조심스럽게 복구를 확인하는 단계”다.
📖 더 보기: Circuit Breaker Pattern — microservices.io — 상태 전환 다이어그램과 적용 기준을 명확하게 설명한 레퍼런스
NestJS에서 Circuit Breaker 구현
섹션 제목: “NestJS에서 Circuit Breaker 구현”방법 1: @nestjs/axios + 수동 구현
import { Injectable } from "@nestjs/common";
enum CircuitState { CLOSED = "CLOSED", OPEN = "OPEN", HALF_OPEN = "HALF_OPEN",}
@Injectable()export class CircuitBreakerService { private state = CircuitState.CLOSED; private failureCount = 0; private lastFailureTime: number | null = null;
private readonly FAILURE_THRESHOLD = 5; // 5회 실패 → OPEN private readonly COOLDOWN_MS = 30_000; // 30초 후 HALF_OPEN
async call<T>(fn: () => Promise<T>, fallback: () => T): Promise<T> { // OPEN 상태: 쿨다운 확인 if (this.state === CircuitState.OPEN) { const elapsed = Date.now() - (this.lastFailureTime ?? 0); if (elapsed > this.COOLDOWN_MS) { this.state = CircuitState.HALF_OPEN; console.log("[CircuitBreaker] OPEN → HALF_OPEN: 복구 탐지 시작"); } else { console.log("[CircuitBreaker] OPEN 상태: 즉시 fallback 반환"); return fallback(); } }
try { const result = await fn(); this.onSuccess(); return result; } catch (err) { this.onFailure(); return fallback(); } }
private onSuccess(): void { this.failureCount = 0; this.state = CircuitState.CLOSED; console.log("[CircuitBreaker] 성공 → CLOSED 상태 유지"); }
private onFailure(): void { this.failureCount++; this.lastFailureTime = Date.now(); if (this.failureCount >= this.FAILURE_THRESHOLD) { this.state = CircuitState.OPEN; console.log( `[CircuitBreaker] ${this.failureCount}회 실패 → OPEN 상태 전환`, ); } }}// order.service.ts - Circuit Breaker 적용@Injectable()export class OrderService { constructor( private readonly circuitBreaker: CircuitBreakerService, private readonly paymentClient: ClientProxy, ) {}
async processPayment(orderId: string, amount: number) { return this.circuitBreaker.call( // 실제 결제 서비스 호출 () => firstValueFrom( this.paymentClient.send("process_payment", { orderId, amount }), ), // Circuit이 열렸을 때 대안 반환 (Fallback) () => ({ status: "pending", message: "결제 서비스 점검 중. 잠시 후 재시도해주세요.", }), ); }}// 예상 시뮬레이션 로그// 1~4번째 실패:[CircuitBreaker] 실패 기록: 1/5[CircuitBreaker] 실패 기록: 2/5...
// 5번째 실패 → OPEN 전환:[CircuitBreaker] 5회 실패 → OPEN 상태 전환
// 이후 요청들 (30초 동안):[CircuitBreaker] OPEN 상태: 즉시 fallback 반환{ status: 'pending', message: '결제 서비스 점검 중. 잠시 후 재시도해주세요.' }
// 30초 경과 후 첫 요청:[CircuitBreaker] OPEN → HALF_OPEN: 복구 탐지 시작
// 결제 서비스 복구 성공:[CircuitBreaker] 성공 → CLOSED 상태 유지방법 2: opossum 라이브러리 (권장) + ECS 헬스체크 연계
npm install opossum @types/opossum// circuit-breaker.service.ts — opossum 기반 구현import { Injectable, Logger } from "@nestjs/common";import * as CircuitBreaker from "opossum";import { HttpService } from "@nestjs/axios";import { firstValueFrom } from "rxjs";
@Injectable()export class PaymentCircuitBreakerService { private readonly logger = new Logger(PaymentCircuitBreakerService.name); private readonly breaker: CircuitBreaker;
constructor(private readonly httpService: HttpService) { this.breaker = new CircuitBreaker(this.callPaymentService.bind(this), { timeout: 3000, // 3초 초과 시 실패 처리 errorThresholdPercentage: 50, // 50% 실패율 초과 시 OPEN resetTimeout: 30000, // 30초 후 HALF-OPEN 시도 volumeThreshold: 5, // 최소 5번 요청 후 통계 시작 });
// 상태 변화 로그 (CloudWatch로 수집됨) this.breaker.on("open", () => this.logger.warn("Circuit OPEN: 결제 서비스 차단"), ); this.breaker.on("halfOpen", () => this.logger.log("Circuit HALF-OPEN: 결제 서비스 탐색 중"), ); this.breaker.on("close", () => this.logger.log("Circuit CLOSED: 결제 서비스 복구"), ); }
private async callPaymentService(orderId: string, amount: number) { const response = await firstValueFrom( this.httpService.post("http://payment-service/process", { orderId, amount, }), ); return response.data; }
async processPayment(orderId: string, amount: number) { return this.breaker.fire(orderId, amount).catch((err) => { this.logger.error(`결제 처리 실패 (fallback): ${err.message}`); return { success: false, reason: "payment_service_unavailable", orderId }; }); }}ECS 헬스체크와 연계 (AWS 환경)
섹션 제목: “ECS 헬스체크와 연계 (AWS 환경)”ECS Task Definition의 헬스체크와 Circuit Breaker를 연계하면 자가 회복(Self-Healing) 구조를 만들 수 있다. Circuit Breaker가 OPEN 상태임을 헬스체크 엔드포인트에 반영하면, ECS가 장애 서비스를 자동 감지하여 재시작한다.
// health.controller.ts — 의존 서비스 상태를 헬스체크에 반영@Controller("health")export class HealthController { constructor( private health: HealthCheckService, private http: HttpHealthIndicator, ) {}
@Get() @HealthCheck() check() { return this.health.check([ () => this.http.pingCheck("payment-service", "http://payment-service/health"), () => this.http.pingCheck( "notification-service", "http://notification-service/health", ), ]); }}# ECS Task Definition — 헬스체크 설정healthCheck: command: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"] interval: 30 # 30초마다 체크 timeout: 5 # 5초 내 응답 없으면 실패 retries: 3 # 3번 연속 실패 시 UNHEALTHY startPeriod: 60 # 컨테이너 시작 후 60초 유예 기간ECS는 헬스체크 실패 시 해당 Task를 교체하고, ALB는 UNHEALTHY 인스턴스로의 트래픽을 자동 차단한다. Circuit Breaker + ECS HealthCheck 조합이 MSA 환경 자가 회복의 핵심 패턴이다.
”왜 Circuit Breaker 없이는 위험한가”
섹션 제목: “”왜 Circuit Breaker 없이는 위험한가””Circuit Breaker 없는 경우:1. 결제 서비스가 느려짐 (응답 30초 이상)2. API Gateway가 결제 서비스 응답 대기 중3. 모든 요청이 대기 상태로 쌓임 (스레드 풀 고갈)4. API Gateway 자체가 타임아웃 → 503 오류5. 주문 서비스 → API Gateway → 전체 시스템 다운 (Cascading Failure)
Circuit Breaker가 있는 경우:1. 결제 서비스가 느려짐2. 5회 실패 후 즉시 OPEN → Fallback 응답 반환 (10ms 이내)3. 다른 서비스는 정상 동작 유지4. 30초 후 자동 복구 탐지📖 더 보기: Resilient Microservices with NestJS: Circuit Breaker — NestJS 인터셉터로 Circuit Breaker를 구현하는 실전 가이드 (중급)
3.6 CQRS 패턴 (Command Query Responsibility Segregation)
섹션 제목: “3.6 CQRS 패턴 (Command Query Responsibility Segregation)”비유: 주문받는 직원 vs 재고 확인 직원
섹션 제목: “비유: 주문받는 직원 vs 재고 확인 직원”식당에서 주문을 받는 직원(Command)과 메뉴판을 보여주는 직원(Query)은 다르다. 주문 처리는 복잡한 규칙(재고 확인, 결제, 알림)을 거치지만, 메뉴 조회는 그냥 빠르게 보여주기만 하면 된다. CQRS는 이 두 역할을 코드 수준에서 명확히 분리한다.
원리: 읽기와 쓰기를 분리하는 이유
섹션 제목: “원리: 읽기와 쓰기를 분리하는 이유”단일 모델로 읽기와 쓰기를 모두 처리하면 두 가지 문제가 생긴다.
단일 모델의 한계:
1. 성능 충돌: 쓰기(주문 생성) → 복잡한 도메인 검증, 트랜잭션, 이벤트 발행 읽기(주문 목록) → 단순 조회, JOIN, 페이징 → 복잡한 도메인 모델이 단순 조회 성능까지 희생
2. 확장 충돌: 쓰기는 트래픽이 낮지만 처리가 복잡 → 수직 확장 필요 읽기는 트래픽이 높지만 처리가 단순 → 수평 확장 필요 → 하나의 서비스로 두 가지를 동시에 최적화 불가
CQRS 분리 후:Command Side: 도메인 모델 기반, 강한 일관성, 쓰기 최적화 DBQuery Side: 읽기 전용 모델, 최종 일관성, 읽기 최적화 DB(뷰, 캐시)NestJS에서 CQRS 구현 (@nestjs/cqrs)
섹션 제목: “NestJS에서 CQRS 구현 (@nestjs/cqrs)”npm install @nestjs/cqrs// Command: 상태를 변경하는 요청export class CreateOrderCommand { constructor( public readonly userId: string, public readonly items: { productId: string; quantity: number }[], ) {}}
// Command Handler: 도메인 로직 실행// create-order.handler.tsimport { CommandHandler, ICommandHandler, EventBus } from "@nestjs/cqrs";
@CommandHandler(CreateOrderCommand)export class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> { constructor( private readonly orderRepository: OrderRepository, private readonly eventBus: EventBus, ) {}
async execute(command: CreateOrderCommand): Promise<string> { const order = Order.create(command.userId, command.items); await this.orderRepository.save(order);
// 도메인 이벤트 발행 → Query Side가 읽기 모델 업데이트 this.eventBus.publish(new OrderCreatedEvent(order.id, order.userId));
return order.id; }}// Query: 데이터 조회 요청 (도메인 모델 거치지 않음)export class GetOrderListQuery { constructor( public readonly userId: string, public readonly page: number, ) {}}
// Query Handler: DB에서 직접 읽기 최적화 조회// get-order-list.handler.tsimport { QueryHandler, IQueryHandler } from "@nestjs/cqrs";
@QueryHandler(GetOrderListQuery)export class GetOrderListHandler implements IQueryHandler<GetOrderListQuery> { constructor(private readonly dataSource: DataSource) {}
async execute(query: GetOrderListQuery): Promise<OrderListDto[]> { // 읽기 전용 최적화 쿼리 (도메인 모델을 거치지 않음) return this.dataSource.query( `SELECT o.id, o.status, o.total_price, o.created_at FROM orders o WHERE o.user_id = $1 ORDER BY o.created_at DESC LIMIT 20 OFFSET $2`, [query.userId, (query.page - 1) * 20], ); }}// Controller에서 사용@Controller("orders")export class OrdersController { constructor( private readonly commandBus: CommandBus, private readonly queryBus: QueryBus, ) {}
@Post() async create(@Body() dto: CreateOrderDto) { const orderId = await this.commandBus.execute( new CreateOrderCommand(dto.userId, dto.items), ); return { orderId }; }
@Get() async list(@Query("userId") userId: string, @Query("page") page = 1) { return this.queryBus.execute(new GetOrderListQuery(userId, page)); }}// 예상 동작 흐름POST /orders → CommandBus → CreateOrderHandler → Order.create() (도메인 규칙 검증) → orderRepository.save() → EventBus.publish(OrderCreatedEvent) → 응답: { orderId: "ord-abc123" }
GET /orders?userId=user-1&page=1 → QueryBus → GetOrderListHandler → 직접 SQL 조회 (도메인 모델 없음, 빠름) → 응답: [{ id, status, total_price, created_at }, ...]“왜 AWS SNS/SQS와 CQRS를 함께 쓰는가” — 이벤트 기반 읽기 모델 동기화
섹션 제목: ““왜 AWS SNS/SQS와 CQRS를 함께 쓰는가” — 이벤트 기반 읽기 모델 동기화”MSA에서 CQRS의 Query Side는 다른 서비스의 이벤트를 수신하여 자신만의 읽기 모델을 유지한다. AWS SNS/SQS는 이 이벤트 전달의 신뢰성을 보장한다.
이벤트 기반 읽기 모델 동기화:
주문 서비스 알림 서비스 OrderCreatedEvent ──▶ SNS Topic ──▶ SQS Queue ──▶ Query Model 업데이트 (Command Side) (자체 읽기 DB에 저장)
사용자 서비스가 "최근 주문 5건"을 조회할 때: ← 주문 서비스에 직접 API 호출 (❌ 동기 의존) ← 자신의 읽기 모델 DB에서 조회 (✅ 독립성 유지)// NestJS에서 SQS 이벤트 수신 후 읽기 모델 업데이트import { SqsMessageHandler } from "@ssut/nestjs-sqs";
@Injectable()export class OrderEventHandler { constructor(private readonly orderReadRepository: OrderReadRepository) {}
@SqsMessageHandler("order-events-queue", false) async handleOrderEvent(message: AWS.SQS.Message) { const event = JSON.parse(message.Body);
if (event.type === "OrderCreated") { // 읽기 모델(Query Side) 업데이트 await this.orderReadRepository.upsert({ id: event.orderId, userId: event.userId, status: "pending", createdAt: event.timestamp, }); } }}📖 더 보기: Decompose Monoliths using CQRS and Event Sourcing — AWS Prescriptive Guidance — AWS 환경에서 CQRS와 이벤트 소싱으로 모놀리스를 마이크로서비스로 분해하는 공식 가이드 (중급)
📖 더 보기: NestJS Microservices with AWS SNS and SQS — NestJS에서 SNS 팬아웃 패턴으로 여러 SQS 큐에 이벤트를 배포하는 실전 구현 (중급)
3.7 Saga 패턴과 Outbox 패턴 — 분산 트랜잭션 일관성
섹션 제목: “3.7 Saga 패턴과 Outbox 패턴 — 분산 트랜잭션 일관성”비유: 은행 이체와 취소 영수증
섹션 제목: “비유: 은행 이체와 취소 영수증”A 계좌에서 B 계좌로 100만 원을 이체한다고 가정하자. 모놀리식에서는 하나의 DB 트랜잭션으로 처리된다. MSA에서는 “A 계좌 서비스”와 “B 계좌 서비스”가 물리적으로 다른 DB를 쓴다. 이 상황에서 A에서 출금 후 B 입금이 실패하면 어떻게 되는가?
Saga 패턴은 이 문제를 **보상 트랜잭션(Compensating Transaction)**으로 해결한다. B 입금이 실패하면 A 출금을 자동으로 되돌리는 “취소 트랜잭션”을 실행하는 것이다.
원리: Choreography vs Orchestration
섹션 제목: “원리: Choreography vs Orchestration”Choreography(안무) 방식 — 각 서비스가 이벤트를 발행하고 서로 반응
1. OrderService → "order_placed" 이벤트 발행2. PaymentService 수신 → 결제 처리 ├── 성공: "payment_completed" 이벤트 발행 └── 실패: "payment_failed" 이벤트 발행3. OrderService가 "payment_failed" 수신 → 주문 취소 (보상 트랜잭션)4. InventoryService가 "order_cancelled" 수신 → 재고 복구
[OrderService] [PaymentService] [InventoryService] order_placed ──────────────▶ 결제 시도 payment_failed ──────────────▶ (무관) order_cancelled ◀─────────── (OrderService가 처리) 재고 복구 ◀────────Orchestration(지휘) 방식 — 중앙 Orchestrator가 흐름 제어
[Saga Orchestrator] │ 1. OrderService에 "createOrder" 명령 │ 2. PaymentService에 "processPayment" 명령 │ 3. InventoryService에 "reserveItems" 명령 │ 4. 실패 시 역순으로 "compensate" 명령 발행 │ └── AWS Step Functions 또는 Temporal로 구현NestJS + AWS Step Functions로 Orchestration Saga 구현
섹션 제목: “NestJS + AWS Step Functions로 Orchestration Saga 구현”// order-saga.handler.ts — Step Functions 실행 시작import { Injectable } from "@nestjs/common";import { SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn";
@Injectable()export class OrderSagaHandler { private readonly sfnClient = new SFNClient({ region: "ap-northeast-2" });
async startOrderSaga(order: CreateOrderDto): Promise<string> { const command = new StartExecutionCommand({ stateMachineArn: process.env.ORDER_SAGA_STATE_MACHINE_ARN, input: JSON.stringify({ orderId: order.id, userId: order.userId, items: order.items, amount: order.totalAmount, }), });
const result = await this.sfnClient.send(command); console.log(`[OrderSaga] 실행 시작: ${result.executionArn}`); return result.executionArn; }}// Step Functions 상태 머신 정의 (단순화){ "Comment": "Order Saga", "StartAt": "ProcessPayment", "States": { "ProcessPayment": { "Type": "Task", "Resource": "arn:aws:lambda:...:payment-handler", "Next": "ReserveInventory", "Catch": [{ "ErrorEquals": ["PaymentFailed"], "Next": "CompensateOrder" }] }, "ReserveInventory": { "Type": "Task", "Resource": "arn:aws:lambda:...:inventory-handler", "End": true, "Catch": [ { "ErrorEquals": ["InventoryFailed"], "Next": "CompensatePayment" } ] }, "CompensatePayment": { "Type": "Task", "Resource": "arn:aws:lambda:...:payment-refund-handler", "Next": "CompensateOrder" }, "CompensateOrder": { "Type": "Task", "Resource": "arn:aws:lambda:...:order-cancel-handler", "End": true } }}// 예상 실행 로그 (결제 실패 시나리오)[OrderSaga] 실행 시작: arn:aws:states:...:execution:order-saga:exec-001[PaymentHandler] 결제 처리 시도: orderId=ORD-001, amount=15000[PaymentHandler] 결제 실패: InsufficientFunds[OrderSaga] 보상 트랜잭션 시작: CompensateOrder[OrderCancelHandler] 주문 취소 처리: ORD-001 → status: cancelled📖 더 보기: Saga Orchestration for Microservices Using the Outbox Pattern — InfoQ — AWS Step Functions와 Outbox 패턴을 결합한 실전 아키텍처 (중급)
Outbox 패턴 — 이벤트 발행의 원자성 보장
섹션 제목: “Outbox 패턴 — 이벤트 발행의 원자성 보장”📌 Outbox Pattern 상세 구현(outbox-relay.service.ts, cron relay, idempotency key)은 L8 cqrs-event-sourcing.md에서 다룹니다. Saga에서 Outbox를 사용하는 이유: DB 저장과 이벤트 발행을 하나의 로컬 트랜잭션으로 묶어 “DB는 커밋됐는데 SQS 발행이 실패”하는 상황을 방지합니다.
Saga + Outbox 조합의 의의
섹션 제목: “Saga + Outbox 조합의 의의”| 패턴 | 해결하는 문제 | 보장 |
|---|---|---|
| Saga | 여러 서비스에 걸친 분산 트랜잭션 일관성 | 최종 일관성 + 보상 트랜잭션 |
| Outbox | 이벤트 발행의 원자성 (DB 저장 = 발행) | At-Least-Once 이벤트 전달 보장 |
| 조합 | Saga 각 단계를 Outbox로 안전하게 전달 | 메시지 유실 없는 분산 트랜잭션 |
📖 더 보기: Microservices.io — Transactional Outbox Pattern — Outbox 패턴의 공식 레퍼런스. Message Relay 구현 방법과 주의사항 포함 (중급)
4. 실무에서 어떻게 쓰이나
섹션 제목: “4. 실무에서 어떻게 쓰이나”API Gateway 활용 (가장 보편적)
- AWS API Gateway + Lambda로 서버리스 API 구성
- 인증 미들웨어를 각 서비스에 중복 구현하지 않고 Gateway에서 일괄 처리
- Throttling 설정으로 DDoS 기본 방어
- Stage(dev/prod) 분리로 환경별 배포 관리
NestJS 마이크로서비스 실무 패턴
// 주문이 완료되면 이벤트 발행 → 알림 서비스가 구독@EventPattern('order_completed')async handleOrderCompleted(@Payload() data: OrderCompletedEvent) { await this.notificationService.sendPushNotification({ userId: data.userId, message: `주문 ${data.orderId}가 완료되었습니다.`, });}Service Mesh 도입 타이밍 대부분의 스타트업은 서비스 5~10개 규모에서 Kubernetes로 이전하면서 Istio를 검토한다. AWS EKS 환경이라면 AWS App Mesh가 CloudWatch, X-Ray와 기본 통합되어 있어 진입 장벽이 낮다.
5. 내 업무와의 연결고리
섹션 제목: “5. 내 업무와의 연결고리”- NestJS:
@nestjs/microservices모듈을 활용하면 TCP/Redis/RabbitMQ 기반 마이크로서비스를 빠르게 구현할 수 있다. - AWS 스택: API Gateway + Lambda 조합은 NestJS 앱을 서버리스로 배포할 때도 그대로 사용 가능하다.
- BackOps 관점: 서비스 간 의존 관계를 파악하고, 장애 시 어떤 서비스가 다운스트림에 영향을 주는지 추적하는 것이 핵심 역할이다.
- CloudFront Functions: 인증 토큰 검증을 엣지에서 처리하면 Origin(NestJS 서버) 부하를 크게 줄일 수 있다.
6. 비슷한 개념과 비교
섹션 제목: “6. 비슷한 개념과 비교”| 패턴 | 역할 | 위치 | 선택 기준 |
|---|---|---|---|
| API Gateway | 단일 진입점, 라우팅·인증 | 클라이언트-서비스 사이 | 모든 MSA에서 기본 |
| BFF | 클라이언트별 최적화 | API Gateway 역할 분화 | 클라이언트 종류가 2개 이상 |
| Service Mesh | 서비스 간 네트워크 제어 | 서비스-서비스 사이 | 서비스 10개 이상, Kubernetes 환경 |
| Load Balancer | 트래픽 분산 | Gateway 앞단 | 모든 환경 |
| Edge Computing | 엣지에서 로직 실행 | CDN 노드 | 지연 최소화, 글로벌 서비스 |
API Gateway vs Service Mesh 명확한 차이
- API Gateway: 외부 클라이언트 → 내부 서비스 트래픽 제어 (North-South 트래픽)
- Service Mesh: 내부 서비스 ↔ 내부 서비스 트래픽 제어 (East-West 트래픽)
두 가지는 경쟁 관계가 아니라 상호 보완적으로 함께 사용된다.
6.5. 트러블슈팅
섹션 제목: “6.5. 트러블슈팅”1. 서비스 간 순환 의존 (Circular Dependency)
섹션 제목: “1. 서비스 간 순환 의존 (Circular Dependency)”증상/에러
서비스 A → 서비스 B 호출 중 타임아웃서비스 B → 서비스 A를 다시 호출하는 구조결과: 두 서비스 모두 응답 없이 hang 상태로그: [OrderService] Timeout waiting for UserService response (30000ms)원인 도메인 경계가 잘못 정의된 경우 발생한다. 주문 서비스가 사용자 서비스를 호출하고, 사용자 서비스가 다시 주문 이력을 위해 주문 서비스를 호출하는 패턴이다. 동기 호출 체인에서는 데드락이 발생한다.
해결 방법
- 이벤트 기반 비동기 통신으로 전환: 동기 HTTP 호출 대신 메시지 큐(RabbitMQ, SQS) 활용
- 도메인 경계 재설계: 주문 이력은 주문 서비스가 소유하고, 사용자 서비스는 주문 서비스에 의존하지 않도록 변경
- 데이터 복제: 사용자 서비스가 필요한 주문 데이터를 이벤트로 수신하여 자체 DB에 복제 (CQRS 패턴)
// 해결책: 이벤트 발행으로 순환 의존 제거// 주문 서비스 - 이벤트 발행this.eventEmitter.emit('order.created', { userId: order.userId, orderId: order.id, amount: order.totalAmount,});
// 사용자 서비스 - 이벤트 수신 (주문 서비스를 직접 호출하지 않음)@OnEvent('order.created')async handleOrderCreated(payload: OrderCreatedEvent) { await this.userOrderHistoryRepository.save(payload);}2. API Gateway 병목 (Bottleneck)
섹션 제목: “2. API Gateway 병목 (Bottleneck)”증상/에러
모든 서비스 응답은 정상인데 전체 응답 시간이 느림AWS CloudWatch: API Gateway P99 latency > 3000ms특정 시간대에 503 Service Unavailable 급증로그: [API Gateway] Throttling limit exceeded for path /api/orders원인
- AWS API Gateway 기본 계정 한도: 초당 10,000 요청 (리전당)
- 스테이지별 Throttling 설정이 너무 낮게 설정됨
- Gateway 자체가 단일 장애점(SPOF)이 되는 구조
- 응답 캐싱 미설정으로 불필요한 백엔드 호출 발생
해결 방법
# 1. AWS API Gateway Throttling 한도 증가 요청aws service-quotas request-service-quota-increase \ --service-code apigateway \ --quota-code L-8A5B8E43 \ --desired-value 20000
# 2. API Gateway 응답 캐싱 활성화 (콘솔 경로)# API Gateway → Stage → Default Route Throttling# Cache Settings → Enable API cache: ✅# Cache capacity: 0.5GB# Cache TTL: 300초
# 3. CloudWatch로 병목 구간 파악aws cloudwatch get-metric-statistics \ --namespace AWS/ApiGateway \ --metric-name Latency \ --dimensions Name=ApiName,Value=MyServiceGateway \ --start-time 2024-01-15T00:00:00Z \ --end-time 2024-01-15T23:59:59Z \ --period 300 \ --statistics p99예상 출력
{ "Datapoints": [ { "Timestamp": "2024-01-15T10:00:00Z", "ExtendedStatistics": { "p99": 2800.5 }, "Unit": "Milliseconds" } ]}3. 분산 트랜잭션 실패 (Distributed Transaction Failure)
섹션 제목: “3. 분산 트랜잭션 실패 (Distributed Transaction Failure)”증상/에러
주문 생성 성공 → 결제 처리 실패 → 주문이 '결제 대기' 상태로 영구 stuck사용자에게는 주문이 완료된 것으로 표시되나 결제는 되지 않은 상태로그: [PaymentService] Payment failed: InsufficientFunds [OrderService] Order ORD-001 status: pending_payment (rollback 없음)원인 마이크로서비스에서는 여러 서비스에 걸친 작업에 RDBMS의 ACID 트랜잭션을 적용할 수 없다. 주문 서비스 DB와 결제 서비스 DB는 물리적으로 분리되어 있기 때문이다.
해결 방법: Saga 패턴
Saga 패턴은 분산 트랜잭션을 일련의 로컬 트랜잭션 + 보상 트랜잭션으로 분해한다. 구현 방식은 두 가지다.
| 구분 | Choreography (안무) | Orchestration (지휘) |
|---|---|---|
| 중앙 제어 | 없음. 각 서비스가 이벤트 발행/구독 | 있음. Saga Orchestrator가 흐름 제어 |
| 결합도 | 낮음 (이벤트만 알면 됨) | 중간 (Orchestrator가 모든 서비스를 알아야 함) |
| 디버깅 | 어려움 (이벤트 체인 추적 필요) | 쉬움 (Orchestrator 로그에 전체 흐름) |
| 적합한 상황 | 서비스 3~4개 이내, 단순 흐름 | 서비스 5개 이상, 복잡한 분기 |
| 도구 예시 | SQS, RabbitMQ 이벤트 | Temporal, AWS Step Functions |
실무 팁: 처음 시작할 때는 Choreography가 간단하다. 서비스가 늘어나고 흐름이 복잡해지면(조건부 분기, 타임아웃 등) Orchestration으로 전환을 검토한다. 하이브리드 방식(단순 흐름은 Choreography, 복잡한 흐름은 Orchestration)도 실무에서 흔하다.
📖 더 보기: Saga Pattern — microservices.io — Choreography와 Orchestration 방식의 상세 비교와 선택 기준 (중급)
📖 더 보기: Saga Pattern Demystified — ByteByteGo — 실무 예시와 함께 두 방식을 시각적으로 비교 (입문~중급)
Choreography-based Saga (이벤트 기반):
1. OrderService: 주문 생성 → 'order_created' 이벤트 발행2. PaymentService: 이벤트 수신 → 결제 처리 시도 ├── 성공: 'payment_completed' 이벤트 발행 └── 실패: 'payment_failed' 이벤트 발행3. OrderService: 'payment_failed' 수신 → 주문 상태를 'cancelled'로 변경 (보상 트랜잭션)// 보상 트랜잭션 (Compensating Transaction) 구현@EventPattern('payment_failed')async handlePaymentFailed(@Payload() data: { orderId: string; reason: string }) { console.log(`결제 실패 이벤트 수신: ${data.orderId}, 사유: ${data.reason}`);
// 보상 트랜잭션: 주문 취소 처리 await this.orderRepository.update(data.orderId, { status: 'cancelled', cancelReason: `결제 실패: ${data.reason}`, cancelledAt: new Date(), });
// 재고 복구 이벤트 발행 this.eventEmitter.emit('order.cancelled', { orderId: data.orderId });
console.log(`주문 ${data.orderId} 취소 처리 완료`);}// 예상 로그[OrderService] 결제 실패 이벤트 수신: ORD-001, 사유: InsufficientFunds[OrderService] 주문 ORD-001 취소 처리 완료[InventoryService] 재고 복구 완료: 상품 PROD-001 수량 +14. SQS 메시지 중복 처리 (Idempotency 위반)
섹션 제목: “4. SQS 메시지 중복 처리 (Idempotency 위반)”증상/에러
주문 완료 이메일이 한 건인데 고객에게 2~3통 발송됨재고가 실제 주문 수보다 더 많이 차감됨로그: [NotificationService] 이메일 전송: order-001 (타임스탬프 3회)원인
AWS SQS는 At-Least-Once Delivery를 보장한다. 즉, 네트워크 이슈나 처리 시간 초과 시 동일한 메시지가 여러 번 전달될 수 있다. 또한 ECS 태스크가 배포 중 재시작되면, 처리 중이던 메시지가 visibility timeout 만료 후 다시 큐에 들어온다.
해결 방법: 멱등성(Idempotency) 보장
// notification.service.ts - 멱등성 키로 중복 처리 방지import { Injectable } from "@nestjs/common";import { InjectRepository } from "@nestjs/typeorm";import { Repository } from "typeorm";
@Injectable()export class NotificationService { constructor( @InjectRepository(ProcessedEvent) private readonly processedEventRepo: Repository<ProcessedEvent>, ) {}
async handleOrderCompleted(event: OrderCompletedEvent) { const idempotencyKey = `order-completed-${event.orderId}`;
// 이미 처리된 이벤트인지 확인 const alreadyProcessed = await this.processedEventRepo.findOne({ where: { key: idempotencyKey }, });
if (alreadyProcessed) { console.log(`[중복 무시] 이미 처리된 이벤트: ${idempotencyKey}`); return; // 중복 메시지 → 아무 작업 없이 종료 (SQS에서 삭제됨) }
// 처리 기록 저장 (트랜잭션으로 묶기) await this.processedEventRepo.save({ key: idempotencyKey, processedAt: new Date(), });
// 실제 비즈니스 로직 await this.sendOrderCompletionEmail(event.userId, event.orderId); console.log(`[처리 완료] 주문 완료 이메일 전송: ${event.orderId}`); }}// ProcessedEvent 엔티티@Entity("processed_events")export class ProcessedEvent { @PrimaryColumn() key: string; // idempotency key (unique)
@Column() processedAt: Date;
// TTL 처리: 7일 이상 된 레코드는 배치 작업으로 정리}// 첫 번째 메시지 처리 시[처리 완료] 주문 완료 이메일 전송: order-001
// 동일 메시지 재전달 시 (SQS 재시도)[중복 무시] 이미 처리된 이벤트: order-completed-order-001추가 팁: SQS FIFO 큐는 MessageDeduplicationId를 제공하여 5분 내 중복 메시지를 자동으로 제거한다. Standard 큐보다 처리량이 낮지만(초당 300 TPS), 순서 보장이 필요한 결제·재고 처리에 적합하다.
# SQS FIFO 큐로 메시지 전송 (중복 제거 ID 포함)aws sqs send-message \ --queue-url https://sqs.ap-northeast-2.amazonaws.com/123456789012/orders.fifo \ --message-body '{"orderId": "order-001"}' \ --message-group-id "order-group" \ --message-deduplication-id "order-001-completed"7. 체크리스트
섹션 제목: “7. 체크리스트”MSA 설계 전 검토 항목:
- 팀 규모와 서비스 수에 비해 MSA가 적합한가 (10명 이하, 5개 미만이면 모놀리식 권장)
- 도메인 경계(Bounded Context)가 명확히 정의되어 있는가
- CI/CD 파이프라인이 서비스별 독립 배포를 지원하는가
- 분산 추적(AWS X-Ray 또는 Jaeger)이 구성되어 있는가
- API Gateway에 인증·Rate Limiting이 설정되어 있는가
- 서비스 간 통신 실패 시 재시도/Circuit Breaker 정책이 있는가
- 분산 트랜잭션이 필요한 경우 Saga 패턴으로 설계되었는가
- 서비스 다운 시 장애가 다른 서비스로 전파되지 않는가
- Edge Computing 활용 시 CloudFront Functions/Lambda@Edge 중 복잡도에 맞는 선택을 했는가
- Service Mesh 도입 전 “정말 필요한가”를 팀과 합의했는가
8. 핵심 키워드
섹션 제목: “8. 핵심 키워드”| 키워드 | 설명 |
|---|---|
| Microservices | 독립 배포·확장 가능한 작은 서비스 단위 아키텍처 |
| Monolith | 단일 코드베이스에 모든 기능을 포함하는 전통적 구조 |
| API Gateway | 클라이언트와 서비스 사이의 단일 진입점 |
| BFF (Backend for Frontend) | 클라이언트 유형별로 최적화된 전용 API Gateway |
| Service Mesh | 서비스 간 트래픽을 투명하게 관리하는 인프라 레이어 |
| Sidecar Proxy | 서비스 컨테이너 옆에 실행되는 네트워크 프록시 컨테이너 |
| Envoy | Istio에서 사용하는 고성능 오픈소스 프록시 |
| Istio | Kubernetes 기반 Service Mesh 솔루션 |
| mTLS | 서비스 간 양방향 TLS 인증·암호화 |
| Saga 패턴 | 분산 트랜잭션을 이벤트 기반 보상 트랜잭션으로 구현하는 패턴 |
| Edge Computing | CDN 엣지 노드에서 로직을 실행하는 컴퓨팅 패러다임 |
| CloudFront Functions | AWS CDN 엣지에서 실행되는 경량 JavaScript 함수 |
| Lambda@Edge | CloudFront 엣지에서 실행되는 AWS Lambda 확장 |
| North-South 트래픽 | 외부 클라이언트 ↔ 내부 서비스 간 트래픽 (API Gateway 영역) |
| East-West 트래픽 | 내부 서비스 ↔ 내부 서비스 간 트래픽 (Service Mesh 영역) |
8.5. 추천 리소스
섹션 제목: “8.5. 추천 리소스”📚 추천 리소스
섹션 제목: “📚 추천 리소스”- 📖 NestJS 공식 문서 — Microservices — TCP, Redis, RabbitMQ, Kafka 트랜스포트 레이어별 예제 포함. NestJS MSA의 출발점 (입문)
- 📖 Microservices.io — MSA 패턴 카탈로그 (Chris Richardson) — API Gateway, Saga, CQRS 등 모든 패턴을 문제→해결책→결과 구조로 정리한 업계 표준 레퍼런스 (입문~중급)
- 📖 Saga Pattern Demystified: Orchestration vs Choreography — ByteByteGo — 두 가지 Saga 방식을 시각 다이어그램으로 비교, 실무 선택 기준 포함 (입문~중급)
- 📖 Decompose Monoliths using CQRS and Event Sourcing — AWS Prescriptive Guidance — AWS 환경에서 CQRS + Event Sourcing으로 모놀리스를 MSA로 분해하는 공식 가이드 (중급)
- 📖 AWS Builders Library — Amazon의 MSA 운영 경험 — AWS가 자사 서비스 운영에서 얻은 실무 지식 공개 문서. “Timeouts, retries, and backoff” 등 실전 장애 대응 (중급)
9. 직접 확인해보기
섹션 제목: “9. 직접 확인해보기”실습 1: NestJS 마이크로서비스 로컬 테스트
섹션 제목: “실습 1: NestJS 마이크로서비스 로컬 테스트”# NestJS 마이크로서비스 프로젝트 생성npx @nestjs/cli new order-servicecd order-servicenpm install @nestjs/microservices
# 마이크로서비스로 실행npm run start:dev// 예상 출력[Nest] LOG [NestFactory] Starting Nest application...[Nest] LOG [InstanceLoader] AppModule dependencies initialized[Nest] LOG [NestMicroservice] Nest microservice successfully started실습 2: AWS API Gateway 상태 확인
섹션 제목: “실습 2: AWS API Gateway 상태 확인”# 현재 계정의 API Gateway 목록 조회aws apigateway get-rest-apis --region ap-northeast-2
# 예상 출력{ "items": [ { "id": "abc123", "name": "MyAPI", "createdDate": "2024-01-01T00:00:00+00:00", "version": "1", "apiKeySource": "HEADER", "endpointConfiguration": { "types": ["REGIONAL"] } } ]}
# API Gateway 특정 엔드포인트 Throttle 설정 확인aws apigateway get-stage \ --rest-api-id abc123 \ --stage-name prod \ --region ap-northeast-2실습 3: Istio 서비스 메시 상태 확인 (Kubernetes 환경)
섹션 제목: “실습 3: Istio 서비스 메시 상태 확인 (Kubernetes 환경)”# Istio 설치 상태 확인kubectl get pods -n istio-system
# 예상 출력NAME READY STATUS RESTARTSistio-ingressgateway-xxx 1/1 Running 0istiod-xxx 1/1 Running 0
# Sidecar Proxy 주입 여부 확인 (READY 컬럼이 2/2이면 Sidecar 주입됨)kubectl get pods -n production
# 예상 출력NAME READY STATUS RESTARTSorder-service-xxx 2/2 Running 0 ← 2/2: 앱 컨테이너 + Envoyuser-service-xxx 2/2 Running 0
# 서비스 간 mTLS 상태 확인istioctl authn tls-check order-service.production.svc.cluster.local
# 예상 출력HOST:PORT STATUS SERVER CLIENT AUTHN POLICYorder-service.production.svc.cluster.local:80 OK STRICT STRICT /default10. 한 줄 요약
섹션 제목: “10. 한 줄 요약”MSA는 서비스를 독립적으로 분리하고(마이크로서비스), API Gateway로 진입점을 단일화하며, Service Mesh로 서비스 간 네트워크를 제어하고, Edge Computing으로 지연을 최소화하는 아키텍처 패턴의 집합이다. — 단, 소규모 팀은 “모놀리식 먼저, 경계가 보이면 분리”가 현실적인 전략이다.