Skip to content

필사 모드: 마이크로서비스 통신 패턴 완전 가이드 2025: REST vs gRPC vs 비동기 메시징, Saga, Outbox

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

TL;DR

- **동기 vs 비동기**: REST/gRPC는 결합도 높고 빠른 응답, 이벤트는 결합도 낮고 비동기. 둘 다 필요

- **REST vs gRPC vs GraphQL**: REST(표준), gRPC(성능 + 타입), GraphQL(클라이언트 주도)

- **Saga 패턴**: 분산 트랜잭션의 표준. Choreography(이벤트) vs Orchestration(중앙 조정자)

- **Outbox**: dual-write 문제(DB + Kafka 동시 쓰기 실패)를 해결하는 필수 패턴

- **회복탄력성 4종**: Circuit Breaker(차단), Retry(재시도), Bulkhead(격리), Timeout(시간 제한)

1. 마이크로서비스 통신의 본질적 어려움

1.1 모놀리스 vs 마이크로서비스 통신

| | 모놀리스 | 마이크로서비스 |

|---|---|---|

| 함수 호출 | 메서드 호출 (ns) | 네트워크 호출 (ms) |

| 트랜잭션 | 단일 ACID | 분산 (Saga) |

| 디버깅 | 스택 트레이스 | 분산 트레이싱 |

| 데이터 일관성 | 즉시 | 최종 일관성 |

| 배포 | 한 번 | 독립적 |

**핵심 통찰**: 마이크로서비스는 **분산 시스템의 모든 어려움을 상속**합니다. CAP, 부분 실패, 네트워크 지연, 메시지 순서 등.

1.2 8가지 분산 컴퓨팅의 오류

L. Peter Deutsch가 정리한 **개발자가 자주 하는 잘못된 가정**:

1. 네트워크는 신뢰할 수 있다

2. 지연시간은 0이다

3. 대역폭은 무한하다

4. 네트워크는 안전하다

5. 토폴로지는 변하지 않는다

6. 관리자는 한 명이다

7. 전송 비용은 0이다

8. 네트워크는 동질적이다

**모두 거짓**입니다. 마이크로서비스를 설계할 때 이 가정들을 의식적으로 깨뜨려야 합니다.

2. 동기 통신 — REST vs gRPC vs GraphQL

2.1 REST — 사실상의 표준

GET /api/v1/users/1000 HTTP/1.1

Host: api.example.com

Accept: application/json

Authorization: Bearer xyz...

HTTP/1.1 200 OK

Content-Type: application/json

{

"id": 1000,

"name": "Alice",

"email": "alice@example.com"

}

**장점**:

- HTTP/JSON 표준 — 모든 언어 지원

- 캐시 친화적 (HTTP 캐시)

- 디버깅 쉬움 (curl, 브라우저)

- API 게이트웨이와 잘 통합

**단점**:

- 텍스트 직렬화 (JSON) 비효율

- HTTP/1.1 한계 (HTTP/2로 개선)

- 스키마 강제 부족 (OpenAPI로 보완)

- 양방향 스트리밍 어려움

2.2 gRPC — 고성능 RPC

// user.proto

syntax = "proto3";

service UserService {

rpc GetUser(GetUserRequest) returns (User);

rpc StreamUsers(StreamUsersRequest) returns (stream User);

}

message GetUserRequest {

int64 user_id = 1;

}

message User {

int64 id = 1;

string name = 2;

string email = 3;

}

**장점**:

- **Protocol Buffers** 바이너리 직렬화 (JSON 대비 5-10배 작음)

- HTTP/2 기반 (멀티플렉싱, 양방향 스트리밍)

- 강한 타입 (.proto 스키마)

- 자동 코드 생성 (모든 언어)

- 4가지 통신 모드 (Unary, Server Streaming, Client Streaming, Bidirectional)

**단점**:

- 브라우저 직접 지원 X (gRPC-Web 필요)

- 디버깅 어려움 (바이너리)

- HTTP/2 인프라 필요

**언제 사용**: 서비스 간 내부 통신, 고성능 필요, 양방향 스트리밍.

2.3 GraphQL — 클라이언트 주도

query GetUserWithPosts {

user(id: 1000) {

name

email

posts(limit: 10) {

title

createdAt

}

}

}

**장점**:

- 클라이언트가 필요한 필드만 요청 (over-fetching 방지)

- 단일 엔드포인트

- 강한 타입 시스템

