CHANGHYUNAN

Developer, maker, thinker.

서버리스 설계 패턴 가이드: 상황별 판단 기준

DevArchitectureDesign Patterns

서버리스 설계 패턴 가이드: 상황별 판단 기준

서버리스 아키텍처를 설계할 때 가장 어려운 건 "어떤 패턴이 있는가"가 아닙니다. "지금 이 상황에서 어떤 패턴을 써야 하는가"입니다.

이 글은 주요 서버리스 설계 패턴들을 정리하되, 각 패턴의 판단 기준 에 집중합니다. 상황이 주어졌을 때 어떤 패턴을 꺼내야 할지 스스로 판단할 수 있는 게 목표입니다.

특정 클라우드 벤더에 종속된 이야기가 아니라, 어디서든 적용할 수 있는 설계 원칙이기 때문에 패턴 이름은 범용적인 용어를 씁니다.


1. API Gateway 패턴

클라이언트와 백엔드 사이에 단일 진입점을 두는 패턴입니다.

문제 상황

백엔드가 여러 서비스로 나뉘어 있을 때, 클라이언트가 각 서비스의 엔드포인트를 직접 알아야 하면 곤란합니다. 인증, 라우팅, 속도 제한, 요청 변환 같은 공통 관심사도 서비스마다 중복 구현하게 됩니다.

게이트웨이가 이 모든 걸 한 곳에서 처리합니다.

적절한 케이스

  • 마이크로서비스가 3개 이상이고, 클라이언트 종류(웹, 모바일, 외부 API)가 다양할 때
  • 인증/인가, 속도 제한, 요청 로깅 같은 횡단 관심사(Cross-Cutting Concerns)를 한 곳에서 관리하고 싶을 때
  • 백엔드 서비스 구조를 클라이언트에게 숨기고 싶을 때

부적절한 케이스

  • 서비스가 하나뿐이면 게이트웨이는 불필요한 레이어입니다
  • 게이트웨이에 비즈니스 로직을 넣고 싶은 충동이 들면, 그건 나쁜 시그널입니다. 게이트웨이는 라우터일 뿐, 서비스가 아닙니다

변형: BFF (Backend for Frontend)

클라이언트 유형별로 게이트웨이를 분리하는 패턴입니다. 웹용 BFF, 모바일용 BFF를 따로 두면 각 클라이언트에 최적화된 응답을 만들 수 있습니다. 클라이언트 간 요구사항 차이가 클 때 고려해볼 만합니다.


2. Event-Driven 아키텍처

서비스 간 통신을 직접 호출 대신 이벤트로 하는 패턴입니다.

문제 상황

A 서비스가 B 서비스를 직접 호출하면 둘이 강하게 결합됩니다. B가 죽으면 A도 실패합니다. B의 응답을 기다리는 동안 A는 블로킹됩니다.

이벤트 기반으로 바꾸면 A는 "이런 일이 일어났다"는 이벤트만 발행하고 자기 할 일을 끝냅니다. B가 그걸 구독해서 처리하든 말든 A는 신경 쓰지 않습니다.

핵심 구성 요소

  • 이벤트 버스 (Event Bus): 이벤트를 라우팅하는 중앙 허브. 규칙 기반으로 이벤트를 필터링하고 적절한 구독자에게 전달합니다.
  • Pub/Sub: 발행자가 토픽에 메시지를 보내면, 해당 토픽을 구독한 모든 소비자가 받습니다. 1:N 통신의 기본입니다.
  • 메시지 큐: 1:1 통신입니다. 메시지가 큐에 쌓이고, 소비자가 자기 페이스대로 꺼내 처리합니다. 생산 속도와 소비 속도를 분리할 수 있습니다.

적절한 케이스

  • 서비스 간 결합도를 낮춰야 할 때
  • 하나의 사건에 여러 서비스가 독립적으로 반응해야 할 때 (주문 완료 → 결제, 알림, 재고 업데이트)
  • 처리 순서가 중요하지 않거나, 비동기로 처리해도 되는 작업일 때
  • 생산자와 소비자의 처리 속도가 다를 때

