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 전환을 직접 지원한다.
2.1 모놀리스의 한계에서 MSA 패턴이 등장한 이유
섹션 제목: “2.1 모놀리스의 한계에서 MSA 패턴이 등장한 이유”모놀리스는 초기에 유리하다. 함수 호출은 네트워크 호출보다 빠르고, 하나의 DB 트랜잭션으로 주문·결제·재고를 묶을 수 있으며, 로컬에서 전체 흐름을 디버깅하기 쉽다. 문제는 규모가 커졌을 때 변경 단위·확장 단위·장애 단위가 모두 애플리케이션 전체로 고정된다는 점이다. 한 팀이 결제 로직만 바꿔도 전체 서비스를 다시 배포해야 하고, 주문 조회 트래픽만 늘어도 전체 애플리케이션을 복제해야 하며, 한 모듈의 메모리 누수가 프로세스 전체를 죽인다.
MSA는 이 한계를 “서비스 분리 + 중앙 통제 구조”로 우회한다. 비즈니스 경계별로 배포 단위를 나누고, 각 서비스가 자신의 데이터 저장소를 소유하게 만든 뒤, 외부 진입은 API Gateway로 모으고 내부 서비스 간 정책은 Service Mesh·Circuit Breaker·Saga·Outbox로 제어한다. Twilio Segment 사례가 이 양면을 잘 보여준다. 초기에는 단일 큐에서 한 destination 장애가 전체 delivery를 지연시키는 head-of-line blocking을 겪었고, destination별 큐와 서비스로 분리해 장애 격리를 얻었다. 하지만 이후 서비스가 140개를 넘자 운영 오버헤드가 선형 증가했고, 공유 라이브러리 변경 때 140개 이상 서비스를 배포해야 하는 비용 때문에 일부 영역을 다시 모놀리스로 합쳤다. 즉 MSA의 등장 배경은 “모놀리스가 낡아서”가 아니라 독립 배포·독립 확장·장애 격리가 전체 재배포 비용보다 커지는 순간이며, 반대로 그 이득보다 운영 복잡도가 크면 모놀리스나 모듈러 모놀리스가 더 낫다. (출처: Twilio Segment - Goodbye Microservices)
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는 만능 해결책이 아니다. 아래 표는 “도입 허가서”가 아니라 반례를 찾기 위한 질문지다. 예를 들어 팀이 8명이고 도메인 경계가 아직 매주 바뀌는데 “결제만 독립 배포하고 싶다”는 이유로 주문·결제·재고를 쪼개면, ACID 트랜잭션 하나로 끝나던 변경이 Saga, Outbox, 멱등성 저장소, 분산 추적까지 요구하는 운영 과제로 바뀐다. 프론트엔드의 Module Federation도 같은 원리다. 디자인 시스템과 인증 세션 경계가 안정되기 전에 화면을 팀별 remote bundle로 찢으면 배포 독립성보다 런타임 버전 충돌과 장애 원인 추적 비용이 먼저 온다.
| 조건 | 모놀리식 권장 | MSA 권장 |
|---|---|---|
| 팀 규모 | ~10명 이하 | 30명 이상, 팀 분리 가능 |
| 도메인 경계 | 불명확함 | 명확히 정의됨 |
| 배포 주기 | 주 1회 이하 | 일 단위, 팀별 독립 배포 필요 |
| 트래픽 패턴 | 균일 | 특정 서비스에 집중 |
| DevOps 성숙도 | 낮음 | 높음(컨테이너, CI/CD, 모니터링 완비) |
실무에서는 “모놀리식 먼저, 경계가 보이면 분리”가 안전한 접근법이다. 분리 후보는 다음 세 조건을 동시에 만족할 때 우선순위가 높다. 첫째, 배포 주기가 다른 영역보다 뚜렷하게 빠르거나 느리다. 둘째, 트래픽 피크가 전체 시스템과 다르다. 셋째, 장애가 났을 때 다른 영역으로 전파되면 안 된다. 셋 중 하나만 해당하면 패키지/모듈 경계 강화로도 충분한 경우가 많고, 셋 모두 해당하면 API Gateway·Service Mesh·Saga 같은 주변 패턴의 운영 비용을 감수할 명분이 생긴다.
MSA 도입 비용의 현실적 수치
섹션 제목: “MSA 도입 비용의 현실적 수치”판단 기준표에 체크하기 전에 다음 수치를 먼저 확인하라. MSA가 “무조건 좋다”고 생각하면 아래 비용이 보이지 않는다.
| 항목 | 모놀리식 | MSA |
|---|---|---|
| 서비스 간 호출 오버헤드 | 함수 호출 ~1μs | HTTP 호출 ~1ms (약 1,000배 차이) |
| 운영 리소스 증가 | 서비스 1개 | ECS 서비스 분리 시 서비스당 Task Definition + CloudWatch 대시보드 + ALB Target Group 추가 |
| 장애 원인 파악 시간 | 평균 ~15분 (단일 로그 스트림) | 분산 추적 미구축 시 45분~2시간 (로그가 서비스별로 산재) |
실제 역행 사례 — Segment(Twilio) 2018년 Segment는 destination 처리 영역에서 140개가 넘는 서비스를 단일 서비스로 통합했다. 각 destination을 독립 서비스로 둔 구조는 장애 격리에는 효과가 있었지만, 서비스·큐·레포가 늘수록 운영 오버헤드가 선형 증가했고 shared library 변경이 대규모 배포 문제로 변했다. 통합 후에는 140개 이상 destination의 테스트가 밀리초 단위로 끝나고, 공유 라이브러리 개선 건수도 1년 뒤 32건에서 46건으로 늘었다고 보고했다. 이 사례의 결론은 “MSA 금지”가 아니라 “분리의 이득을 측정할 수 없으면 통합이 더 빠를 수 있다”이다. (출처: Twilio Segment - Goodbye Microservices)
모놀리스 → 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: falseAPI Gateway가 깨지는 조건
섹션 제목: “API Gateway가 깨지는 조건”API Gateway 자체가 병목이나 단일 장애점이 되는 조건을 미리 파악해 두어야 한다.
처리량 한계 수치 (AWS API Gateway 기준)
| 항목 | 기본값 | 초과 시 동작 |
|---|---|---|
| 계정 초당 최대 요청 수 | 10,000 req/s (리전당) | 429 Too Many Requests 반환 (Throttling) |
| 버스트 한도 | 5,000 req | 한도 초과 요청 즉시 거부 |
위 수치는 계정·리전 단위 기본 quota다. 즉 orders-api 하나가 9,000 RPS를 쓰고 같은 리전의 users-api가 2,000 RPS를 동시에 쓰면, 개별 API가 작아 보여도 계정 전체 한도에서 throttling이 발생할 수 있다. 운영 중 429가 늘면 먼저 API별 평균 RPS가 아니라 계정·리전 전체 RPS 합계를 확인해야 한다. (출처: AWS API Gateway quotas)
P99 지연이 1초를 넘을 때 확인 방법
CloudWatch에서 Latency(전체 응답 시간)와 IntegrationLatency(백엔드 처리 시간)를 분리해서 확인하라.
Latency - IntegrationLatency가 크면 → Gateway 자체 오버헤드IntegrationLatency가 크면 → 백엔드 서비스 병목
API Gateway가 단일 장애점이 되는 조건
- 단일 스테이지에 모든 환경을 몰아넣은 경우: dev/prod 트래픽이 섞이면 dev 부하가 prod에 영향. Stage 분리 필수.
- 캐싱 미설정으로 모든 요청이 백엔드 통과: 단순 GET 요청도 매번 백엔드를 경유하여 불필요한 부하 전가. TTL 기반 캐싱 활성화 필요.
- WAF 없이 공개 엔드포인트 운영: 단순 HTTP flood만으로 Throttling 한도에 도달 가능. AWS WAF 연결 또는 Shield Standard 최소 적용.
NestJS 자체 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를 검토하는 것이 현실적이다.
도입 직전에는 “mesh가 해결할 문제”와 “mesh가 새로 만드는 문제”를 같은 명령으로 확인해야 한다. 예를 들어 서비스 간 p99 지연과 오류율을 Envoy 지표로 볼 수 없는 상태라면 먼저 관측성부터 구축하고, 이미 sidecar를 주입한 클러스터라면 다음처럼 프록시 리소스와 mTLS 상태를 확인한다. Istio 공식 문서는 Envoy sidecar가 서비스의 inbound/outbound 트래픽을 중재한다고 설명하므로, 장애 분석도 애플리케이션 컨테이너만 보지 말고 istio-proxy를 함께 봐야 한다.
kubectl top pod -n production --containers | grep istio-proxyistioctl proxy-statusistioctl authn tls-check order-service.production.svc.cluster.local# 예상 해석istio-proxy CPU가 앱 컨테이너보다 높음 → mesh 정책/재시도/telemetry 비용 의심proxy-status에 STALE 표시 → Istiod 설정 전파 지연 또는 xDS 연결 문제 의심tls-check가 DISABLE/PERMISSIVE → STRICT mTLS 요구 환경이면 정책 누락 의심이 확인 없이 “서비스가 10개를 넘었다”는 이유만으로 mesh를 넣으면, 문제 원인을 애플리케이션 로그에서 찾을지 Envoy 로그에서 찾을지조차 불명확해진다. (출처: Istio Architecture)
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 |
|---|---|---|
| 실행 위치 | 모든 CloudFront 엣지 | 리전 엣지 캐시 중심 |
| 실행 시간 | 보통 1ms 미만 | 수 ms~수 초 작업 |
| 네트워크 접근 | 불가 | 가능 |
| 주요 용도 | URL 재작성, 헤더 조작 | 복잡한 로직, DB 조회 |
CloudFront는 600개 이상의 PoP와 13개 리전 엣지 캐시를 제공한다. CloudFront Functions는 모든 엣지 로케이션에서 실행되어 URL 재작성·헤더 조작처럼 짧은 작업에 적합하고, AWS는 일반적으로 1ms 미만의 오버헤드와 수백만 RPS 확장을 목표 용도로 설명한다. 단, CloudFront Functions 런타임은 네트워크·파일 시스템·환경변수 접근이 제한되므로 외부 API 호출, S3/DynamoDB 연동, SSR처럼 네트워크 접근이나 긴 실행이 필요한 로직은 Lambda@Edge 쪽이 맞다. 주요 활용 사례는 A/B 테스트 트래픽 분기, 지역별 리다이렉트, 가벼운 토큰/헤더 검증, 보안 헤더 자동 추가다. (출처: AWS CloudFront Documentation, CloudFront Features, CloudFront Functions restrictions)
상세 구현 코드(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는 올바르게 설정해야 효과가 있다. 너무 민감하거나 너무 둔감한 설정은 오히려 해가 된다.
너무 민감한 설정 (false positive — 정상 서비스를 차단)
errorThresholdPercentage: 10, timeout: 500ms→ 일시적인 네트워크 지터(jitter)나 GC pause에도 Open 상태 전환→ 영향: 정상 서비스를 차단, 불필요한 Fallback 응답 증가너무 둔감한 설정 (false negative — 심각한 장애를 방치)
errorThresholdPercentage: 80, volumeThreshold: 100→ 에러율이 80%를 넘고 요청이 100건 이상 쌓인 뒤에야 Open 전환→ 영향: 그 사이 Cascading Failure 전파, 복구 지연권장 기본값 (opossum 기준)
| 옵션 | 권장값 | 의미 |
|---|---|---|
timeout | 3,000ms | 서비스 SLA P99 응답 시간의 3~5배로 설정 |
errorThresholdPercentage | 50 | 요청의 50% 이상 실패 시 Open |
resetTimeout | 30,000ms | 30초 후 Half-Open 전환 |
volumeThreshold | 5 | 최소 5회 요청 이후부터 통계 적용 |
Half-Open이 즉시 재Open되는 조건
resetTimeout이 지나 Half-Open으로 전환되었지만 서비스가 아직 완전히 회복되지 않은 상태라면 → 첫 요청에서 실패 → 즉시 재Open된다.
이 경우 **지수 백오프(exponential backoff)**를 적용하라: 실패할 때마다 resetTimeout을 2배씩 증가시켜 불필요한 탐색 요청을 줄인다.
// opossum에서 지수 백오프 구현 (커스텀)let resetTimeoutMs = 30_000;
this.breaker.on("open", () => { // 재Open될 때마다 대기 시간을 2배로 늘림 (최대 5분) resetTimeoutMs = Math.min(resetTimeoutMs * 2, 300_000); this.breaker.options.resetTimeout = resetTimeoutMs; this.logger.warn( `Circuit OPEN: 다음 Half-Open 시도까지 ${resetTimeoutMs / 1000}초 대기`, );});
this.breaker.on("close", () => { // 완전 복구 시 초기화 resetTimeoutMs = 30_000;});”왜 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)은 cqrs-event-sourcing.md에서 다룹니다. Saga에서 Outbox를 사용하는 이유: DB 저장과 이벤트 발행을 하나의 로컬 트랜잭션으로 묶어 “DB는 커밋됐는데 SQS 발행이 실패”하는 상황을 방지합니다.
Saga 패턴이 실패하는 엣지 케이스
섹션 제목: “Saga 패턴이 실패하는 엣지 케이스”Saga는 분산 트랜잭션을 해결하지만, 구현이 잘못되면 오히려 더 복잡한 장애를 만든다.
보상 트랜잭션 자체가 실패하는 경우
결제 취소(보상 트랜잭션) 실행 중 PG사 네트워크 장애 발생→ 보상도 실패 → 시스템이 불일치 상태(주문은 취소됐는데 결제는 차감된 상태)로 남음→ 자동 복구 불가능, 수동 개입 필요대응 전략: 보상 트랜잭션에도 **재시도(retry with exponential backoff)**를 적용하고, 최종 실패 시 Dead Letter Queue(DLQ)로 이관 + 운영팀 알림.
Choreography Saga의 이벤트 순서 역전
네트워크 지연으로 payment_completed 이벤트가order_created 이벤트보다 먼저 도착하는 경우
→ 수신 서비스가 아직 주문이 없는 상태에서 결제 완료를 처리→ 상태 불일치 발생대응 전략: 이벤트 페이로드에 버전 번호 또는 타임스탬프 포함 → 수신 서비스에서 순서 검증 + 멱등성 키(idempotency key) 확인으로 중복 처리 방지.
장기 실행 Saga의 타임아웃 미설정
AWS Step Functions 표준 워크플로우의 최대 실행 시간은 1년이지만, 단계별 타임아웃을 설정하지 않으면 단계가 응답 없이 영구 대기 상태가 될 수 있다.
// Step Functions Task에 HeartbeatSeconds 설정 예시"ProcessPayment": { "Type": "Task", "Resource": "arn:aws:lambda:...:payment-handler", "HeartbeatSeconds": 60, // 60초 내 진행 신호 없으면 실패 처리 "TimeoutSeconds": 300, // 최대 5분 "Next": "ReserveInventory"}권장: HeartbeatSeconds는 단계의 최대 예상 처리 시간의 2배로 설정하되, TimeoutSeconds보다 작아야 한다. 타임아웃 발생 시 Step Functions는 Task를 실패로 표시하므로 States.Timeout 또는 heartbeat timeout을 Catch하여 보상 트랜잭션을 실행한다. 운영 중에는 실행 이력을 먼저 확인한다.
aws stepfunctions get-execution-history \ --execution-arn arn:aws:states:ap-northeast-2:123456789012:execution:order-saga:exec-001 \ --max-items 20# 예상 출력 해석TaskTimedOut 또는 TaskFailed가 ProcessPayment에 있음 → 결제 보상 단계로 이동했는지 확인ExecutionStarted 이후 같은 Task가 오래 유지됨 → TimeoutSeconds/HeartbeatSeconds 누락 의심이 기준은 “최대 1년 실행 가능”을 장점으로 오해하지 않게 해준다. 긴 실행 한도는 비즈니스 프로세스 수명을 허용하는 장치이지, 개별 결제·재고 단계가 무제한 대기해도 된다는 뜻이 아니다. (출처: AWS Step Functions - Task state, AWS Step Functions - workflow type)
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분 내 중복 메시지를 자동으로 제거한다. 기본 FIFO 큐는 파티션당 API action별 300 TPS, batching 사용 시 3,000 messages/s까지 처리하며, high throughput mode를 켜면 리전별 quota까지 확장할 수 있다. 그래도 Standard 큐보다 순서 보장 비용이 있으므로 결제·재고처럼 message group 단위 순서가 중요한 흐름에 우선 적용한다. (출처: Amazon SQS message quotas)
주의할 점은 FIFO deduplication이 5분 내 SendMessage 재시도 중복을 줄여줄 뿐, 소비자 로직의 영구 멱등성을 대체하지 않는다는 것이다. 같은 주문 완료 이벤트가 7분 뒤 운영자 재처리, DLQ redrive, 배포 중 visibility timeout 만료로 다시 들어오면 FIFO deduplication 창을 벗어난다. 따라서 ProcessedEvent 같은 저장소 기반 멱등성 키는 FIFO를 쓰더라도 유지해야 한다. (출처: Amazon SQS exactly-once processing)
# 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 도입 전 “정말 필요한가”를 팀과 합의했는가
7.5 실습 후 자가 점검
섹션 제목: “7.5 실습 후 자가 점검”다음 질문에 설계 근거와 함께 답할 수 있으면 이 문서를 이해한 것이다.
- MSA 적합성 판단: 현재 팀(또는 현재 규모의 팀)에서 MSA 도입이 적합한가? 적합하지 않다면 어떤 조건이 충족되어야 하는가?
- API Gateway vs Service Mesh 역할 분리: API Gateway와 Service Mesh를 동시에 도입해야 하는가? 각각의 역할(North-South vs East-West 트래픽)로 구분해서 설명하라.
- Saga 보상 트랜잭션 순서: 주문→결제→재고 3단계 Saga에서 재고 차감 실패 시 보상 트랜잭션의 실행 순서는 무엇인가?
- Circuit Breaker 튜닝:
errorThresholdPercentage를 50%로 설정했는데 서비스가 너무 자주 Open 상태가 된다. 어떤 설정 값을 먼저 조정하겠는가? 그 이유는? - CQRS 읽기 모델 동기화 지연 허용 기준: Command Side와 Query Side가 서로 다른 데이터베이스를 사용할 때, 읽기 모델의 동기화 지연은 얼마까지 허용 가능한가? 그 판단 근거(SLA, 비즈니스 요건)는 무엇인가?
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으로 지연을 최소화하는 아키텍처 패턴의 집합이다. — 단, 소규모 팀은 “모놀리식 먼저, 경계가 보이면 분리”가 현실적인 전략이다.