**단점**:

- N+1 쿼리 위험 (DataLoader 필수)

- 캐싱 복잡 (HTTP 캐시 우회)

- 학습 곡선

**언제 사용**: 다양한 클라이언트(웹/모바일), 다양한 필드 조합.

2.4 비교표

| 특성 | REST | gRPC | GraphQL |

|---|---|---|---|

| 직렬화 | JSON | Protobuf | JSON |

| 전송 | HTTP/1.1 또는 HTTP/2 | HTTP/2 | HTTP |

| 스키마 | OpenAPI (선택) | .proto (필수) | SDL (필수) |

| 캐시 | HTTP 캐시 (쉬움) | 어려움 | 복잡 |

| 양방향 스트리밍 | ❌ | ✅ | Subscriptions |

| 브라우저 | ✅ | gRPC-Web | ✅ |

| 성능 | 보통 | **최고** | 보통 |

| 학습 곡선 | 낮음 | 중간 | 높음 |

| 추천 사용 | 외부 API | 내부 서비스 | 다양한 클라이언트 |

3. 비동기 통신 — 이벤트 드리븐

3.1 왜 비동기인가?

**시나리오**: 주문 → 결제 → 재고 차감 → 배송 → 알림

**동기 방식의 문제**:

1. **결합도**: 모든 서비스가 즉시 응답해야 함

2. **연쇄 실패**: 알림 서비스 다운 = 전체 실패

3. **느림**: 모든 단계의 지연 합산

4. **확장성**: 가장 느린 서비스가 병목

**비동기 방식의 해결**:

1. 주문 서비스: "주문 생성됨" 이벤트 발행 → 즉시 응답

2. 다른 서비스들이 자기 페이스로 처리

3. 한 서비스 다운 → 다른 서비스는 영향 없음

3.2 메시지 브로커 비교

| | Kafka | RabbitMQ | NATS | AWS SQS | Redis Streams |

|---|---|---|---|---|---|

| 모델 | 로그 (영속) | 큐 (소비 시 삭제) | 메시지 + JetStream | 큐 | 로그 |

| 처리량 | 100만+ msg/s | 5만 msg/s | 1100만+ msg/s | 매니지드 | 100만+ msg/s |

| 영속성 | ✅ (디스크) | ✅ (옵션) | JetStream만 | ✅ | ✅ |

| 순서 보장 | 파티션 내 | 큐 내 | 부분적 | FIFO 큐 | Stream 내 |

| 라이선스 | Apache 2.0 | MPL 2.0 | Apache 2.0 | AWS | RSAL |

| 사용 사례 | 이벤트 소싱, 분석 | 작업 큐, RPC | 마이크로서비스 | AWS 기본 | Redis 사용 시 |

3.3 Kafka 패턴

**Topic 설계**:

order-events # 주문 도메인 이벤트

payment-events # 결제 도메인 이벤트

inventory-events # 재고 도메인 이벤트

**파티션 전략**: `order_id` 기준 → 같은 주문의 이벤트가 같은 파티션 → 순서 보장.

producer.send(

'order-events',

key=str(order_id).encode(), # 같은 키 → 같은 파티션

value=json.dumps(event).encode()

)

**Consumer Group**: 같은 그룹의 컨슈머는 파티션을 분담 → 자동 부하 분산.

4. Saga 패턴 — 분산 트랜잭션

4.1 문제: 2PC는 왜 안 되나?

분산 트랜잭션의 전통적 해결책 **2-Phase Commit (2PC)**:

1. Prepare 단계: 모든 참여자에게 "준비됐어?" 확인

2. Commit 단계: 모든 응답이 OK면 commit, 아니면 abort

**문제점**:

- **블로킹**: 하나라도 응답 안 하면 모든 참여자가 락 유지

- **단일 장애점**: 코디네이터 장애 → 전체 멈춤

- **확장성 X**: 분산 환경에서 비현실적

4.2 Saga 패턴

**핵심 아이디어**: 큰 트랜잭션을 작은 로컬 트랜잭션의 시퀀스로 분해. 실패 시 **보상 트랜잭션(compensating transaction)**으로 롤백.

**예시: 주문 처리**

| 단계 | 정상 | 실패 시 보상 |

|---|---|---|

| 1. 주문 생성 | `CreateOrder` | `CancelOrder` |

| 2. 결제 | `ChargePayment` | `RefundPayment` |