부적절한 케이스

  • 즉각적인 응답이 필요한 동기 요청이라면 맞지 않습니다. 유저가 버튼을 누르고 결과를 기다리는 상황이 대표적입니다.
  • 서비스가 2-3개뿐이고 호출 관계가 단순할 때. 이벤트 버스를 도입하면 오히려 디버깅이 어려워집니다.
  • 이벤트 순서가 반드시 보장되어야 하는 경우에는 별도의 순서 보장 메커니즘이 필요합니다.

주의할 점

이벤트 기반 시스템에서 가장 흔한 실수는 이벤트 스톰 입니다. A 이벤트가 B를 트리거하고, B가 C를, C가 다시 A를 트리거하는 순환이 생기면 시스템이 폭주합니다. 이벤트 흐름은 반드시 시각화해서 관리해야 합니다.


3. Transactional Outbox 패턴

이벤트 발행의 신뢰성을 보장하는 패턴입니다.

문제 상황

Event-Driven 아키텍처에서 서비스가 할 일은 두 가지입니다. 비즈니스 데이터를 DB에 저장하고, 이벤트를 메시지 브로커에 발행하는 것. 문제는 이 두 작업이 원자적이지 않다는 것입니다.

주문 서비스를 생각해봅시다. 주문을 DB에 저장하고, "주문 생성됨" 이벤트를 발행해야 합니다. DB 저장은 성공했는데 이벤트 발행이 실패하면? 주문은 있는데 다른 서비스는 모릅니다. 반대로 이벤트를 먼저 발행하고 DB 저장이 실패하면? 존재하지 않는 주문에 대해 결제가 진행됩니다.

이것이 이중 쓰기(Dual Write) 문제입니다.

해결 방법

이벤트를 메시지 브로커에 직접 발행하지 않습니다. 대신 비즈니스 데이터와 같은 DB의 outbox 테이블에 이벤트를 함께 저장합니다. 같은 DB 트랜잭션이니까 원자성이 보장됩니다.

별도의 프로세스(Message Relay)가 outbox 테이블을 폴링하거나 CDC(Change Data Capture)로 변경을 감지해서 메시지 브로커에 발행합니다.

[서비스]  하나의 DB 트랜잭션 {
    비즈니스 테이블에 데이터 저장
    outbox 테이블에 이벤트 저장
}

[Message Relay]  outbox 테이블 읽기  메시지 브로커에 발행  outbox에서 삭제

적절한 케이스

  • Event-Driven 아키텍처에서 이벤트 유실이 허용되지 않을 때
  • Saga 패턴에서 각 단계의 이벤트가 반드시 발행되어야 할 때
  • DB 상태 변경과 이벤트 발행의 일관성이 중요한 모든 상황

부적절한 케이스

  • 이벤트 유실이 허용되는 경우 (로그, 통계 등 best-effort 이벤트)
  • NoSQL처럼 트랜잭션 지원이 제한적인 DB를 쓸 때. outbox 테이블과 비즈니스 데이터의 원자적 쓰기가 보장되어야 합니다

Message Relay 구현 방식

두 가지 접근이 있습니다.

Polling Publisher: outbox 테이블을 주기적으로 조회해서 미발행 이벤트를 발행합니다. 구현이 단순하지만 폴링 간격만큼의 지연이 생기고, DB 부하가 있습니다.

Transaction Log Tailing (CDC): DB의 트랜잭션 로그(binlog, WAL 등)를 구독해서 outbox 테이블의 변경을 실시간으로 감지합니다. 지연이 거의 없고 DB 부하도 적지만, 별도 인프라(Debezium 등)가 필요합니다.

서버리스 환경에서는 DynamoDB Streams가 CDC 역할을 할 수 있어서, DynamoDB를 outbox로 쓰면 자연스럽게 이 패턴을 구현할 수 있습니다.

멱등성 처리

