개발 일기

[Resilience4j] 분산 환경에서의 장애 전파 방지(서킷 브레이커 패턴) 본문

Back-End/Spring Cloud(분산환경)

[Resilience4j] 분산 환경에서의 장애 전파 방지(서킷 브레이커 패턴)

개발 일기장 주인 2025. 4. 4. 14:36

소프트웨어 개발에서 100% 완벽한 시스템을 구축하는 것이 이상적이지만, 현실적으로는 매우 어렵다. 특히 Microservices Architecture와 같이 분산 환경에서 운영되는 서비스에서는 하나의 서버 장애가 전체 시스템으로 전파되는 문제가 발생할 수 있다.

이는 단순한 장애를 넘어, MSA를 도입한 근본적인 목적 자체를 무의미하게 만들 수 있다.

따라서 완벽함을 추구하기보다는, 장애 상황에서도 전체 시스템이 무너지지 않도록 유연하게 대응할 수 있는 구조, 즉 Fault Tolerance (내결함성)를 갖추는 것이 중요하다. 이는 개별 마이크로서비스에서 발생한 오류나 장애가 전체 서비스로 확산되지 않도록 방지하는 데 핵심적인 역할을 한다.

이러한 내결함성을 실현하는 방법 중 하나로, Circuit Breaker 패턴을 활용할 수 있다. Circuit Breaker는 장애가 반복적으로 발생하는 외부 시스템이나 마이크로서비스에 대한 호출을 차단하고, 빠르게 실패(fail-fast)하거나 대체 로직(fallback)을 실행함으로써 장애 확산을 방지하고 전체 시스템의 안정성을 확보할 수 있도록 도와준다.


💥 예외가 계속 터질 때 성능 문제가 생기는 이유

1. 스레드 점유 + 응답 지연

  • 예외가 발생하는 메서드는 결국 외부 서비스나 내부 연산을 기다리다 실패하는 거야.
  • 예를 들어, 타임아웃이 3초라면 그 동안 스레드가 대기 → 응답도 늦어짐 → 다음 요청도 줄줄이 대기.
  • 결과적으로 사용자 요청이 밀리고, 처리량이 확 줄어듦.

2. 예외 처리 자체도 비용

  • Java의 예외는 일반적인 if 조건문보다 비싼 연산이야.
    try-catch가 실행될 때 JVM은 스택 추적(stack trace)도 수집하고 로그도 남기기 때문에 CPU와 메모리 리소스를 더 많이 씀.
  • 예외가 수백, 수천 번 반복되면 GC도 더 자주 발생하고, 애플리케이션이 느려짐.

3. 의미 없는 재시도

  • 실패할 게 뻔한 외부 시스템을 계속 호출하면서 리소스를 낭비.
  • 서버뿐만 아니라 외부 시스템에도 부담을 줌.
    → 마치 "죽어있는 서버에 계속 문 두드리는" 꼴이야.

4. 시스템 전체에 영향

  • 일부 API만 문제가 생겼다고 생각하겠지만, 서버 전체 스레드 풀을 잠식할 수 있어.
  • 특히 WebFlux나 Async 환경이 아닌 이상, 동시 요청이 많아질수록 전체 시스템이 느려지거나 장애로 번질 수 있어.

🔄 Falut-tolerance

위에서 언급했던 것처럼 외부 서버 또는 타 마이크로서비스에 문제가 발생하거나 응답에 지연이 발생하는 등 장애가 발생했을 때, 전체 서비스의 흐름이 중단되지 않고 정상적인 경로로 우회하거나 대체 로직을 통해 처리할 수 있도록 하는 것이 바로 Fault-tolerance(내결함성)이다.

https://dev.gmarket.com/86
분산 시스템의 장점 중 하나는 일부 구성 요소, 프로세스, 또는 네트워크 연결에 장애가 발생하더라도 시스템이 일정 수준의 성능을 유지하며 계속 동작할 수 있다는 점이다.

✅ CircuitBreaker란?

우선 서킷 브레이커에 대해서 알아보자.

직역해보면 회로 차단기로 가정집의 누전 차단기가 화재를 막는 것과 같이

CircuitBreaker는 서비스 간의 장애가 전파되는 것을 막는 역할을 합니다.

 

CircuitBreaker는 문제가 발생한 지점을 감지하고 실패하는 요청을 계속하지 않도록 방지하며,
이를 통해 시스템의 장애 확산을 막고 장애 복구를 도와주며 유저는 불필요하게 대기하지 않게 됩니다.

 

즉, 아래 그림과 같이 Service A 가 Service B를 호출할 때, Service B가 반복적으로 실패한다면 CircuitBreaker를 Open 하여 Service B 에 대한 흐름을 차단하는 게 CircuitBreaker의 역할입니다.


⚙️ 서킷 브레이커의  세 가지 상태

1️⃣ Closed (닫힘) - 정상 상태

  • 서비스가 정상적으로 동작하는 상태.
  • 모든 요청을 원래 서비스(또는 외부 API)로 보냄.
  • 실패율이 일정 기준을 초과하면 Open(열림) 상태로 전환됨.