| 3. 재고 차감 | `ReserveInventory` | `ReleaseInventory` |

| 4. 배송 | `CreateShipment` | `CancelShipment` |

**규칙**:

- 각 로컬 트랜잭션은 독립적으로 commit

- 실패 시 이미 commit된 트랜잭션을 보상으로 되돌림

- 보상은 **반드시 멱등** (중복 호출에도 안전)

4.3 Choreography vs Orchestration

Choreography (분산 조율)

서비스들이 이벤트를 통해 서로 트리거.

[Order Service]

↓ "OrderCreated" event

[Payment Service]

↓ "PaymentProcessed" event

[Inventory Service]

↓ "InventoryReserved" event

[Shipping Service]

**장점**: 결합도 낮음, 각 서비스 독립적

**단점**: 흐름 추적 어려움, 디버깅 복잡

Orchestration (중앙 조율)

**Orchestrator**가 각 단계를 명시적으로 호출.

[Saga Orchestrator]

├─ Call Payment Service

├─ Call Inventory Service

└─ Call Shipping Service

(실패 시 보상 호출)

**장점**: 흐름 명시적, 디버깅 쉬움

**단점**: Orchestrator가 단일 장애점, 복잡한 로직

**도구**: AWS Step Functions, Camunda, Temporal, Netflix Conductor

4.4 Temporal — 차세대 워크플로 엔진

@workflow.defn

class OrderSaga:

@workflow.run

async def run(self, order_id: str):

try:

await workflow.execute_activity(

charge_payment, order_id,

start_to_close_timeout=timedelta(seconds=30)

)

await workflow.execute_activity(

reserve_inventory, order_id,

start_to_close_timeout=timedelta(seconds=30)

)

await workflow.execute_activity(

create_shipment, order_id,

start_to_close_timeout=timedelta(seconds=30)

)

except ActivityError:

자동 보상

await workflow.execute_activity(refund_payment, order_id)

raise

**Temporal의 강점**:

- 워크플로가 코드처럼 보임 (가독성)

- **상태 자동 영속화** — 서버 재시작 후 이어서 실행

- 자동 재시도, 타임아웃, 보상

5. Outbox 패턴 — Dual-Write 문제 해결

5.1 문제: DB와 Kafka 동시 쓰기

def create_order(order):

db.save(order) # 1. DB 저장

kafka.send("order-events", order) # 2. Kafka 발행

**문제 시나리오**:

- 1번 성공, 2번 실패: DB에는 있지만 이벤트 안 감 → 다른 서비스가 모름

- 1번 실패, 2번 성공: 이벤트는 갔지만 DB에 없음 → 데이터 불일치

이를 **dual-write problem**이라고 합니다.

5.2 해결책: Outbox 테이블

DB에 `outbox` 테이블을 만들어 이벤트를 함께 저장:

CREATE TABLE outbox (

id UUID PRIMARY KEY,

aggregate_type VARCHAR(255),

aggregate_id VARCHAR(255),

event_type VARCHAR(255),

payload JSONB,

created_at TIMESTAMPTZ DEFAULT NOW(),

processed_at TIMESTAMPTZ

);

def create_order(order):

with db.transaction():

db.save(order)

db.save(OutboxEvent(

aggregate_type='Order',

aggregate_id=order.id,

event_type='OrderCreated',

payload=order.to_dict()

))

트랜잭션 commit 시 두 INSERT가 원자적

5.3 Outbox → Kafka 전달

**방법 1: Polling Publisher**

별도 프로세스가 주기적으로 outbox를 폴링:

while True:

events = db.query("SELECT * FROM outbox WHERE processed_at IS NULL LIMIT 100")

for event in events:

kafka.send(event.aggregate_type, event.payload)

db.update("UPDATE outbox SET processed_at = NOW() WHERE id = ?", event.id)

sleep(1)

**방법 2: CDC (Change Data Capture)**

**Debezium** 같은 도구가 PostgreSQL의 WAL을 읽어 자동으로 Kafka에 전송:

Debezium connector config

{

"name": "outbox-connector",

"config": {

"connector.class": "io.debezium.connector.postgresql.PostgresConnector",

"database.hostname": "postgres",

"database.dbname": "orders",

"table.include.list": "public.outbox",

"transforms": "outbox",

"transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter"

}

}

**CDC가 우수한 이유**:

- 별도 폴링 프로세스 불필요

- WAL 기반이라 거의 실시간