Message Relay가 이벤트를 발행한 후 outbox에서 삭제하기 전에 실패하면 같은 이벤트가 중복 발행됩니다. 따라서 이벤트 소비자는 멱등성(idempotency)을 반드시 갖춰야 합니다. 각 이벤트에 고유 ID를 부여하고, 소비자가 이미 처리한 이벤트를 무시하는 구조가 필요합니다.


4. Fan-out / Fan-in (Scatter-Gather)

하나의 작업을 여러 작업자에게 분산하고, 결과를 다시 모으는 패턴입니다.

문제 상황

큰 작업을 순차적으로 처리하면 시간이 오래 걸립니다. 작업을 쪼개서 병렬로 처리하면 총 소요 시간은 가장 느린 작업자의 시간과 같아집니다.

적절한 케이스

  • 작업이 독립적인 하위 작업으로 나눌 수 있을 때 (이미지 여러 장 처리, 여러 API 동시 호출, 대량 데이터 배치 처리)
  • 단일 함수의 실행 시간 제한을 우회해야 할 때
  • 처리량(throughput)이 중요할 때

부적절한 케이스

  • 하위 작업 간에 의존성이 있을 때. A의 결과가 B의 입력이면 병렬화할 수 없습니다.
  • Fan-in 단계에서 모든 결과를 기다려야 하는데, 하나가 실패하면 전체가 실패하는 구조가 부담스러울 때

설계 시 고려할 점

Fan-in이 Fan-out보다 어렵습니다. "모든 작업이 끝났는지 어떻게 아는가"가 핵심 문제입니다. 접근 방법은 두 가지입니다.

  1. 카운터 기반: 분산 저장소에 완료 카운트를 기록하고, 마지막 작업자가 최종 합산 진행
  2. 오케스트레이터 기반: 중앙 조율자가 모든 작업자의 상태를 추적

서버리스 환경에서는 오케스트레이터 기반이 더 안정적입니다. 카운터 기반은 동시성 문제에 취약합니다.


5. Saga 패턴

여러 서비스에 걸친 트랜잭션을 관리하는 패턴입니다.

문제 상황

모노리스에서는 DB 트랜잭션 하나로 "전부 성공하거나 전부 롤백"을 보장합니다. 마이크로서비스에서는 각 서비스가 자기 DB를 갖고 있으니 분산 트랜잭션이 필요합니다.

2PC(Two-Phase Commit): 분산 트랜잭션의 전통적인 해법입니다. 코디네이터가 모든 참여자에게 "준비됐나?"(Prepare)를 물어보고, 전원이 OK하면 "커밋하라"(Commit)를 보냅니다. 한 명이라도 실패하면 전체 롤백입니다.

2PC는 서버리스에서 쓸 수 없습니다. Prepare부터 Commit까지 모든 참여자가 잠금(Lock)을 유지해야 하는데, 서버리스 함수는 요청이 끝나면 사라지기 때문에 이 장기 잠금(Long-Lived Lock)을 유지할 인스턴스가 없습니다.

Saga는 이걸 상쇄 트랜잭션(Compensating Transaction) 으로 풉니다. 분산 환경에서 롤백 대신, 이미 완료된 작업을 되돌리는 반대 작업(상쇄 작업/Compensation)을 새로 실행하는 방식입니다. 각 단계가 성공하면 다음 단계로 넘어가고, 중간에 실패하면 이전 단계들의 상쇄 액션을 역순으로 실행합니다. 예를 들어 "결제 완료"의 보상은 "환불 처리", "재고 차감"의 상쇄 작업은 "재고 복원"입니다.

두 가지 방식

Choreography (안무): 각 서비스가 이벤트를 발행하고, 다음 서비스가 그걸 구독해서 자기 작업을 수행합니다. 중앙 조율자가 없습니다.

왜 Choreography인가? 무용수들이 중앙 지휘자 없이, 각자 정해진 동작을 음악에 맞춰 스스로 수행하며 전체적으로 하나의 공연이 완성되는 것과 같습니다.

  • 장점: 단순하고, 서비스 간 결합도가 낮습니다
  • 단점: 전체 흐름을 파악하기 어렵습니다. 서비스가 늘어나면 이벤트 체인이 복잡해집니다

