어제 오늘 내일

[Spring Cloud] 외부 서버가 죽어도 내 서버는 산다? 서킷 브레이커(Resilience4j) 패턴 본문

IT/SpringBoot

[Spring Cloud] 외부 서버가 죽어도 내 서버는 산다? 서킷 브레이커(Resilience4j) 패턴

hi.anna 2026. 3. 27. 00:51

 
집에서 드라이기랑 에어컨을 동시에 켰다가 전기가 팍! 나간 적 있으신가요?
이때 두꺼비집(누전 차단기)이 내려가서 전기를 끊어주지 않았다면, 과열로 인해 집 전체에 불이 났을지도 모릅니다.
소프트웨어에서도 똑같습니다. 외부 API가 에러를 뿜어내고 있는데 계속 호출하면 내 서버까지 불이 납니다. 이때 "잠깐 연결 끊어!"라고 해주는 기술이 바로 서킷 브레이커입니다.


1. 서킷 브레이커의 3가지 상태

이 기술의 핵심은 상태(State)를 관리하는 것입니다.

  1. CLOSED (닫힘 - 정상): 전기가 통하는 상태. API 호출이 정상적으로 잘 됩니다.
  2. OPEN (열림 - 차단): 에러가 너무 많이 나서 회로를 끊어버린 상태. API 호출을 아예 안 하고 바로 에러를 뱉습니다. (Fail Fast)
  3. HALF-OPEN (반열림 - 간보기): 차단된 지 일정 시간이 지나서, "이제 좀 괜찮아졌나?" 하고 한 번 찔러보는 상태. 성공하면 다시 CLOSED로, 실패하면 다시 OPEN으로 갑니다.

2. 설정하기 (Resilience4j)

예전에는 Netflix Hystrix를 썼지만, 지금은 업데이트가 중단되었습니다. 요즘은 Resilience4j가 표준입니다.

① 의존성 추가 ()

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
    implementation 'org.springframework.boot:spring-boot-starter-aop' // AOP 필수!
}

② 설정 파일 ()

"10번 호출했는데 50% 이상 실패하면 60초 동안 차단해라!"라는 규칙을 정합니다.

resilience4j:
  circuitbreaker:
    instances:
      my-weather-api: # 서킷 브레이커 이름
        registerHealthIndicator: true
        slidingWindowType: COUNT_BASED # 횟수 기준 (또는 TIME_BASED)
        slidingWindowSize: 10 # 최근 10번 요청을 기록
        failureRateThreshold: 50 # 실패율 50% 넘으면 OPEN (차단)
        waitDurationInOpenState: 60s # 60초 동안은 호출하지 마!
        permittedNumberOfCallsInHalfOpenState: 3 # 반열림 상태에서 3번 테스트해봐

3. 실전 코드: 와 Fallback

이제 서비스 코드에 어노테이션만 붙이면 됩니다.
그리고 가장 중요한 것! 차단되었을 때 대신 실행할 '대안(Fallback)'을 마련해야 합니다.

@Service
@RequiredArgsConstructor
public class WeatherService {

    private final WeatherClient weatherClient;

    // 1. name: 설정 파일에 적은 인스턴스 이름
    // 2. fallbackMethod: 에러 나면 실행할 메서드 이름
    @CircuitBreaker(name = "my-weather-api", fallbackMethod = "getFallbackWeather")
    public String getSeoulWeather() {
        return weatherClient.getForecast("seoul"); // 외부 API 호출
    }

    // ★ Fallback 메서드 (대안)
    // 파라미터와 리턴 타입이 원본 메서드와 똑같아야 함!
    // 마지막 파라미터로 Throwable을 받을 수 있음
    private String getFallbackWeather(Throwable t) {
        log.error("날씨 API가 죽었습니다: {}", t.getMessage());
        return "맑음 (기본값)"; // 혹은 DB에 캐시된 데이터 리턴
    }
}

결과:

  1. 날씨 API가 죽어서 에러를 뱉습니다.
  2. 실패율이 50%를 넘으면 서킷 브레이커가 OPEN 됩니다.
  3. 이후 요청부터는 날씨 API를 호출조차 하지 않고, 바로 getFallbackWeather가 실행되어 "맑음 (기본값)"을 리턴합니다.
  4. 사용자는 "서버 오류" 화면 대신, 조금 부정확하더라도 날씨 정보를 볼 수 있습니다. (서비스 생존!)

4. 모니터링 (Actuator 연동)

서킷 브레이커가 열렸는지 닫혔는지 어떻게 알까요?
스프링 부트 Actuator를 통해 실시간으로 상태를 볼 수 있습니다.

  • 접속 주소: http://localhost:8080/actuator/health
{
  "status": "UP",
  "components": {
    "circuitBreakers": {
      "status": "UP",
      "details": {
        "my-weather-api": {
          "status": "CLOSED", // 현재 상태 (정상)
          "failureRate": "-1.0%",
          "bufferedCalls": 0
        }
      }
    }
  }
}

이걸 프로메테우스+그라파나에 연결하면, "서킷 브레이커 열림 알림"을 슬랙으로 받을 수도 있겠죠?


마치며

오늘의 결론입니다.

  1. 외부 API는 언제든 죽을 수 있다고 가정해야 한다.
  2. 서킷 브레이커는 외부 장애가 내 서버로 번지는 것을 막아주는 방화벽이다.
  3. Fallback(대안)을 잘 준비하면, 장애 상황에서도 최소한의 서비스는 유지할 수 있다.

이제 여러분의 서버는 주변 서버들이 다 죽어나가도 혼자 꿋꿋하게 살아남는 생존왕이 되었습니다.
다음 포스팅에서는 "사용자가 실수로 결제 버튼을 두 번 눌렀어요!" 중복 요청을 막아주는 멱등성(Idempotency) 설계와 Redis 활용법에 대해 알아보겠습니다. (돈과 관련된 중요한 문제입니다!)
도움이 되셨다면 좋아요와 댓글 부탁드립니다! 😊

반응형
Comments