- 트랜잭션 순서 보장

6. 회복탄력성 패턴

6.1 Circuit Breaker

장애 서비스 호출을 차단하여 cascade failure 방지.

**3가지 상태**:

1. **Closed**: 정상, 호출 통과

2. **Open**: 장애 감지, 즉시 실패 (타임아웃 절약)

3. **Half-Open**: 일정 시간 후 시험적으로 통과

from circuitbreaker import circuit

@circuit(failure_threshold=5, recovery_timeout=60)

def call_payment_service(order):

response = requests.post('http://payment/charge', json=order)

return response.json()

**임계값 튜닝**:

- `failure_threshold`: 5-10회 (너무 낮으면 false positive)

- `recovery_timeout`: 30-60초 (서비스 회복 시간)

6.2 Retry with Exponential Backoff

def retry_with_backoff(func, max_retries=3):

for attempt in range(max_retries):

try:

return func()

except Exception as e:

if attempt == max_retries - 1:

raise

wait = (2 ** attempt) + random.uniform(0, 1) # 지수 + jitter

time.sleep(wait)

**왜 jitter가 필요한가?**: 모든 클라이언트가 동시에 재시도하면 thundering herd. Random jitter로 분산.

6.3 Bulkhead (격리)

선박의 격벽처럼, 한 부분 장애가 전체로 퍼지지 않게 격리.

서비스마다 별도 스레드 풀

payment_pool = ThreadPoolExecutor(max_workers=10)

inventory_pool = ThreadPoolExecutor(max_workers=10)

Payment 서비스가 느려져도 inventory 호출은 영향 없음.

6.4 Timeout

**모든 외부 호출에 타임아웃 필수**.

requests.get(url, timeout=(3, 10)) # connect 3s, read 10s

타임아웃 없으면 → 클라이언트 무한 대기 → 스레드 고갈 → 전체 서비스 멈춤.

7. 멱등성 (Idempotency)

7.1 왜 멱등성이 중요한가?

분산 시스템에서 메시지 중복은 **불가피**합니다:

- 네트워크 재전송

- 컨슈머 재처리

- 실패 후 retry

**같은 메시지를 여러 번 처리해도 결과가 동일**해야 합니다.

7.2 멱등성 키

def process_payment(idempotency_key, amount):

if cache.exists(f"payment:{idempotency_key}"):

return cache.get(f"payment:{idempotency_key}") # 이전 결과 반환

result = charge_card(amount)

cache.set(f"payment:{idempotency_key}", result, ttl=86400)

return result

**Stripe API**는 멱등성 키를 1급으로 지원:

POST /v1/charges HTTP/1.1

Idempotency-Key: my-unique-key-123

7.3 멱등성 패턴

| 작업 | 멱등성 |

|---|---|

| `GET` | ✅ (자연 멱등) |

| `PUT user/1 {name: "Alice"}` | ✅ (덮어쓰기) |

| `DELETE user/1` | ✅ (이미 없으면 OK) |

| `POST` | ❌ (멱등성 키 필요) |

| `INCR counter` | ❌ |

8. 분산 트레이싱

8.1 왜 필요한가?

마이크로서비스 환경에서 한 사용자 요청이 여러 서비스를 거치면, "왜 느려?"를 디버깅하기 어렵습니다. **트레이스 ID**로 전체 흐름을 추적해야 합니다.

8.2 OpenTelemetry

표준 트레이싱 프레임워크. 모든 언어 SDK 제공.

from opentelemetry import trace

tracer = trace.get_tracer(__name__)

def process_order(order):

with tracer.start_as_current_span("process_order") as span:

span.set_attribute("order.id", order.id)

with tracer.start_as_current_span("charge_payment"):

payment_service.charge(order)

with tracer.start_as_current_span("reserve_inventory"):

inventory_service.reserve(order)

**백엔드**: Jaeger, Tempo, Datadog, Honeycomb.

8.3 W3C Trace Context

서비스 간 트레이스 ID 전달의 표준:

GET /api/inventory HTTP/1.1

traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01

모든 서비스가 이 헤더를 읽고 전달하면 → 전체 요청 흐름이 단일 트레이스로 시각화.

9. 실전 패턴 조합

9.1 주문 처리 시나리오

**요구사항**:

- 주문 생성 시 재고 확인, 결제, 배송 시작

- 어떤 단계라도 실패하면 롤백

- 200ms 이내 응답