Orchestration (오케스트레이션): 중앙 조율자가 각 서비스를 순서대로 호출하고 상태를 관리합니다.

  • 장점: 전체 흐름이 한 곳에 정의되어 있어 파악이 쉽습니다
  • 단점: 조율자가 단일 장애점이 될 수 있습니다

판단 기준

기준 Choreography Orchestration
서비스 수 3개 이하 4개 이상
흐름 복잡도 선형 (A→B→C) 분기, 조건부 실행
에러 처리 단순한 보상 복잡한 보상 로직
가시성 요구 낮음 높음 (감사, 모니터링)

실무에서는 4개 이상의 서비스가 관여하는 Saga라면 거의 예외 없이 Orchestration이 낫습니다. 디버깅할 때 차이가 극명합니다.

적절한 케이스

  • 여러 서비스에 걸친 비즈니스 프로세스가 있고, 중간 실패 시 일관성을 유지해야 할 때
  • 주문 처리(결제 → 재고 차감 → 배송 생성) 같은 다단계 프로세스

부적절한 케이스

  • 단일 서비스 내에서 해결 가능한 트랜잭션이라면 불필요합니다
  • 보상 트랜잭션을 정의할 수 없는 작업이 포함된 경우. 이미 발송된 이메일이나 외부 시스템에 전달된 알림처럼 본질적으로 되돌릴 수 없는 작업은 보상이 불가능합니다.

6. CQRS (Command Query Responsibility Segregation)

읽기와 쓰기를 분리하는 패턴입니다.

문제 상황

읽기와 쓰기의 특성은 근본적으로 다릅니다. 쓰기는 유효성 검증, 비즈니스 규칙, 일관성이 중요합니다. 읽기는 속도와 유연한 조회가 중요합니다. 하나의 모델로 양쪽을 만족시키려면 어느 쪽이든 타협하게 됩니다.

CQRS는 쓰기 모델(Command)과 읽기 모델(Query)을 완전히 분리합니다. 제약 없이 자유롭게 분리가 가능하며 각각 최적화된 DB 저장소, 스키마 구조, 데이터 형태 등을 가질 수 있습니다.

적절한 케이스

  • 읽기와 쓰기의 비율이 극단적으로 다를 때 (읽기가 90% 이상)
  • 읽기에 필요한 데이터 형태가 쓰기 모델과 크게 다를 때 (여러 테이블을 조인해야 하는 복잡한 조회)
  • 읽기와 쓰기의 스케일링 요구사항이 다를 때

실제 예시: 이커머스 상품 카탈로그

상품 등록(쓰기)과 상품 검색(읽기)을 생각해보면 CQRS의 필요성이 명확해집니다.

  • 쓰기 모델: 상품 정보를 정규화된 RDB에 저장합니다. 카테고리, 가격, 재고, 옵션 등이 각각 정규화된 테이블에 들어갑니다. 데이터 무결성이 핵심입니다.
  • 읽기 모델: 검색 결과 페이지에서는 상품명, 대표 이미지, 가격, 리뷰 평점, 재고 상태를 한 번에 보여줘야 합니다. 여기에 카테고리 필터, 가격대 필터, 정렬까지. 정규화된 테이블을 매번 5-6개 조인하면 성능이 나오지 않습니다.

CQRS를 적용하면 쓰기는 RDB에, 읽기는 Elasticsearch나 DynamoDB 같은 비정규화된 저장소에 따로 유지합니다. 상품이 등록/수정되면 이벤트를 통해 읽기 모델을 비동기로 갱신합니다.

부적절한 케이스

  • 단순 CRUD 애플리케이션이라면 과합니다. CQRS는 복잡성을 크게 증가시킵니다.
  • 읽기와 쓰기 모델이 거의 같은 구조일 때도 마찬가지입니다.
  • 강한 일관성(strong consistency)이 반드시 필요하다면 신중해야 합니다. CQRS는 본질적으로 최종 일관성(eventual consistency)을 동반합니다.

