Skip to content

feat: Feign 내부 호출에 서킷브레이커를 건다 - #85

Merged
popeye0618 merged 1 commit into
mainfrom
feat/feign-circuit-breaker
Aug 26, 2026
Merged

popeye0618 merged 1 commit into
mainfrom
feat/feign-circuit-breaker

Conversation

@popeye0618

Copy link
Copy Markdown
Member

배경

Feign 안정화 Phase 2. 타임아웃(#78)만으로는 피호출 서비스가 죽어 있는 동안에도 모든 요청이 매번 타임아웃(1~5초)까지 기다린다. Resilience4j 서킷브레이커를 붙여 실패가 쌓이면 호출을 즉시 차단하고 503으로 응답한다.

변경 내용

서킷브레이커 (feign-resilience.yml)

  • spring-cloud-starter-circuitbreaker-resilience4j 추가, spring.cloud.openfeign.circuitbreaker.enabled: true
  • 공통 파라미터: 카운트 윈도우 10 / 최소 호출 5 / 실패율 50% / slow call 2s·80% / open 10s / half-open 3회
  • 4xx는 실패로 세지 않는다 (ignore-exceptions: FeignException$FeignClientException) — 없는 리소스 404 같은 비즈니스 에러가 브레이커를 열면 멀쩡한 서비스로 가는 호출까지 차단된다.

설계 결정 3가지

  1. 브레이커를 Feign 클라이언트(name) 단위로 묶는다 (InternalFeignConfig의 CircuitBreakerNameResolver). 기본 네이밍은 메서드 시그니처 단위라 내부 호출처럼 트래픽이 적으면 minimum-number-of-calls에 도달하지 못해 브레이커가 영영 열리지 않는다. 장애는 엔드포인트가 아니라 프로세스 단위로 온다.
  2. 스레드풀 실행을 끈다 (disable-thread-pool: true). 켜두면 Spring Cloud 기본 TimeLimiter(1s)가 read-timeout(3~5s)보다 먼저 호출을 끊고, EC2 한 대 6 JVM 전제에서 서비스마다 스레드풀이 하나씩 늘어난다. 끄면 Feign read-timeout이 유일한 시간 상한이다.
  3. 원본 예외 계약을 유지한다 (FeignCircuitBreakerExceptionUnwrapper). 서킷브레이커를 켜면 폴백 없는 호출의 모든 예외가 NoFallbackAvailableException으로 래핑되는데, 그대로 두면 기존 catch (FeignException) 분기(예: AdminUserItemServiceImpl의 404→비즈니스 에러 변환)가 컴파일 에러 없이 조용히 무력화된다. 프록시 단계에서 cause를 복원해 호출부는 도입 전과 같은 예외를 받는다.

실패 응답 (GlobalExceptionHandler, GeneralErrorCode)

  • 브레이커 open(CallNotPermittedException)과 연결 실패·타임아웃(RetryableException)을 503 + GEN-105로 내린다. 기존 FeignException 응답 패스스루는 그대로다.
  • 폴백은 붙이지 않았다(fail-fast). 현재 호출부는 전부 "그 데이터 없으면 요청이 성립하지 않는" 조회라 fallback으로 지어낼 값이 없다.

부수 정리

  • gateway·notification은 Feign을 쓰지 않으므로 새 스타터를 exclude (notification의 기존 openfeign exclude 방침과 동일).
  • 메트릭은 기존 prometheus 엔드포인트에 자동 노출된다(resilience4j_circuitbreaker_*). health indicator는 의도적으로 켜지 않았다 — 브레이커 open이 /actuator/health를 DOWN으로 만들면 헬스체크가 멀쩡한 컨테이너를 재시작시키는 2차 장애가 된다.

검증

FeignCircuitBreakerTest — 실제 HTTP 서버(JDK 내장, 루프백)를 상대로 운영 파일(feign-resilience.yml) 그대로를 물려 실증:

  1. 5xx 5회 누적 시 브레이커 open, 이후 호출은 서버에 닿지 않음
  2. 4xx 8회 연속에도 브레이커 CLOSED, 실패 집계 0
  3. read-timeout 안쪽의 1.5s 호출 성공 → 기본 TimeLimiter(1s) 미개입 증명
  4. read-timeout 초과는 RetryableException + 실패로 집계 (클라이언트별 타임아웃 오버라이드 바인딩도 함께 검증)
  5. 모든 단언이 원본 예외 타입 기준 → 언랩퍼 동작 검증

전 모듈 단위 테스트 통과(서비스 컨텍스트 부팅 테스트들이 새 설정을 물고 정상 로드됨).

하위 호환성

  • API·이벤트 스키마 변경 없음. common-module 변경이라 5개 서비스가 같이 재빌드된다 — 전체 재배포 한 번으로 롤아웃.
  • 호출부 코드 변경 0건: 언랩퍼가 기존 예외 계약을 유지하므로 각 서비스는 서킷브레이커 도입을 모른 채 동작한다. 새로 추가되는 것은 브레이커 open 시의 CallNotPermittedException(503) 뿐이다.

🤖 Generated with Claude Code

타임아웃(#78)만으로는 피호출 서비스가 죽어 있는 동안에도 모든 요청이
매번 타임아웃까지 기다린다. Resilience4j 서킷브레이커를 붙여 실패가
쌓이면 호출을 즉시 차단하고 503 으로 응답한다.

- 브레이커는 메서드가 아니라 Feign 클라이언트(name) 단위로 묶는다.
  내부 호출은 메서드별 트래픽이 적어 기본(메서드 단위) 네이밍으로는
  minimum-number-of-calls 에 도달하지 못해 브레이커가 열리지 않는다.
- 4xx 는 실패로 세지 않는다. 비즈니스 에러가 브레이커를 열면 멀쩡한
  서비스로 가는 호출까지 차단된다.
- 스레드풀 실행을 꺼서 Spring Cloud 기본 TimeLimiter(1s)가 read-timeout
  보다 먼저 호출을 끊는 것을 막는다.
- 폴백 없는 호출의 예외가 NoFallbackAvailableException 으로 래핑되는
  문제는 프록시 단계 언랩퍼로 원본 예외를 복원한다. 기존
  catch (FeignException) 분기가 그대로 동작한다.
- 브레이커 open / 타임아웃은 503(GEN-105) 으로 내린다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@popeye0618
popeye0618 merged commit b3fa9d1 into main Aug 26, 2026
3 checks passed
popeye0618 added a commit that referenced this pull request Aug 26, 2026
common-module 변경 PR(#85)의 자동 리뷰가 9분 넘게 걸렸다. CLAUDE.md 의
"서비스 경계를 넘는 변경은 하위 호환성을 특히 주의해서 본다" 지침을 따라
diff 밖의 다른 서비스 소비 코드까지 찾아 읽었기 때문으로 보인다 - diff
자체는 403줄로 크지 않았다.

자동 리뷰(claude-code-review.yml)는 diff 에 나온 코드만 보게 프롬프트로
범위를 좁히고, common-module/내부 API/Kafka 스키마 변경이 보이면 실제
분석 대신 "@claude 로 수동 리뷰를 요청하라"는 코멘트만 남기도록 한다.
cross-service 하위 호환성 분석은 claude.yml(@claude 수동 호출)의 역할로
문서화한다.

allowedTools 는 건드리지 않았다 - #79 에서 도구를 뺐다가 리뷰가 통째로
증발한 사고가 있어서, 속도 문제는 프롬프트로만 좁힌다.
@claude

claude Bot commented Aug 26, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant