본문으로 건너뛰기

외부 API 장애 대응

발생 상황

사주 계산 기능은 외부 API에 의존하는데, 외부 서비스는 언제든 일시적 장애가 발생할 수 있습니다. 장애 상황을 별도로 처리하지 않으면 다음과 같은 연쇄 장애가 발생합니다.

  1. 외부 API가 응답하지 않음
  2. 우리 서버의 요청 스레드들이 타임아웃까지 블로킹 대기
  3. 스레드 풀이 고갈되면 사주와 전혀 무관한 회원 API, 커뮤니티 API까지 응답 불가

외부 API 하나의 장애가 서버 전체 장애로 번지는 상황을 막아야 했습니다.


해결 방법

Resilience4j Circuit BreakerSpring Retry를 조합해 외부 API 호출 레이어에 적용했습니다.

재시도 정책

일시적인 오류는 재시도로 해결될 수 있으므로, 네트워크 오류와 5xx 서버 오류에 한해서만 최대 3회 재시도합니다. 재시도 간격은 지수 백오프를 적용해 점점 늘어납니다.

1회 시도 실패 → 500ms 대기 → 2회 시도
2회 시도 실패 → 1000ms 대기 → 3회 시도
3회 시도 실패 → 예외 변환 (502 EXTERNAL_API_ERROR)

4xx 클라이언트 오류는 재시도 없이 즉시 예외로 변환합니다. 요청 자체가 잘못된 경우 재시도해도 동일한 오류가 반환되기 때문입니다.

Circuit Breaker 동작

최근 10회 중 5회 이상 실패
CLOSED ──────────────────────────────▶ OPEN
(정상 통과) (30초 차단)
↑ │
│ 3회 시험 호출 전부 성공 ▼
└────────────────────────── HALF-OPEN

실패 시 다시 OPEN
설정의미
슬라이딩 윈도우10회최근 10번 호출을 기준으로 실패율 산정
실패율 임계값50%5회 이상 실패 시 OPEN 전환
OPEN 유지 시간30초외부 서버 회복 대기 시간
HALF-OPEN 시험 호출3회회복 여부 확인, 성공 시 CLOSED 복귀

서킷이 OPEN 상태인 동안에는 외부 API에 요청 자체를 보내지 않고 클라이언트에 502를 즉시 반환합니다. 장애 중인 외부 API 대기로 스레드가 낭비되는 것을 차단합니다.


이 방식을 선택한 이유

타임아웃 설정만으로는 부족한 이유

타임아웃을 설정하면 개별 요청이 무한 대기하는 상황은 막을 수 있습니다. 하지만 장애가 지속되는 동안에는 요청이 들어올 때마다 타임아웃 시간만큼 스레드가 점유됩니다. 서킷브레이커는 OPEN 상태에서 요청 자체를 차단하므로 스레드 소모 없이 즉시 실패 응답을 반환할 수 있습니다.

재시도 대상을 5xx와 네트워크 오류로만 제한한 이유

4xx는 외부 API가 정상 동작하면서 요청 자체를 거부한 응답입니다. 요청 내용이 문제이므로 재시도해도 동일한 결과가 나옵니다. 불필요한 재시도로 오히려 외부 API에 부하만 주게 되므로 제외했습니다.

지수 백오프를 적용한 이유

일정 간격(예: 500ms마다)으로 재시도하면, 외부 서버가 과부하 상태일 때 재시도 요청이 집중되어 회복을 더 어렵게 만들 수 있습니다. 간격을 점점 늘려서 외부 서버가 회복할 시간을 확보했습니다.

Resilience4j를 선택한 이유

Spring Boot 생태계와 잘 통합되고, 어노테이션 기반으로 비즈니스 로직 코드에 영향을 주지 않으면서 적용할 수 있습니다. CB 상태를 Spring Actuator Health로 모니터링할 수 있어 운영 관측성도 확보됩니다.