Event Sourcing과의 관계

CQRS와 Event Sourcing은 자주 같이 언급되지만 별개의 패턴입니다. Event Sourcing은 상태 변경을 이벤트의 시퀀스로 저장합니다. CQRS 없이 Event Sourcing을 쓸 수 있고, 그 반대도 가능합니다.

다만 둘을 결합하면 시너지가 있습니다. 이벤트를 쓰기 모델에 저장하고, 그 이벤트를 구독해서 읽기 모델을 비동기로 갱신하는 구조입니다. 이 조합은 강력하지만 복잡도도 상당합니다. 진짜 필요한지 먼저 따져봐야 합니다.


7. Circuit Breaker 패턴

외부 서비스 호출의 실패를 감지하고, 장애가 전파되는 것을 차단하는 패턴입니다.

문제 상황

A가 B를 호출하는데 B가 응답하지 않으면, A는 타임아웃까지 기다립니다. 이 요청이 수천 개 쌓이면 A도 죽습니다. 장애가 연쇄적으로 전파되는 겁니다.

Circuit Breaker는 전기 회로의 차단기와 같은 원리입니다. 실패가 임계치를 넘으면 회로를 열어서 호출 자체를 차단합니다. 일정 시간이 지나면 일부 요청만 통과시켜 복구를 확인합니다.

세 가지 상태

  1. Closed (정상): 요청이 정상적으로 통과합니다. 실패를 카운트합니다.
  2. Open (차단): 실패가 임계치를 넘었습니다. 모든 요청을 즉시 실패 처리합니다. 외부 서비스를 호출하지 않습니다.
  3. Half-Open (테스트): 일정 시간 후 일부 요청만 통과시킵니다. 성공하면 Closed로 복귀, 실패하면 다시 Open.

적절한 케이스

  • 외부 API나 서드파티 서비스를 호출할 때
  • 장애가 전파될 가능성이 있는 동기 호출 체인이 있을때
  • 실패 시 합리적인 폴백(fallback)이 있을 때 (캐시된 데이터 반환, 기본값 사용, 큐에 넣고 나중에 재시도)

부적절한 케이스

  • 비동기 메시지 기반 통신에는 필요 없습니다. 메시지 큐 자체가 버퍼 역할을 하니까요.
  • 실패 시 어떤 폴백도 불가능한 경우, 예를 들어 결제 같은 핵심 경로에서는 재시도 + 멱등성이 더 적절합니다.

구현 알고리즘

Circuit Breaker의 상태 전이를 결정하는 데 쓰이는 대표적인 알고리즘이 있습니다.

  • 고정 카운터: 연속 N회 실패 시 Open. 단순하지만 경계값 근처에서 불안정합니다.
  • 슬라이딩 윈도우: 최근 T초 동안의 실패율이 임계치를 넘으면 Open. 시간 기반이라 더 안정적입니다.
  • 지수 백오프(Exponential Backoff): Half-Open에서 복구 시도 간격을 점진적으로 늘립니다. 장애가 길어질수록 부하를 줄이는 효과가 있습니다.

서버리스에서의 고려사항

서버리스 함수는 상태가 없습니다. Circuit Breaker의 상태(실패 카운트, 현재 상태)를 어디에 저장할 것인가가 문제입니다. 선택지는 세 가지입니다.

  1. 외부 저장소 (Redis, DynamoDB 등): 정확하지만 매 호출마다 저장소 조회 비용이 발생합니다
  2. 설정 서비스: 중앙에서 Circuit 상태를 관리하고, 함수가 주기적으로 확인합니다
  3. 미들웨어 레벨: API Gateway 단에서 처리합니다. 가장 가볍게 처리할 수 있습니다.

8. Throttling / Backpressure

시스템이 처리할 수 있는 양 이상의 부하가 들어올 때 이를 제어하는 패턴입니다.

문제 상황

