티스토리 뷰
백엔드에서 복원력 패턴은 언제부터 넣어야 할까? Timeout, Retry, Circuit Breaker, Bulkhead를 FastAPI · Spring Boot · Node.js로 이해하기
octo54 2026. 6. 16. 11:19백엔드에서 복원력 패턴은 언제부터 넣어야 할까? Timeout, Retry, Circuit Breaker, Bulkhead를 FastAPI · Spring Boot · Node.js로 이해하기
한 줄 요약
외부 API 호출이나 느린 작업이 조금만 늘어나면, 백엔드는 “정상 기능 구현”만으로는 버티기 어려워집니다. 이때 제일 먼저 필요한 건 timeout, 그다음은 조건 있는 retry, 그리고 장애 전파를 막는 circuit breaker, 마지막으로 자원 고갈을 막는 bulkhead입니다. HTTPX는 기본적으로 네트워크 비활성 5초 timeout을 강제하고, Resilience4j는 CircuitBreaker, Retry, Bulkhead, TimeLimiter를 공식 제공하며, Spring Cloud CircuitBreaker는 Resilience4j와 Spring Retry 기반 구현을 제공합니다. (HTTPX)
이 글에서 다루는 내용
이번 글에서는 백엔드 복원력 패턴이 왜 필요한지, 어떤 순서로 도입하면 좋은지, 그리고 FastAPI · Spring Boot · Node.js에서 어디까지가 현실적인 시작점인지 정리합니다. 특히 검색에 잘 걸리도록 질문형 제목, 첫 문단 정답 요약, 개념 정의, 실행 코드, FAQ 흐름으로 구성했습니다.
복원력 패턴이란?
복원력 패턴은 외부 장애가 우리 서비스 전체를 같이 무너뜨리지 않게 막는 방법입니다.
예를 들어 이런 상황을 떠올리면 쉽습니다.
- 결제 API가 갑자기 8초씩 느려짐
- 문자 발송 API가 503을 자꾸 반환
- 추천 시스템 API가 순간적으로 에러를 냄
- 어떤 요청이 너무 많이 몰려서 스레드나 커넥션 풀이 바닥남
이때 아무 장치가 없으면 이런 일이 생깁니다.
- 요청이 끝없이 대기함
- 같은 요청을 무턱대고 재시도함
- 느린 외부 API 때문에 우리 서버 스레드도 같이 막힘
- 일부 기능 장애가 전체 장애로 번짐
그래서 복원력 패턴은 “장애가 나지 않게”가 아니라,
장애가 나도 전체로 번지지 않게 하는 데 가깝습니다.
어떤 순서로 도입해야 할까
저는 이 순서를 제일 추천합니다.
1. Timeout
가장 먼저 넣어야 합니다.
HTTPX는 기본적으로 모든 네트워크 작업에 reasonable timeout을 적용하고, 기본값은 네트워크 비활성 5초라고 설명합니다. timeout이 없으면 요청이 영원히 매달릴 수 있습니다. (HTTPX)
2. Retry
모든 실패를 재시도하면 안 됩니다.
HTTPX transport 수준 재시도는 ConnectError와 ConnectTimeout에 한정된다고 설명하고, 더 일반적인 retry는 별도 정책 도구를 고려하라고 안내합니다. (HTTPX)
3. Circuit Breaker
계속 실패하는 외부 의존성에 요청을 무한정 보내지 않게 막습니다.
Resilience4j는 CircuitBreaker가 count-based 또는 time-based sliding window를 사용해 최근 호출 결과를 집계한다고 설명합니다. (resilience4j)
4. Bulkhead
느린 외부 시스템이 우리 전체 자원을 잡아먹지 못하게 분리합니다.
Spring Cloud CircuitBreaker는 Resilience4j bulkhead도 함께 지원한다고 설명합니다. (Home)
즉 아주 짧게 말하면 이렇습니다.
무조건 timeout부터, retry는 신중하게, circuit breaker로 장애 전파 차단, bulkhead로 자원 보호.
Timeout은 왜 제일 먼저여야 할까
이건 진짜 기본기입니다.
외부 API 호출에 timeout이 없으면, 상대 서비스가 멈췄을 때 우리도 같이 멈춥니다.
HTTPX는 기본적으로 timeout을 적용하고, timeout 예외를 TimeoutException 계열로 구분합니다. ConnectTimeout, ReadTimeout, WriteTimeout, PoolTimeout 같은 세부 예외도 공식 문서에 나와 있습니다. (HTTPX)
이게 왜 중요하냐면, timeout이 있어야 비로소 이런 판단이 가능해지거든요.
- 이건 연결 자체가 안 되는 문제인가
- 읽는 중이 느린 문제인가
- 커넥션 풀을 못 구한 건가
- 이 실패는 retry 해도 되나
timeout이 없으면 그냥 “느리다”로 끝납니다.
운영에서는 그게 제일 무섭습니다.
Retry는 왜 무작정 넣으면 안 될까
여기서 진짜 많이 실수합니다.
재시도는 장애 복원에 도움이 되지만, 잘못 쓰면 오히려 더 큰 장애를 만듭니다.
예를 들면 이런 경우죠.
- 이미 느린 외부 API에 요청을 더 많이 보냄
- 결제 생성 POST를 두 번 보냄
- 같은 주문 후처리가 중복 실행됨
HTTPX는 transport 재시도가 연결 관련 오류에 한정된다고 분명히 설명하고, 더 폭넓은 retry는 tenacity 같은 별도 도구를 보라고 안내합니다. Resilience4j Retry는 RetryRegistry를 통해 retry 인스턴스를 관리할 수 있고, Spring Boot에서는 application.yml로도 설정할 수 있다고 설명합니다. (HTTPX)
제 기준은 이렇습니다.
- GET은 비교적 retry 친화적
- POST는 멱등성 보장 없으면 매우 조심
- 503, 일시적 네트워크 오류는 retry 후보
- 검증 오류, 4xx는 보통 retry 대상 아님
Circuit Breaker는 무슨 문제를 푸는 걸까
Circuit Breaker는 말 그대로 계속 실패하는 외부 시스템에 요청을 당분간 차단하는 장치입니다.
Resilience4j 문서는 CircuitBreaker가 sliding window로 최근 호출 결과를 집계하고, 상태를 기반으로 호출 허용 여부를 제어한다고 설명합니다. count-based와 time-based 두 가지 window를 쓸 수 있고요. (resilience4j)
쉽게 말하면 이런 흐름입니다.
- 외부 API가 계속 실패
- 일정 기준 넘으면 circuit open
- 잠시 동안은 아예 호출 안 함
- 나중에 일부만 시험적으로 허용
- 회복되면 다시 닫음
이걸 왜 쓰냐면,
이미 죽어가는 외부 시스템에 계속 호출을 보내는 건 우리도 같이 죽자는 얘기랑 비슷하기 때문입니다.
Bulkhead는 왜 생각보다 중요할까
Bulkhead는 제일 늦게 배우는 경우가 많은데, 막상 운영에서는 꽤 중요합니다.
핵심은 이겁니다.
특정 외부 의존성이 느려져도, 우리 전체 자원이 같이 잠식되지 않게 분리한다.
예를 들어 문자 발송 API가 느려졌다고 해볼게요.
그 API 호출 때문에 스레드풀이나 커넥션이 전부 묶이면, 문자 기능만 느린 게 아니라 전체 서비스가 느려질 수 있습니다.
Spring Cloud CircuitBreaker는 Resilience4j bulkhead를 지원한다고 설명하고, Resilience4j도 Bulkhead와 ThreadPoolBulkhead 메트릭을 Micrometer에 연결할 수 있다고 설명합니다. (Home)
즉 bulkhead는 “장애를 막는 기술”이라기보다,
장애 범위를 가두는 기술에 가깝습니다.
FastAPI에서는 어디까지가 현실적인 시작점일까
FastAPI에서는 보통 HTTPX 기반 외부 호출이 많기 때문에, 시작점은 이렇습니다.
- HTTPX timeout 명시
- 연결 계열 오류만 제한적 retry
- 실패 누적 시 간단한 circuit 상태 관리
- 중요한 외부 API는 semaphore나 별도 풀로 제한
HTTPX는 async 지원을 공식 제공하고, async 웹 프레임워크라면 async client를 쓰는 게 맞다고 설명합니다. 또 strict timeout이 기본이고, retry는 transport 계층에서 제한적으로만 제공된다고 안내합니다. (HTTPX)
FastAPI 예제: timeout + 제한적 retry
import httpx
from fastapi import FastAPI, HTTPException
app = FastAPI()
transport = httpx.HTTPTransport(retries=2)
client = httpx.Client(
transport=transport,
timeout=httpx.Timeout(3.0),
)
@app.get("/api/weather")
def get_weather():
try:
response = client.get("https://example.com/weather")
response.raise_for_status()
return response.json()
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="외부 API timeout")
except httpx.HTTPError:
raise HTTPException(status_code=502, detail="외부 API 호출 실패")
이 코드는 단순하지만 꽤 중요한 감각을 담고 있습니다.
- timeout이 있다
- retry는 무한이 아니다
- timeout과 일반 HTTP 에러를 다르게 본다
FastAPI 예제: 아주 단순한 circuit breaker 감각
import time
import httpx
from fastapi import FastAPI, HTTPException
app = FastAPI()
client = httpx.Client(timeout=httpx.Timeout(2.0))
failure_count = 0
opened_until = 0.0
@app.get("/api/payment-check")
def payment_check():
global failure_count, opened_until
now = time.time()
if now < opened_until:
raise HTTPException(status_code=503, detail="circuit open")
try:
response = client.get("https://example.com/payment/health")
response.raise_for_status()
failure_count = 0
return response.json()
except Exception:
failure_count += 1
if failure_count >= 3:
opened_until = time.time() + 10
raise HTTPException(status_code=502, detail="payment provider error")
실서비스라면 이보다 더 정교한 라이브러리나 상태 저장이 필요하지만,
이 예제로도 “연속 실패 시 잠시 차단한다”는 감각은 분명하게 잡힙니다.
Spring Boot에서는 왜 이 주제가 특히 잘 맞을까
Spring 진영은 복원력 패턴을 정말 체계적으로 가져가기 좋습니다.
Spring Cloud CircuitBreaker는 Resilience4j와 Spring Retry 기반 구현을 제공하고, Resilience4j는 CircuitBreaker, Retry, RateLimiter, Bulkhead, TimeLimiter를 Spring Boot application.yml로 설정할 수 있다고 설명합니다. 또 CircuitBreaker/TimeLimiter 설정 우선순위도 문서화돼 있습니다. (Home)
Spring Boot 예제: Resilience4j 의존성
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
}
application.yml 예제
resilience4j:
circuitbreaker:
instances:
paymentApi:
slidingWindowType: COUNT_BASED
slidingWindowSize: 10
minimumNumberOfCalls: 5
failureRateThreshold: 50
waitDurationInOpenState: 10s
retry:
instances:
paymentApi:
maxAttempts: 3
waitDuration: 500ms
timelimiter:
instances:
paymentApi:
timeoutDuration: 2s
bulkhead:
instances:
paymentApi:
maxConcurrentCalls: 5
이런 식 설정은 Resilience4j Spring Boot 설정 문서와 Spring Cloud CircuitBreaker 속성 구성 문서 흐름에 맞습니다. (resilience4j)
Spring Boot 예제: 서비스 코드
package com.example.demo;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
@Service
public class PaymentClientService {
private final RestClient restClient = RestClient.builder()
.baseUrl("https://example.com")
.build();
@Retry(name = "paymentApi")
@CircuitBreaker(name = "paymentApi", fallbackMethod = "fallback")
@Bulkhead(name = "paymentApi")
public String checkPayment() {
return restClient.get()
.uri("/payment/health")
.retrieve()
.body(String.class);
}
public String fallback(Throwable t) {
return "payment service unavailable";
}
}
이 코드가 좋은 이유는 “복원력 정책”이 비즈니스 코드에 완전히 섞이지 않는다는 점입니다.
즉 서비스는 여전히 외부 API를 호출하고, 실패 제어는 설정과 annotation 계층이 맡습니다.
그리고 Resilience4j는 Micrometer와 연동되는 메트릭도 공식 제공해서,
CircuitBreaker나 Bulkhead 상태를 메트릭으로 관측할 수도 있습니다. (resilience4j)
Node.js에서는 어떻게 시작하는 게 현실적일까
Node.js는 프레임워크가 복원력 패턴을 강하게 잡아주진 않아서,
오히려 더 의식적으로 나눠야 합니다.
- fetch 또는 HTTP client timeout
- 실패 유형에 따른 retry
- circuit 상태를 메모리/Redis 등으로 관리
- 특정 외부 API 동시 호출 수 제한
Node.js는 AbortSignal.timeout()을 공식 제공하고, abort 시 관련 오류가 날 수 있다고 설명합니다. 이건 timeout 시작점으로 정말 좋습니다. (HTTPX)
Node.js 예제: timeout + retry
async function callExternalApi() {
let lastError;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
const response = await fetch("https://example.com/payment/health", {
signal: AbortSignal.timeout(2000),
});
if (!response.ok) {
throw new Error(`upstream status=${response.status}`);
}
return await response.json();
} catch (error) {
lastError = error;
if (attempt < 3) {
await new Promise((resolve) => setTimeout(resolve, 300 * attempt));
}
}
}
throw lastError;
}
이 코드는 단순하지만 꽤 실전적입니다.
- timeout이 있다
- 무한 retry가 아니다
- backoff가 있다
Node.js 예제: 매우 단순한 circuit breaker 감각
let failureCount = 0;
let openedUntil = 0;
async function callPaymentApi() {
const now = Date.now();
if (now < openedUntil) {
throw new Error("circuit open");
}
try {
const response = await fetch("https://example.com/payment/health", {
signal: AbortSignal.timeout(2000),
});
if (!response.ok) {
throw new Error(`upstream status=${response.status}`);
}
failureCount = 0;
return await response.json();
} catch (error) {
failureCount += 1;
if (failureCount >= 3) {
openedUntil = Date.now() + 10000;
}
throw error;
}
}
마찬가지로 이건 교육용 최소 예제지만,
“계속 실패하면 잠시 차단한다”는 circuit breaker 핵심은 잘 보여줍니다.
timeout, retry, circuit breaker, bulkhead를 한 번에 넣어야 할까
아니요.
이건 진짜 아니에요.
제가 추천하는 순서는 다시 이겁니다.
1단계
timeout
2단계
retry를 아주 제한적으로
3단계
circuit breaker
4단계
bulkhead
Resilience4j가 제공하는 기능이 많다고 해서, 처음부터 다 붙이는 게 좋은 건 아닙니다.
오히려 너무 일찍 다 넣으면, 왜 실패하고 왜 차단됐는지 팀 전체가 이해를 못 하는 경우도 많아요.
fallback은 언제 써야 할까
fallback도 꽤 자주 오해합니다.
fallback은 “장애를 감춘다”가 아니라,
핵심 기능을 완전히 죽이지 않기 위한 대체 응답에 가깝습니다.
예를 들면 이런 건 fallback이 괜찮습니다.
- 추천 상품 API 실패 → 빈 추천 목록
- 외부 환율 API 실패 → 마지막 캐시값
- 통계 API 실패 → “일시적으로 집계 불가”
반면 이런 건 fallback을 신중히 봐야 합니다.
- 결제 승인
- 재고 차감
- 주문 확정
즉 business critical 영역은 fallback으로 덮기보다 실패를 명확히 드러내는 게 맞을 때가 많습니다.
모니터링은 왜 같이 붙여야 할까
복원력 패턴은 붙이는 것보다 제대로 동작하는지 보는 것이 더 중요합니다.
예를 들어 이런 걸 봐야 하죠.
- timeout이 얼마나 자주 나는지
- retry가 얼마나 발생하는지
- circuit open 상태가 얼마나 유지되는지
- bulkhead 거절이 얼마나 생기는지
Resilience4j는 Micrometer 연동 메트릭을 공식 제공하고, Spring Boot는 모니터링과 연결하기 쉽습니다. 이건 Spring 진영의 큰 장점이에요. (resilience4j)
즉 복원력 패턴은 “코드에 붙이고 끝”이 아니라,
관측 가능한 형태로 운영에 올리는 것까지가 한 세트입니다.
실무에서 자주 하는 실수
1. timeout 없이 retry부터 넣는다
이건 진짜 안 좋습니다.
timeout이 없으면 retry 간격도, 실패 판단도 모호해집니다. HTTPX가 strict timeout을 기본 제공하는 이유를 떠올리면 좋습니다. (HTTPX)
2. 모든 에러를 retry한다
검증 오류, 4xx, 비즈니스 오류까지 retry하면 오히려 시스템을 더 괴롭힙니다.
3. circuit breaker를 너무 공격적으로 열어버린다
설정이 너무 민감하면 잠깐 흔들린 것도 곧바로 차단해버릴 수 있습니다. Resilience4j가 sliding window와 minimum number of calls를 따로 두는 이유가 있습니다. (resilience4j)
4. bulkhead 없이 중요한 외부 API를 다 같은 자원으로 호출한다
이러면 한 외부 API 장애가 전체를 같이 끌어내릴 수 있습니다.
5. fallback으로 모든 걸 덮는다
이건 장애 은폐가 될 수 있습니다.
특히 결제, 재고, 주문 확정은 실패를 명확히 다루는 게 낫습니다.
FAQ
Q. 복원력 패턴은 마이크로서비스에서만 필요한가요?
아닙니다. 외부 API 호출이 조금이라도 있으면 필요합니다. 결제, 문자, 메일, 지도, AI API만 붙어도 이미 복원력 이슈는 생깁니다. 로그와 메트릭만으로는 부족한 순간이 꽤 빨리 옵니다.
Q. FastAPI에서는 Resilience4j 같은 표준 라이브러리가 없나요?
Spring만큼 강한 표준 세트가 있는 편은 아닙니다. 그래서 HTTPX timeout, 제한적 retry, 간단한 circuit/bulkhead 패턴을 서비스 계층에서 분리해서 설계하는 쪽이 현실적입니다. HTTPX는 timeout과 제한적 retry 계층을 공식 지원합니다. (HTTPX)
Q. Spring Boot에서는 어떤 조합이 가장 무난한가요?
Spring Cloud CircuitBreaker + Resilience4j 조합이 가장 정석적입니다. CircuitBreaker, Retry, Bulkhead, TimeLimiter를 설정 기반으로 관리하기 쉽고, Micrometer 메트릭 연동도 좋습니다. (Home)
Q. Node.js에서는 라이브러리 없이도 시작할 수 있나요?
네. AbortSignal.timeout() 기반 timeout, 수동 retry, 간단한 circuit 상태 관리만으로도 충분히 출발할 수 있습니다. 다만 서비스가 커지면 정책과 상태 관리를 모듈로 더 정리하는 편이 좋습니다. (HTTPX)
Q. Retry는 몇 번이 적당한가요?
정답은 없지만, 보통 2~3회 정도의 제한적 retry + 짧은 backoff부터 시작하는 경우가 많습니다. 중요한 건 횟수보다 어떤 실패만 retry할지입니다.
핵심 요약
- 복원력 패턴은 외부 장애가 우리 전체를 같이 무너뜨리지 않게 하는 방법입니다.
- 가장 먼저 필요한 건 timeout 입니다. HTTPX도 기본 timeout을 강제합니다. (HTTPX)
- retry는 무조건 좋지 않습니다. 연결 계열·일시적 오류처럼 retry 가능한 실패만 골라야 합니다. (HTTPX)
- Circuit Breaker는 계속 실패하는 외부 의존성에 요청을 잠시 차단합니다. Resilience4j는 sliding window 기반으로 이를 구현합니다. (resilience4j)
- Bulkhead는 특정 외부 API 장애가 전체 자원을 같이 잠식하지 못하게 분리합니다. (Home)
- 검색에 잘 걸리는 개발 글은 질문형 제목, 첫 문단 정답, 명확한 정의 문장, 실행 코드, FAQ 구조가 강합니다.
출처
- HTTPX 공식 문서 — Timeouts. (HTTPX)
- HTTPX 공식 문서 — QuickStart / default timeout. (HTTPX)
- HTTPX 공식 문서 — Transports / connection retries. (HTTPX)
- HTTPX 공식 문서 — Exceptions. (HTTPX)
- HTTPX 공식 문서 — Async Support. (HTTPX)
- Resilience4j 공식 문서 — CircuitBreaker. (resilience4j)
- Resilience4j 공식 문서 — Retry. (resilience4j)
- Resilience4j 공식 문서 — Spring Boot configuration. (resilience4j)
- Spring Cloud CircuitBreaker 공식 문서. (Home)
- Spring Cloud CircuitBreaker — properties configuration. (Home)
- Resilience4j 공식 문서 — Micrometer metrics. (resilience4j)
백엔드개발, FastAPI, SpringBoot, Nodejs, Express, Timeout, Retry, CircuitBreaker, Bulkhead, 백엔드시리즈
'study > 백엔드' 카테고리의 다른 글
- Total
- Today
- Yesterday
- Express
- llm
- 웹개발
- DevOps
- JAX
- SpringBoot
- 개발블로그
- SEO최적화
- nextJS
- flax
- Prisma
- JWT
- fastapi
- REACT
- 백엔드개발
- Next.js
- seo 최적화 10개
- 쿠버네티스
- 딥러닝
- LangChain
- NestJS
- 생성형AI
- kotlin
- node.js
- nodejs
- rag
- 주니어개발자
- PostgreSQL
- Python
- CI/CD
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