2️⃣ Open (열림) - 차단 상태

  • 특정 시간 동안 요청을 차단하여, 실패한 서비스에 대한 호출을 막음.
  • 장애가 확산되지 않도록 보호하는 역할.
  • 일정 시간이 지나면 Half-Open(반열림) 상태로 전환됨.

3️⃣ Half-Open (반열림) - 테스트 상태

  • 일정 시간이 지나면 일부 요청을 허용하여 서비스가 복구되었는지 테스트.(판단이 이뤄지는 상황)
  • 요청이 성공하면 Closed(닫힘) 상태로 복귀.
  • 요청이 실패하면 다시 Open(열림) 상태로 유지.
Slow Call과 Failure : CircuitBreaker에서 장애 판단 기준
Slow Call: 기준보다 오래 걸린 요청
Failure: 실패 혹은 오류 응답을 받은 요청

⚙️ 서킷 브레이커의  흐름

  1. Closed 상태에선 정상 요청 수행
  2. 실패 임계치(failureRateThreshold or slowCallRateThreshold) 도달시 Closed 에서 Open 으로 상태 변경
  3. Open 상태에서 일정 시간(waitDurationInOpenState) 소요시 Half Open 으로 상태 변경
  4. Half Open 상태에서의 요청 수행
    a. 지정한 횟수 (permittedNumberOfCallsInHalfOpenState 횟수만큼) 수행 후 성공 시 Half Open 상태에서 Closed 상태로 변경
    b. 지정한 횟수 (permittedNumberOfCallsInHalfOpenState 횟수만큼) 수행 후 실패 시 Half Open 상태에서 Open 상태로 변경

Resilience4j: Spring Boot에서의 CircuitBreaker

CircuitBreaker를 지원하는 라이브러리 종류

1. Netflix Hystrix

Netflix 에서 개발한 라이브러리로 MSA 환경에서 서비스 간 통신이 원활하지 않을 경우 각 서비스가 장애 내성과 지연 내성을 갖게 하는 라이브러리 현재는 deprecated 된 상태로 Resilience4j 사용 권장

2. Resilience4j
Netflix Hystrix로 부터 영감을 받아 개발된 Fault Tolerance Library Java 전용으로 개발된 경량 라이브러리 (Netflix Hystrix 공식 doc 에서도 Resilience4j 사용을 권장하고 있으니, 고민할 거 없이 Resilience4j를 사용했습니다.)

 

Resilience4j 공식문서

 

Introduction

Resilience4j is a lightweight fault tolerance library designed for functional programming. Resilience4j provides higher-order functions (decorators) to enhance any functional interface, lambda expression or method reference with a Circuit Breaker, Rate Lim

resilience4j.readme.io

 

⚙️ Resilience4j 의 코어 모듈

Resilience4j 의 코어 모듈은 아래와 같으며, 필요한 모듈을 선택하여 사용할 수 있다.

dependencies {
  // 1. CircuitBreaker : 장애 전파 방지 기능 제공
  implementation("io.github.resilience4j:resilience4j-circuitbreaker:${resilience4jVersion}")
  // 2. Retry : 요청 실패 시 재시도 처리 기능 제공
  implementation("io.github.resilience4j:resilience4j-retry:${resilience4jVersion}")
  // 3. RateLimiter : 제한치를 넘어서 요청을 거부하거나 Queue 생성하여 처리하는 기능 제공
  implementation("io.github.resilience4j:resilience4j-ratelimiter:${resilience4jVersion}")
  // 4. TimeLimiter : 실행 시간제한 설정 기능 제공
  implementation("io.github.resilience4j:resilience4j-timelimiter:${resilience4jVersion}")
  // 5. Bulkhead : 동시 실행 횟수 제한 기능 제공
  implementation("io.github.resilience4j:resilience4j-bulkhead:${resilience4jVersion}")
  // 6. Cache : 결과 캐싱 기능 제공
  implementation("io.github.resilience4j:resilience4j-cache:${resilience4jVersion}")
}

 


드링클리 프로젝트에 도입해야할 근거 (아직 도입X)

드링클리 프로젝트에서는 마이크로서비스 간 통신을 위해 Feign Client를 사용하고 있으며, 외부 API 또는 다른 내부 서비스에 요청을
보낼 때, 상대 서비스의 연산 지연이나 DB 부하로 인해 응답이 2~3초 이상 지연되는 경우가 발생할 수 있다.

이러한 상황에서 스프링 서버는 결과적으로 200 OK 응답을 받기 때문에, 이를 정상적인 호출로 인식하지만, 실제로는 사용자 입장에서 큰 지연을 겪게 되어 서비스 경험이 저하될 수 있다.

또한, 요청을 수신하는 서비스 측의 장애나 처리 지연으로 인해 호출 스레드가 해당 응답을 기다리는 동안 스레드가 블로킹되며, 이로 인해 시스템 전체의 리소스가 잠식되고, 나아가 성능 저하 및 장애 확산으로 이어질 수 있다.

따라서, 지속적인 실패나 응답 지연을 사전에 감지하고, 불필요한 요청을 차단하며, fallback 로직을 통해 유연하게 대응하기 위해 Resilience4j의 Circuit Breaker를 도입하게 되었다. 이를 통해 사용자 경험 및 서비스 안정성을 향상시킬 수 있다고 판단했었다.