서버리스는 자동으로 스케일링됩니다. 근데 그게 오히려 위험합니다. 트래픽이 폭증하면 함수 인스턴스가 수천 개로 늘어나는데, 그 뒤에 있는 DB나 외부 서비스는 그 부하를 감당하지 못합니다. 자동 스케일링이 오히려 병목을 터뜨리는 겁니다.

두 가지 접근

Throttling (유입 제어): 들어오는 요청의 양을 제한합니다. - API 레벨 속도 제한 (초당 N건) - 함수 동시 실행 수 제한 - 요청 할당량 (분/시간/일 단위)

Backpressure (배압): 소비자가 처리할 수 있는 만큼만 가져가게 합니다. - 메시지 큐의 배치 크기 조절 - 소비자의 폴링 간격 조절 - 동시 처리 수 제한

구현 알고리즘

Throttling을 구현하는 데 쓰이는 대표적인 알고리즘입니다.

  • Token Bucket: 버킷에 일정 속도로 토큰이 채워지고, 요청마다 토큰을 소모합니다. 버킷이 비면 요청이 거부됩니다. 버스트 트래픽을 일정 수준까지 허용하면서 평균 처리율을 제한할 수 있어서 가장 널리 쓰입니다.
  • Leaky Bucket: 요청이 버킷에 쌓이고, 고정된 속도로 빠져나갑니다. Token Bucket과 달리 버스트를 평탄화하므로, 다운스트림에 일정한 부하를 보장해야 할 때 적합합니다.
  • Fixed/Sliding Window Counter: 시간 구간별로 요청 수를 카운트합니다. Fixed Window는 구간 경계에서 버스트가 생길 수 있고, Sliding Window가 이를 보완합니다.

적절한 케이스

  • 다운스트림 서비스(DB, 외부 API)에 용량 한계가 있을 때
  • 비용을 예측 가능한 범위 내로 유지하고 싶을 때. 서버리스에서 무제한 스케일링 == 무제한 비용(!!)
  • 갑작스러운 트래픽 스파이크가 예상될 때

부적절한 케이스

  • 모든 요청이 손실 없이 처리되어야 한다면 Throttling보다 큐가 낫습니다. 큐가 Backpressure를 자연스럽게 제공합니다.

9. Dead Letter Queue (DLQ)

처리에 실패한 메시지를 별도로 격리하는 패턴입니다.

문제 상황

비동기 처리에서 메시지가 실패하면 보통 재시도합니다. 근데 어떤 메시지는 몇 번을 재시도해도 실패합니다. 메시지 포맷이 잘못됐거나, 참조하는 데이터가 없거나, 비즈니스 규칙에 위배되거나. 이런 메시지가 계속 재시도되면 큐가 막힙니다.

DLQ는 이 "독이 든 메시지"를 격리합니다. 정상 메시지의 처리를 방해하지 않으면서, 나중에 원인을 분석하고 수동으로 재처리할 수 있게 합니다.

적절한 케이스

  • 메시지 큐 기반의 비동기 처리가 있다면, DLQ는 거의 항상 필요합니다
  • 실패한 메시지를 잃어버리면 안 되는 경우

설계 시 고려할 점

DLQ를 설정하는 것 자체는 쉽습니다. 어려운 건 DLQ에 쌓인 메시지를 어떻게 처리할 것인가 입니다.

  • 모니터링 + 알림: DLQ에 메시지가 쌓이면 즉시 알림이 가야 합니다
  • 분석 도구: 왜 실패했는지 빠르게 파악할 수 있어야 합니다
  • 재처리 메커니즘: 원인을 수정한 후 DLQ의 메시지를 원래 큐로 다시 넣을 수 있어야 합니다

DLQ에 메시지가 쌓이는데 아무도 모르면, DLQ가 없는 것과 같습니다.


10. Strangler Fig 패턴

기존 모노리스를 점진적으로 서버리스/마이크로서비스로 전환하는 패턴입니다.

문제 상황

