Resilience4j로 서킷 브레이커와 장애 격리 구현
그래서 Resilience4j를 잘 쓰는 핵심은 어노테이션을 많이 붙이는 것이 아니라, 의존성별 실패 동작을 의도적으로 설계하는 데 있습니다.
라이브러리보다 실패 모드부터 봐야 합니다
구체적인 설정 전에 먼저 보호 대상 의존성을 분류해야 합니다.
- 실패가 일시적인가, 지속적인가
- 읽기 호출인가, 쓰기 호출인가
- 멱등한 작업인가
- 이 의존성이 죽으면 사용자 경험은 어떻게 바뀌는가
- 실패 시 스레드, 커넥션 풀, 큐 용량을 계속 소모하는가
이 판단 없이 Retry, Circuit Breaker, Bulkhead를 기계적으로 붙이면 오히려 장애를 키우기 쉽습니다.
Circuit Breaker는 보호 경계입니다
Circuit Breaker는 실패율과 느린 호출 비율을 추적하다가, 의존성이 충분히 unhealthy하다고 판단되면 회로를 엽니다.
실전 가치는 분명합니다.
- 이미 실패하는 경로에 자원을 계속 태우지 않기
- 큐 적체와 스레드 고갈 줄이기
- downstream이 망가졌을 때 빠르게 실패시키기
- 회복 중인 시스템에 숨 쉴 시간을 주기
중요한 것은 설정값이 의존성 특성에 맞아야 한다는 점입니다. 기본값 복붙으로는 좋은 보호 경계가 되기 어렵습니다.
Timeout이 Retry보다 먼저입니다
많은 팀이 재시도부터 넣고, 나중에야 이미 너무 오래 걸리는 요청을 계속 재시도하고 있었다는 사실을 발견합니다.
실무에서는 보통 다음 순서가 더 안전합니다.
- 허용 가능한 latency budget 정의
- timeout 또는 time limit 적용
- retry가 안전한지 판단
- circuit breaker 적용
- 필요하면 bulkhead로 자원 격리
timeout이 없으면 retry와 circuit breaker 모두 너무 늦게 반응하는 경우가 많습니다.
Retry는 도움이 될 수도, 과부하를 키울 수도 있습니다
Retry는 짧은 네트워크 장애, 일시적인 throttling, 리더 선출 같은 transient failure에는 유용합니다. 하지만 다음 경우에는 특히 조심해야 합니다.
- 비멱등 쓰기 요청
- 이미 과부하 상태인 의존성
- 오래 걸리며 희소 자원을 점유하는 작업
좋은 retry 정책은 항상 다음 질문에 답해야 합니다.
- 어떤 오류만 retry 가능한가
- 최대 몇 번까지 시도하는가
- backoff 전략은 무엇인가
- retry까지 포함한 총 latency budget은 얼마인가
이 답이 없으면 retry는 복원력이 아니라 노이즈가 됩니다.
Bulkhead가 진짜 격리를 만듭니다
Circuit Breaker는 건강 상태에 따라 빠르게 실패시키지만, 자원 소비 자체를 분리해주지는 않습니다.
Bulkhead는 다음 자원이 특정 의존성 때문에 잠식될 때 특히 중요합니다.
- servlet thread
- worker pool
- connection pool
- async execution capacity
Bulkhead가 없으면 breaker가 열리기 전까지 한 느린 연동이 서비스 전반을 늦추는 일이 충분히 발생할 수 있습니다.
Fallback은 실패 숨기기가 아니라 명시적 저하 동작이어야 합니다
Fallback은 자주 오용됩니다. 오류를 감추면 사용자 경험은 잠깐 좋아 보일 수 있지만, 이후 더 큰 데이터 문제를 만듭니다.
강한 fallback은 저하된 상태를 솔직하게 드러냅니다.
- 읽기 시나리오에서는 캐시나 stale 데이터 제공
- 추천이나 부가 정보처럼 선택적 기능은 축소
- 부분 기능만 제공하되 클라이언트에 명시적으로 전달
반대로 쓰기 작업을 성공처럼 보이게 하거나 중요한 처리를 조용히 생략하는 fallback은 위험합니다.
상태 전이와 느린 호출을 관측해야 합니다
관측 없는 복원력은 사실상 희망 사항에 가깝습니다.
최소한 다음은 봐야 합니다.
- circuit state transition
- failure rate
- slow call rate
- retry volume
- timeout rate
- bulkhead saturation
특히 절대 오류 건수보다, 완전한 장애 직전에 느려지는 패턴을 보는 것이 더 유용한 경우가 많습니다.
Spring Boot에서의 현실적인 기준선
많은 Spring Boot 서비스에서 건강한 기본선은 대략 다음과 같습니다.
- 외부 호출마다 명시적 timeout budget 존재
- retry는 멱등하거나 안전성이 검증된 연산에만 제한적으로 적용
- 불안정한 의존성은 circuit breaker로 보호
- 희소 자원은 bulkhead로 분리
- fallback은 제품적으로 허용된 경우에만 사용
이 정도가 되어야 Resilience4j가 장식용 어노테이션이 아니라 실제 보호 계층이 됩니다.
흔한 실수
다음 패턴은 실제 운영에서 자주 문제를 만듭니다.
- 비멱등 작업을 retry함
- 실패한 쓰기를 fallback으로 숨김
- 모든 의존성에 동일한 breaker 설정을 적용함
- 오류율만 보고 slow-call 추세는 보지 않음
- breaker는 걸었지만 thread pool 격리는 하지 않음
코드상으로는 복원력이 있어 보여도, 운영에서는 여전히 취약한 상태가 됩니다.
체크리스트
운영 준비가 되었다고 하려면 최소한 다음이 확인되어야 합니다.
- 외부 의존성마다 latency budget이 정의되어 있는가
- retry 규칙이 멱등성과 비즈니스 안전성과 맞는가
- 자원 격리가 필요한 경로에 bulkhead가 있는가
- fallback 동작이 명시적이고 제품적으로 승인되었는가
- 대시보드에서 breaker transition, slow call, retry, saturation을 볼 수 있는가
마무리
좋은 Resilience4j 적용은 어노테이션 개수가 아니라, 하나의 나쁜 의존성이 전체 서비스 건강을 소모하지 못하도록 실패 동작을 설계했는가에 달려 있습니다.
그것이 실전에서 말하는 장애 격리입니다.
Continue Reading
다음으로 읽기 좋은 글
CQRS + Event Sourcing 실전 구현 가이드
CQRS와 Event Sourcing을 도메인 경계와 운영 복잡성의 관점에서 설명하고, Aggregate, Event Store, Projection, Snapshot, 정합성 모델의 선택 기준을 정리합니다.
⚙️ BackendApache Kafka로 이벤트 드리븐 아키텍처 구현하기
Kafka를 단순 메시지 큐가 아니라 이벤트 기반 시스템 설계 도구로 보고, 토픽, 파티션, 이벤트 계약, 멱등성, 재처리, DLT, 운영 지표를 정리합니다.
🧪 TestSpring Boot 테스트 슬라이스: @WebMvcTest, @DataJpaTest
Spring Boot 테스트 슬라이스를 단순 어노테이션 모음이 아니라 테스트 피라미드와 실행 비용 관점에서 정리합니다. @WebMvcTest, @DataJpaTest, @JsonTest, @RestClientTest를 언제 쓰고 언제 @SpringBootTest가 더 맞는지 설명합니다.
🧪 TestREST Assured API 테스트 전략 가이드
REST Assured를 사용해 Java 기반 API를 어떻게 검증할지 정리합니다. 요청/응답 예제보다 계약 검증, 인증 흐름, 테스트 데이터, 통합 테스트 경계에 초점을 맞춘 실무형 가이드입니다.
다음 탐색