- 일일 100만 주문 처리

**아키텍처**:

1. **API Gateway**: REST 진입점, 인증, 레이트 리밋

2. **Order Service**: 주문 생성 + Outbox에 이벤트 저장

3. **Saga Orchestrator (Temporal)**: 결제 → 재고 → 배송 조율

4. **Kafka**: 이벤트 버스

5. **각 도메인 서비스**: 자기 도메인만 처리

**패턴 적용**:

- REST: 클라이언트 ↔ API Gateway

- gRPC: 내부 서비스 간

- Kafka: 비동기 이벤트

- Saga (Orchestration): 분산 트랜잭션

- Outbox + CDC: dual-write 해결

- Circuit Breaker: 외부 결제 게이트웨이 보호

- Idempotency Key: 중복 결제 방지

- OpenTelemetry: 전체 트레이싱

퀴즈

**답**: **둘 다 사용**이 정답입니다. 외부 API(클라이언트, 파트너)는 REST/JSON으로 (디버깅 쉬움, 캐시), 내부 서비스 간 통신은 gRPC로 (성능 5-10배, 강한 타입). API Gateway가 REST를 받아 내부적으로 gRPC로 변환하는 패턴이 일반적입니다. GraphQL은 다양한 클라이언트(웹/모바일)에 다양한 데이터 조합이 필요할 때 사용합니다.

**답**: 2PC는 (1) 블로킹 — 하나라도 응답 안 하면 모든 참여자가 락 유지, (2) 단일 장애점 — 코디네이터 장애 시 멈춤, (3) 확장성 부족. Saga는 큰 트랜잭션을 작은 로컬 트랜잭션으로 분해하고, 실패 시 보상 트랜잭션으로 되돌립니다. 단점은 isolation이 약하다는 것 (중간 상태 노출). Saga가 사실상 표준입니다.

**답**: **Dual-write problem** — DB에 저장하고 Kafka에도 발행해야 할 때, 둘 사이의 원자성 보장. 한쪽만 성공하면 데이터 불일치. Outbox는 DB 트랜잭션 안에서 outbox 테이블에 이벤트를 함께 저장 → 원자성 보장. 별도 프로세스(Polling 또는 CDC/Debezium)가 outbox에서 Kafka로 전달. **Debezium + CDC**가 가장 효율적입니다 (WAL 기반).

**답**: (1) **Closed** — 정상, 호출 통과. 실패가 누적되면 Open으로 전환. (2) **Open** — 장애 감지, 즉시 실패 반환 (타임아웃 절약, cascade failure 방지). (3) **Half-Open** — 일정 시간 후 시험적으로 통과. 성공하면 Closed로, 실패하면 다시 Open으로. 임계값: `failure_threshold` 5-10회, `recovery_timeout` 30-60초.

**답**: 분산 시스템에서 메시지 중복은 **불가피**합니다 (네트워크 재전송, 재처리, 재시도). 같은 결제 요청이 두 번 처리되면 사용자가 두 번 청구됩니다. 멱등성 키는 클라이언트가 생성한 고유 ID로, 서버는 같은 키의 요청은 캐시된 결과를 반환합니다. Stripe, Square 같은 결제 API가 1급으로 지원합니다 (`Idempotency-Key` 헤더).

참고 자료

- [Microservices.io](https://microservices.io/) — 패턴 카탈로그

- [Designing Data-Intensive Applications](https://dataintensive.net/) — Martin Kleppmann

- [Building Microservices](https://samnewman.io/books/building_microservices_2nd_edition/) — Sam Newman

- [Microservices Patterns](https://microservices.io/book) — Chris Richardson

- [Temporal](https://temporal.io/) — 워크플로 엔진

- [Debezium](https://debezium.io/) — CDC 도구

- [OpenTelemetry](https://opentelemetry.io/)

- [W3C Trace Context](https://www.w3.org/TR/trace-context/)

- [Apache Kafka Documentation](https://kafka.apache.org/documentation/)

- [Resilience4j](https://resilience4j.readme.io/) — Java 회복탄력성 라이브러리

- [Polly](https://github.com/App-vNext/Polly) — .NET 회복탄력성

현재 단락 (1/344)

- **동기 vs 비동기**: REST/gRPC는 결합도 높고 빠른 응답, 이벤트는 결합도 낮고 비동기. 둘 다 필요

작성 글자: 0원문 글자: 10,422작성 단락: 0/344