빅뱅 마이그레이션은 위험합니다. 기존 시스템을 한꺼번에 새 시스템으로 교체하면, 뭔가 잘못됐을 때 돌아갈 곳이 없습니다. 비즈니스는 마이그레이션 기간 동안에도 멈추지 않습니다.

Strangler Fig(교살자 무화과)는 이름 그대로입니다. 숙주 나무를 감싸며 자라다 결국 대체하는 식물처럼, 기존 시스템을 감싸면서 점진적으로 대체합니다.

동작 원리

  1. 기존 시스템 앞에 프록시(라우터)를 둡니다
  2. 새로운 기능은 새 서비스로 만듭니다
  3. 프록시가 새 기능은 새 서비스로, 나머지는 기존 시스템으로 라우팅합니다
  4. 점진적으로 기능을 이전하면서 기존 시스템의 역할을 줄여갑니다
  5. 모든 기능이 이전되면 기존 시스템을 제거합니다

적절한 케이스

  • 레거시 모노리스를 마이크로서비스로 전환할 때
  • 비즈니스 연속성이 중요해서 한 번에 전환할 수 없을 때
  • 팀이 새 아키텍처에 점진적으로 적응해야 할 때

무엇부터 떼어낼 것인가

모노리스의 모든 부분이 같은 우선순위는 아닙니다. 먼저 떼어낼 부분을 고르는 기준이 있습니다.

  1. 변경 빈도가 높은 부분: 자주 바뀌는 코드를 먼저 독립시키면 배포 속도가 빨라집니다
  2. 스케일링 요구가 다른 부분: 나머지와 스케일링 패턴이 다른 기능을 먼저 분리합니다
  3. 경계가 명확한 부분: 다른 모듈과의 의존성이 적은 기능부터 시작합니다

가장 복잡한 부분부터 떼고 싶은 유혹이 있지만, 그게 가장 위험한 전략입니다. 쉬운 것부터 하는 게 맞습니다.


상황별 패턴 선택 가이드

마지막으로, "이 상황에서 어떤 패턴?"에 대한 빠른 참조표입니다.

상황 1순위 패턴 함께 고려할 패턴
여러 서비스를 클라이언트에 노출해야 함 API Gateway BFF
하나의 이벤트에 여러 서비스가 반응해야 함 Event-Driven (Pub/Sub) Fan-out
대량 데이터를 병렬 처리해야 함 Fan-out / Fan-in Throttling
DB 변경과 이벤트 발행의 일관성이 필요함 Transactional Outbox Event-Driven, Saga
여러 서비스에 걸친 트랜잭션이 필요함 Saga Event-Driven
읽기/쓰기 부하 비율이 극단적으로 다름 CQRS Event Sourcing
외부 서비스 장애가 전파될 수 있음 Circuit Breaker DLQ, 재시도
자동 스케일링이 다운스트림을 터뜨림 Throttling / Backpressure 큐 기반 처리
비동기 처리에서 실패 메시지 관리 DLQ 모니터링 + 알림
모노리스를 점진적으로 전환해야 함 Strangler Fig API Gateway

패턴은 조합해서 씁니다

현실의 시스템은 하나의 패턴으로 만들어지지 않습니다. 주문 시스템을 예로 들면 이런 조합이 됩니다:

  • API Gateway로 진입점을 통합하고
  • Event-Driven으로 서비스 간 통신을 비동기화하고
  • Transactional Outbox로 DB 저장과 이벤트 발행의 원자성을 보장하고
  • Saga (Orchestration)로 주문→결제→재고→배송 트랜잭션을 관리하고
  • Circuit Breaker로 외부 결제 서비스 장애를 차단하고
  • DLQ로 실패한 주문을 격리하고
  • Throttling으로 DB 커넥션 풀 초과를 방지합니다

패턴을 개별적으로 아는 것도 중요하지만, 어떤 패턴을 어떤 조합으로 적용할지 판단하는 게 설계입니다. 그 판단의 기준은 항상 같습니다. 지금 이 상황에서 가장 큰 문제가 무엇인가. 그 문제를 푸는 패턴부터 적용하면 됩니다.


참고 자료

← Back to posts