🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Resilience4j 서킷 브레이커 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 22. · 2 Views

Resilience4j 서킷 브레이커 완벽 가이드

마이크로서비스 환경에서 장애 전파를 막는 Resilience4j 서킷 브레이커의 동작 원리와 실전 활용법을 다룹니다. 초급 개발자도 쉽게 이해할 수 있도록 실무 상황과 비유를 통해 설명합니다.


목차

  1. Resilience4j 소개
  2. 서킷 브레이커 상태
  3. CLOSED, OPEN, HALF_OPEN
  4. @CircuitBreaker 어노테이션
  5. 설정 파라미터
  6. 상태 전이 모니터링

1. Resilience4j 소개

김개발 씨는 이커머스 회사에 입사한 지 2개월 된 주니어 백엔드 개발자입니다. 어느 날 새벽, 결제 서비스가 다운되면서 주문 서비스까지 먹통이 되는 대형 장애가 발생했습니다.

박시니어 씨가 급히 서킷 브레이커를 적용하여 장애를 복구했고, 김개발 씨는 "서킷 브레이커가 정확히 뭐죠?"라고 물었습니다.

Resilience4j는 마이크로서비스 환경에서 장애 전파를 막기 위한 경량 장애 허용 라이브러리입니다. 마치 전기 차단기가 과부하 시 자동으로 전기를 차단하듯이, 서킷 브레이커는 외부 서비스 장애 시 호출을 차단하여 시스템을 보호합니다.

Netflix Hystrix의 후속으로, Java 8의 함수형 프로그래밍을 적극 활용하여 더 가볍고 유연합니다.

다음 코드를 살펴봅시다.

// build.gradle에 의존성 추가
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.1.0'
implementation 'org.springframework.boot:spring-boot-starter-aop'

// 서킷 브레이커를 적용한 서비스 메서드
@Service
public class PaymentService {

    @CircuitBreaker(name = "paymentService", fallbackMethod = "fallbackPayment")
    public PaymentResponse processPayment(PaymentRequest request) {
        // 외부 결제 API 호출 - 실패 가능성 있음
        return externalPaymentApi.charge(request);
    }

    // 서킷이 열렸을 때 실행되는 대체 메서드
    private PaymentResponse fallbackPayment(PaymentRequest request, Exception ex) {
        return PaymentResponse.pending("결제 서비스 일시 중단");
    }
}

김개발 씨는 출근하자마자 슬랙에 올라온 장애 보고서를 읽었습니다. "결제 서비스 다운 → 주문 서비스 응답 지연 → 전체 시스템 마비".

단 하나의 서비스 장애가 전체 시스템을 무너뜨린 것입니다. 박시니어 씨가 커피를 들고 다가왔습니다.

"어젯밤 고생 많았죠? 이게 바로 마이크로서비스의 양날의 검입니다.

서비스는 분리되어 있지만, 서로 의존하고 있어서 한 곳이 무너지면 도미노처럼 쓰러지죠." 그렇다면 Resilience4j란 정확히 무엇일까요? 쉽게 비유하자면, Resilience4j는 마치 가정집 전기 차단기와 같습니다.

과부하가 걸리거나 합선이 발생하면 자동으로 전기를 차단하여 화재를 막습니다. 전체 집이 타버리는 것보다는 한 방의 전기만 차단하는 것이 낫죠.

Resilience4j도 외부 서비스에 문제가 생기면 자동으로 호출을 차단하여 장애가 퍼지는 것을 막습니다. 마이크로서비스가 없던 시절, 그러니까 모놀리식 아키텍처 시대에는 어땠을까요?

모든 기능이 하나의 거대한 애플리케이션 안에 있었습니다. 한 기능이 느려지면 전체가 느려졌고, 한 부분에 버그가 있으면 전체를 재배포해야 했습니다.

하지만 적어도 네트워크 호출 실패나 타임아웃 같은 문제는 덜했습니다. 마이크로서비스 시대가 오면서 상황이 달라졌습니다.

주문 서비스는 결제 서비스를 호출하고, 결제 서비스는 은행 API를 호출합니다. 재고 서비스, 알림 서비스, 배송 서비스까지 수십 개의 서비스가 서로 의존합니다.

이 중 하나만 느려져도 전체 시스템이 연쇄적으로 무너질 수 있습니다. 바로 이런 문제를 해결하기 위해 Resilience4j가 등장했습니다.

Resilience4j를 사용하면 장애 격리가 가능해집니다. 결제 서비스가 다운되어도 주문 서비스는 대체 응답을 반환하며 계속 동작합니다.

또한 자동 복구 시도도 얻을 수 있습니다. 일정 시간 후 자동으로 외부 서비스 호출을 재시도하여 정상화 여부를 확인합니다.

무엇보다 리소스 낭비 방지라는 큰 이점이 있습니다. 이미 실패할 것이 뻔한 호출을 계속 시도하지 않으므로 스레드와 메모리를 아낄 수 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 의존성 추가 부분을 보면 Spring Boot 3와 호환되는 Resilience4j를 사용함을 알 수 있습니다.

AOP 의존성도 필요한데, 어노테이션 기반으로 동작하기 때문입니다. 다음으로 @CircuitBreaker 어노테이션이 핵심입니다.

name 속성으로 설정 파일의 어느 설정을 사용할지 지정하고, fallbackMethod로 장애 시 실행할 메서드를 정의합니다. processPayment 메서드는 실제 외부 결제 API를 호출합니다.

이 호출이 반복적으로 실패하면 서킷 브레이커가 작동합니다. fallbackPayment 메서드는 서킷이 열렸을 때 실행되는 대체 로직입니다.

예외를 받아서 적절한 대체 응답을 반환합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 쿠팡 같은 대형 이커머스를 개발한다고 가정해봅시다. 블랙프라이데이 같은 대규모 이벤트 때 결제 서비스가 트래픽을 감당하지 못하면 어떻게 될까요?

Resilience4j를 적용하지 않으면 주문 서비스의 모든 스레드가 결제 응답을 기다리며 블로킹되고, 결국 전체 시스템이 먹통이 됩니다. 하지만 서킷 브레이커를 적용하면 "죄송합니다.

결제 서비스가 일시적으로 혼잡합니다. 잠시 후 다시 시도해주세요"라는 응답을 즉시 반환하며 시스템을 보호할 수 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 메서드에 무분별하게 서킷 브레이커를 적용하는 것입니다.

내부 메서드 호출이나 데이터베이스 조회처럼 빠르고 안정적인 작업에까지 적용하면 오히려 성능이 떨어집니다. 서킷 브레이커는 불안정한 외부 API 호출에만 선택적으로 적용해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.

"아, 그래서 어젯밤에 서킷 브레이커를 켜자마자 시스템이 살아난 거군요!" Resilience4j를 제대로 이해하면 마이크로서비스 환경에서 더 안정적인 시스템을 구축할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - Netflix Hystrix가 2018년 유지보수 모드로 전환되면서 Resilience4j가 사실상 표준이 되었습니다

  • Spring Cloud Circuit Breaker를 사용하면 Resilience4j를 더 쉽게 통합할 수 있습니다
  • 로컬 개발 환경에서는 Resilience4j Actuator 엔드포인트로 서킷 상태를 실시간 모니터링하세요

2. 서킷 브레이커 상태

김개발 씨가 모니터링 대시보드를 보다가 이상한 점을 발견했습니다. 결제 서비스의 서킷 브레이커 상태가 CLOSED, OPEN, HALF_OPEN을 오가며 계속 바뀌고 있었습니다.

"이게 정상인가요? 왜 계속 바뀌는 거죠?" 박시니어 씨가 웃으며 "그게 바로 서킷 브레이커의 핵심 동작 방식이에요"라고 답했습니다.

서킷 브레이커는 세 가지 상태를 가지며, 상황에 따라 자동으로 전환됩니다. CLOSED 상태는 정상 작동 중이며 모든 요청을 통과시킵니다.

OPEN 상태는 장애 감지 후 모든 요청을 즉시 차단합니다. HALF_OPEN 상태는 복구 여부를 테스트하기 위해 일부 요청만 허용합니다.

이 상태 전이가 자동으로 일어나며 시스템을 보호합니다.

다음 코드를 살펴봅시다.

# application.yml - 서킷 브레이커 상태 전환 설정
resilience4j:
  circuitbreaker:
    instances:
      paymentService:
        # CLOSED -> OPEN: 10개 중 50% 실패 시
        slidingWindowSize: 10
        failureRateThreshold: 50

        # OPEN 상태 유지 시간 (5초 후 HALF_OPEN으로)
        waitDurationInOpenState: 5s

        # HALF_OPEN 상태에서 3개 요청으로 테스트
        permittedNumberOfCallsInHalfOpenState: 3

        # HALF_OPEN -> CLOSED: 50% 이상 성공 시
        slowCallRateThreshold: 50

김개발 씨는 모니터링 화면을 뚫어지게 쳐다봤습니다. 5초마다 상태가 바뀌는 게 마치 신호등처럼 보였습니다.

CLOSED에서 OPEN으로, 다시 HALF_OPEN으로, 그리고 CLOSED로 돌아오는 사이클이 반복되었습니다. 박시니어 씨가 의자를 끌고 와서 설명을 시작했습니다.

"서킷 브레이커를 신호등에 비유하면 이해하기 쉬워요. 파란불, 빨간불, 노란불이 있듯이 서킷 브레이커에도 세 가지 상태가 있죠." 그렇다면 서킷 브레이커의 상태란 정확히 무엇일까요?

쉽게 비유하자면, 서킷 브레이커의 상태는 마치 건물 출입구의 보안 시스템과 같습니다. 평상시에는 문이 열려 있어 누구나 들어갈 수 있습니다(CLOSED).

화재 경보가 울리면 문이 자동으로 잠겨 아무도 들어갈 수 없습니다(OPEN). 경보가 해제된 후에는 보안요원이 직접 확인하며 선별적으로 출입을 허가합니다(HALF_OPEN).

이처럼 서킷 브레이커도 외부 서비스의 상태에 따라 자동으로 접근을 제어합니다. 서킷 브레이커가 없던 시절에는 어땠을까요?

개발자들은 try-catch 블록으로 예외를 처리하고, 재시도 로직을 직접 작성했습니다. 하지만 이미 다운된 서비스에 계속 요청을 보내며 리소스를 낭비했습니다.

더 큰 문제는 언제 다시 시도해야 하는지 판단하기 어려웠다는 점입니다. 너무 빨리 재시도하면 부하를 가중시키고, 너무 늦게 재시도하면 복구 시점을 놓쳤습니다.

상태 기반 자동 제어가 등장하면서 상황이 달라졌습니다. 서킷 브레이커는 실패율을 자동으로 모니터링하며 상태를 전환합니다.

개발자가 직접 카운터를 관리하거나 타이머를 설정할 필요가 없습니다. 시스템이 알아서 "지금은 차단해야 할 때", "이제 테스트해볼 때"를 판단합니다.

바로 이런 문제를 해결하기 위해 상태 기반 서킷 브레이커가 등장했습니다. 상태 전환을 사용하면 자동 장애 격리가 가능해집니다.

실패율이 임계치를 넘으면 즉시 OPEN 상태로 전환하여 추가 피해를 막습니다. 또한 자동 복구 시도도 얻을 수 있습니다.

일정 시간 후 HALF_OPEN으로 전환하여 서비스가 복구되었는지 확인합니다. 무엇보다 예측 가능한 동작이라는 큰 이점이 있습니다.

상태별로 명확한 규칙이 있어 시스템 동작을 예측하고 디버깅하기 쉽습니다. 위의 설정 코드를 한 줄씩 살펴보겠습니다.

먼저 slidingWindowSize와 failureRateThreshold를 보면 CLOSED에서 OPEN으로 전환되는 조건을 알 수 있습니다. 최근 10개 요청 중 50%가 실패하면 서킷이 열립니다.

이 부분이 핵심입니다. 다음으로 waitDurationInOpenState는 OPEN 상태를 얼마나 유지할지 정의합니다.

5초 동안 모든 요청을 차단한 후 HALF_OPEN으로 전환됩니다. permittedNumberOfCallsInHalfOpenState는 복구 테스트에 사용할 요청 수를 지정합니다.

3개의 요청만 허용하여 서비스 상태를 확인합니다. 이 중 절반 이상이 성공하면 CLOSED로 돌아가고, 실패하면 다시 OPEN으로 돌아갑니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 배달의민족 같은 배달 서비스를 개발한다고 가정해봅시다.

점심시간에 주문이 폭주하면 외부 지도 API가 타임아웃을 자주 일으킬 수 있습니다. 서킷 브레이커를 적용하면 초기 몇 번의 타임아웃을 감지한 후 자동으로 OPEN 상태로 전환됩니다.

이후 5초 동안은 지도 API를 호출하지 않고 캐시된 데이터를 사용합니다. 5초 후 HALF_OPEN 상태에서 3번의 테스트 요청을 보내고, 성공하면 다시 정상 운영으로 돌아갑니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 waitDurationInOpenState를 너무 짧게 설정하는 것입니다.

1초나 2초로 설정하면 서비스가 아직 복구되지 않았는데도 계속 재시도하여 부하를 가중시킵니다. 일반적으로 5~10초가 적절합니다.

따라서 외부 서비스의 복구 시간을 고려하여 설정해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 신호등처럼 자동으로 바뀌는 거군요!" 서킷 브레이커의 상태 전환을 제대로 이해하면 안정적인 마이크로서비스를 설계할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 프로덕션 환경에서는 failureRateThreshold를 50~70% 사이로 설정하는 것이 일반적입니다

  • Actuator 엔드포인트(/actuator/circuitbreakers)로 실시간 상태를 확인할 수 있습니다
  • 상태 전환 이벤트를 로깅하여 장애 패턴을 분석하세요

3. CLOSED, OPEN, HALF OPEN

김개발 씨가 테스트 환경에서 서킷 브레이커를 직접 테스트해보기로 했습니다. 일부러 외부 API를 다운시키고 요청을 보내자, 처음 몇 번은 응답이 왔다가 갑자기 즉시 실패하기 시작했습니다.

"어? 왜 갑자기 바로 실패하지?" 박시니어 씨가 "그게 바로 OPEN 상태예요.

각 상태의 동작 방식을 제대로 알아야 해요"라고 조언했습니다.

CLOSED 상태는 서킷이 닫혀 있어 모든 요청이 실제 외부 서비스로 전달됩니다. OPEN 상태는 서킷이 열려 있어 요청을 즉시 차단하고 fallback을 실행합니다.

HALF_OPEN 상태는 제한된 수의 요청만 허용하여 복구 여부를 테스트합니다. 각 상태는 명확한 진입 조건과 종료 조건을 가지며, 이를 이해해야 올바르게 설정할 수 있습니다.

다음 코드를 살펴봅시다.

@Service
@Slf4j
public class OrderService {

    @CircuitBreaker(name = "orderService", fallbackMethod = "fallbackOrder")
    public Order createOrder(OrderRequest request) {
        log.info("서킷 상태 확인 후 실제 주문 생성 시도");

        // CLOSED: 이 메서드가 실제로 실행됨
        // OPEN: 이 메서드가 실행되지 않고 바로 fallback으로
        // HALF_OPEN: 일부 요청만 이 메서드 실행
        return externalOrderApi.create(request);
    }

    private Order fallbackOrder(OrderRequest request, Exception ex) {
        log.warn("서킷 브레이커 작동: {}", ex.getMessage());
        // OPEN 또는 HALF_OPEN 실패 시 이 메서드 실행
        return Order.pending(request, "주문 서비스 일시 중단");
    }
}

김개발 씨는 콘솔 로그를 주시했습니다. 처음 5번의 요청은 "실제 주문 생성 시도" 로그가 찍혔습니다.

하지만 6번째 요청부터는 "서킷 브레이커 작동" 로그만 계속 나타났습니다. API를 실제로 호출하지도 않았는데 즉시 fallback이 실행되는 것이었습니다.

박시니어 씨가 화면을 가리키며 설명했습니다. "보세요.

처음에는 CLOSED 상태였어요. 모든 요청이 실제 API로 갔죠.

하지만 실패율이 50%를 넘는 순간 OPEN 상태로 바뀌었어요. 이제는 API를 아예 호출하지 않고 바로 fallback을 실행합니다." 그렇다면 각 상태의 동작 방식이란 정확히 무엇일까요?

쉽게 비유하자면, 이 세 가지 상태는 마치 은행 창구와 같습니다. CLOSED 상태는 모든 창구가 열려 있어 고객들이 자유롭게 업무를 봅니다.

OPEN 상태는 점심시간이라 모든 창구가 닫혀 있고, 안내 데스크에서 "1시에 다시 오세요"라고 안내합니다. HALF_OPEN 상태는 점심시간이 끝나고 한두 개 창구만 열어 시범적으로 운영하는 것과 같습니다.

이처럼 서킷 브레이커도 상황에 따라 접근 방식을 달리합니다. 단순한 재시도 로직만 있던 시절에는 어땠을까요?

요청이 실패하면 개발자가 정의한 횟수만큼 재시도했습니다. 하지만 서비스가 다운된 상태에서도 계속 재시도하며 리소스를 낭비했습니다.

더 큰 문제는 모든 요청이 타임아웃될 때까지 기다려야 했다는 점입니다. 5초 타임아웃이면 5초를 기다리고, 3번 재시도하면 총 15초를 낭비했습니다.

서킷 브레이커의 상태별 제어가 등장하면서 상황이 달라졌습니다. OPEN 상태에서는 요청을 즉시 차단하므로 타임아웃을 기다릴 필요가 없습니다.

1밀리초 만에 fallback 응답을 반환합니다. HALF_OPEN 상태에서는 서비스 복구 여부를 효율적으로 테스트합니다.

모든 요청을 보내지 않고 소수의 요청만으로 판단합니다. 바로 이런 문제를 해결하기 위해 상태별 차별화된 동작이 설계되었습니다.

CLOSED 상태를 사용하면 정상 시 오버헤드가 없습니다. 서킷 브레이커가 있지만 실패율만 측정할 뿐 요청 흐름에 영향을 주지 않습니다.

OPEN 상태를 사용하면 즉각적인 장애 격리가 가능해집니다. 이미 다운된 서비스에 리소스를 낭비하지 않습니다.

HALF_OPEN 상태를 사용하면 안전한 복구 확인이라는 큰 이점이 있습니다. 전체 트래픽을 보내기 전에 소규모 테스트로 안전성을 검증합니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 createOrder 메서드를 보면 @CircuitBreaker 어노테이션이 적용되어 있습니다.

이 메서드가 실제로 실행되는지 여부는 현재 서킷 상태에 달려 있습니다. CLOSED 상태에서는 주석에 나온 것처럼 이 메서드가 정상적으로 실행됩니다.

OPEN 상태에서는 이 메서드에 진입조차 하지 않고 바로 fallbackOrder로 넘어갑니다. fallbackOrder 메서드는 대체 응답을 생성합니다.

로그를 남겨 서킷 브레이커가 작동했음을 기록하고, 사용자에게는 "주문 서비스 일시 중단"이라는 정중한 메시지를 반환합니다. 단순히 에러를 던지는 것보다 훨씬 나은 사용자 경험입니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 넷플릭스 같은 스트리밍 서비스를 개발한다고 가정해봅시다.

추천 알고리즘 API가 다운되었을 때 CLOSED 상태에서는 모든 요청이 API로 가서 타임아웃됩니다. 사용자는 10초씩 기다려야 하죠.

하지만 실패율이 임계치를 넘어 OPEN 상태로 전환되면, 즉시 "인기 콘텐츠" 같은 대체 추천 목록을 보여줍니다. 5초 후 HALF_OPEN 상태에서 3명의 사용자에게만 실제 API를 호출해보고, 성공하면 다시 CLOSED로 돌아가 개인화 추천을 재개합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 fallback 메서드에서 또 다른 외부 API를 호출하는 것입니다.

원래 API가 실패해서 fallback으로 왔는데, 거기서 다른 API를 호출하면 또 실패할 수 있습니다. fallback은 로컬 캐시나 기본값을 사용해야 합니다.

따라서 확실히 성공할 수 있는 대체 로직만 작성해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 OPEN 상태에서는 로그가 바로 찍히는 거군요!" 각 상태의 동작 방식을 제대로 이해하면 서킷 브레이커를 효과적으로 활용할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - HALF_OPEN에서 OPEN으로 돌아가는 것을 "재차단"이라고 하며, 이는 정상 동작입니다

  • fallback 메서드는 원본 메서드와 같은 반환 타입과 파라미터를 가져야 하며, Exception 파라미터를 추가로 받을 수 있습니다
  • 상태 전환 로그를 남기려면 CircuitBreakerRegistry에 이벤트 리스너를 등록하세요

4. @CircuitBreaker 어노테이션

김개발 씨가 새로운 외부 API 연동 작업을 맡았습니다. 박시니어 씨가 "이번엔 처음부터 서킷 브레이커를 적용해보세요"라고 했지만, 어노테이션만 붙이면 되는 건지 다른 설정도 필요한 건지 헷갈렸습니다.

코드를 작성하다가 "이 어노테이션이 정확히 어떻게 동작하는 거죠?"라고 물었습니다.

@CircuitBreaker 어노테이션은 Spring AOP를 활용하여 메서드 호출을 가로채고 서킷 브레이커 로직을 적용합니다. name 속성으로 설정을 참조하고, fallbackMethod로 장애 시 실행할 메서드를 지정합니다.

어노테이션만 붙이면 자동으로 프록시가 생성되어 실패 추적, 상태 관리, fallback 실행을 처리합니다. Spring Bean 메서드에만 적용 가능합니다.

다음 코드를 살펴봅시다.

@Service
public class RecommendationService {

    // 기본 사용법: name과 fallback만 지정
    @CircuitBreaker(name = "recommendationApi", fallbackMethod = "getDefaultRecommendations")
    public List<Product> getPersonalizedRecommendations(Long userId) {
        return externalRecommendationApi.recommend(userId);
    }

    // fallback 메서드: 원본과 같은 파라미터 + Exception
    private List<Product> getDefaultRecommendations(Long userId, Exception ex) {
        log.warn("추천 API 실패, 기본 추천 반환: {}", ex.getMessage());
        return productRepository.findPopularProducts();
    }

    // 여러 예외 타입을 처리하는 fallback 체인
    @CircuitBreaker(name = "paymentApi",
                    fallbackMethod = "fallbackPayment")
    public PaymentResult processPayment(Payment payment) {
        return paymentGateway.charge(payment);
    }

    // 같은 이름의 fallback을 여러 개 오버로딩 가능
    private PaymentResult fallbackPayment(Payment payment, TimeoutException ex) {
        return PaymentResult.retry("타임아웃 발생");
    }

    private PaymentResult fallbackPayment(Payment payment, Exception ex) {
        return PaymentResult.failed("결제 실패");
    }
}

김개발 씨는 어노테이션을 붙이고 서버를 재시작했습니다. 처음에는 아무 일도 일어나지 않는 것처럼 보였습니다.

요청이 정상적으로 처리되고, 로그에도 특별한 메시지가 없었습니다. "이게 맞나?" 의구심이 들었습니다.

박시니어 씨가 모니터링 화면을 열어 보여줬습니다. "보세요, 서킷 브레이커가 조용히 모든 호출을 추적하고 있어요.

성공 횟수, 실패 횟수, 응답 시간까지 다 기록하고 있죠. 정상적으로 동작하는 거예요." 그렇다면 @CircuitBreaker 어노테이션의 동작 원리란 정확히 무엇일까요?

쉽게 비유하자면, @CircuitBreaker 어노테이션은 마치 보안 검문소와 같습니다. 메서드 입구에 보안 검문소를 설치하면, 모든 사람(요청)이 통과할 때마다 신원을 확인하고 기록을 남깁니다.

수상한 사람(실패한 요청)이 너무 많으면 자동으로 출입을 통제합니다. 개발자는 "여기에 검문소를 세워주세요"라고 어노테이션으로 표시만 하면, Spring이 알아서 검문소를 설치하고 운영합니다.

어노테이션이 없던 시절, 그러니까 프로그래밍 방식으로만 서킷 브레이커를 적용하던 시절에는 어땠을까요? 개발자들은 매번 CircuitBreaker 객체를 직접 생성하고, 람다 함수로 로직을 감싸야 했습니다.

코드가 길어지고 읽기 어려워졌습니다. 더 큰 문제는 fallback 로직을 별도로 관리하기 어려웠다는 점입니다.

try-catch 블록이 중첩되며 코드가 복잡해졌습니다. Spring AOP 기반의 어노테이션이 등장하면서 상황이 달라졌습니다.

한 줄의 어노테이션만 추가하면 모든 서킷 브레이커 로직이 자동으로 적용됩니다. 비즈니스 로직과 장애 처리 로직이 깔끔하게 분리됩니다.

fallback 메서드도 일반 메서드처럼 작성하면 됩니다. 바로 이런 문제를 해결하기 위해 선언적 서킷 브레이커가 등장했습니다.

@CircuitBreaker를 사용하면 코드 가독성이 향상됩니다. 비즈니스 로직에 장애 처리 코드가 섞이지 않아 읽기 쉽습니다.

또한 설정 외부화도 얻을 수 있습니다. 재시도 횟수나 타임아웃 같은 값을 코드 수정 없이 YAML 파일에서 변경할 수 있습니다.

무엇보다 테스트 용이성이라는 큰 이점이 있습니다. fallback 메서드를 독립적으로 단위 테스트할 수 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 가장 기본적인 형태를 보면 name과 fallbackMethod만 지정합니다.

name은 application.yml의 설정과 연결되고, fallbackMethod는 같은 클래스 내의 메서드 이름을 문자열로 지정합니다. 이 부분이 핵심입니다.

다음으로 fallback 메서드의 시그니처를 보세요. 원본 메서드와 파라미터가 같고, 마지막에 Exception 파라미터를 추가로 받습니다.

여러 fallback을 오버로딩할 수도 있습니다. TimeoutException에 대한 fallback과 일반 Exception에 대한 fallback을 분리하면 예외 유형별로 다른 처리가 가능합니다.

Resilience4j는 가장 구체적인 타입의 fallback을 먼저 찾아 실행합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 카카오톡 같은 메신저 서비스를 개발한다고 가정해봅시다. 사용자 프로필 이미지를 외부 CDN에서 가져오는데, CDN이 다운되면 어떻게 될까요?

@CircuitBreaker를 적용하지 않으면 모든 채팅방에서 프로필 이미지가 로딩 중 상태로 멈춥니다. 하지만 어노테이션을 적용하고 fallback에서 기본 아바타 이미지를 반환하면, CDN이 다운되어도 채팅 기능 자체는 정상적으로 동작합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 private 메서드에 어노테이션을 붙이는 것입니다.

Spring AOP는 프록시 패턴으로 동작하므로 public 메서드에만 적용됩니다. private이나 protected 메서드에 붙이면 조용히 무시됩니다.

따라서 public 메서드에만 적용해야 합니다. 또한 같은 클래스 내부에서 직접 호출하면 프록시를 거치지 않으므로 서킷 브레이커가 작동하지 않습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.

"아, 그래서 어노테이션만 붙였는데도 자동으로 동작하는 거군요!" @CircuitBreaker 어노테이션을 제대로 이해하면 간결하고 유지보수하기 쉬운 코드를 작성할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - fallbackMethod는 같은 클래스의 private 메서드여도 됩니다 (AOP가 적용되는 것은 원본 메서드뿐)

  • @CircuitBreaker와 @Retry를 함께 사용하면 재시도 후 서킷 브레이커가 작동합니다
  • 비동기 메서드(@Async)에도 적용 가능하며, CompletableFuture를 반환해야 합니다

5. 설정 파라미터

김개발 씨가 처음 서킷 브레이커를 적용하고 테스트해보니 너무 민감하게 반응했습니다. 단 한 번만 실패해도 바로 서킷이 열리는 것 같았습니다.

박시니어 씨에게 물어보니 "아, 설정을 제대로 안 했구나. application.yml을 열어봐요"라고 답했습니다.

설정 파일을 열어보니 낯선 파라미터들이 가득했습니다.

서킷 브레이커의 동작은 설정 파라미터로 세밀하게 제어합니다. slidingWindowSize는 실패율 계산에 사용할 최근 호출 수를 정의하고, failureRateThreshold는 서킷을 열 실패율 임계치를 설정합니다.

waitDurationInOpenState는 OPEN 상태 유지 시간, slowCallDurationThreshold는 느린 호출 판단 기준, slowCallRateThreshold는 느린 호출 비율 임계치를 지정합니다. 이 값들을 조정하여 각 서비스의 특성에 맞게 최적화합니다.

다음 코드를 살펴봅시다.

# application.yml - 실무 환경 서킷 브레이커 설정
resilience4j:
  circuitbreaker:
    instances:
      paymentService:
        # 슬라이딩 윈도우 설정
        slidingWindowType: COUNT_BASED  # 개수 기반 (TIME_BASED도 가능)
        slidingWindowSize: 100  # 최근 100개 호출로 실패율 계산
        minimumNumberOfCalls: 10  # 최소 10개는 호출되어야 실패율 계산

        # CLOSED -> OPEN 전환 조건
        failureRateThreshold: 50  # 실패율 50% 초과 시 OPEN
        slowCallRateThreshold: 80  # 느린 호출 80% 초과 시 OPEN
        slowCallDurationThreshold: 3s  # 3초 이상 걸리면 "느린 호출"

        # OPEN 상태 설정
        waitDurationInOpenState: 10s  # 10초 후 HALF_OPEN으로

        # HALF_OPEN 상태 설정
        permittedNumberOfCallsInHalfOpenState: 5  # 5개 요청으로 테스트

        # 예외 처리
        recordExceptions:  # 이런 예외는 "실패"로 기록
          - java.io.IOException
          - java.util.concurrent.TimeoutException
        ignoreExceptions:  # 이런 예외는 무시 (실패로 안 침)
          - com.example.BusinessException

김개발 씨는 YAML 파일을 보며 당황했습니다. slidingWindowSize, failureRateThreshold, slowCallDurationThreshold...

생소한 용어들이 한가득이었습니다. "이걸 다 설정해야 하나요?" 박시니어 씨가 웃으며 답했습니다.

"처음에는 기본값을 쓰다가 점차 튜닝하면 돼요. 하지만 각 파라미터가 무엇을 의미하는지는 알아야 문제가 생겼을 때 대처할 수 있죠." 그렇다면 서킷 브레이커 설정 파라미터란 정확히 무엇일까요?

쉽게 비유하자면, 설정 파라미터는 마치 자동차의 계기판과 같습니다. 과속 경보를 몇 km/h에서 울릴지, 연료 경고등을 몇 %에서 켤지, 자동 긴급 제동을 얼마나 민감하게 설정할지 등을 조정합니다.

기본값으로도 운전은 되지만, 운전 스타일과 도로 상황에 맞게 조정하면 더 안전하고 편리합니다. 서킷 브레이커도 마찬가지로 각 서비스의 특성에 맞게 파라미터를 조정해야 최적의 성능을 발휘합니다.

설정 파라미터가 없던 시절, 그러니까 하드코딩으로만 서킷 브레이커를 구현하던 시절에는 어땠을까요? 개발자들은 실패 카운터를 증가시키는 코드를 직접 작성했습니다.

임계치를 변경하려면 코드를 수정하고 재배포해야 했습니다. 더 큰 문제는 환경별로 다른 값을 적용하기 어려웠다는 점입니다.

개발 환경에서는 느슨하게, 운영 환경에서는 엄격하게 설정하고 싶어도 불가능했습니다. 외부 설정 파일이 등장하면서 상황이 달라졌습니다.

YAML이나 properties 파일에서 모든 파라미터를 관리합니다. 코드 수정 없이 설정만 변경하여 재시작하면 됩니다.

환경별로 다른 설정 파일을 사용할 수 있습니다. Spring Profile을 활용하면 dev, staging, production 환경마다 다른 임계치를 적용할 수 있습니다.

바로 이런 문제를 해결하기 위해 외부 설정 기반 서킷 브레이커가 등장했습니다. 설정 파라미터를 사용하면 무중단 튜닝이 가능해집니다.

운영 중에도 설정을 조정하여 점진적으로 최적화할 수 있습니다. 또한 A/B 테스트도 얻을 수 있습니다.

일부 인스턴스에만 다른 설정을 적용하여 효과를 비교할 수 있습니다. 무엇보다 가독성과 유지보수성이라는 큰 이점이 있습니다.

모든 설정이 한곳에 모여 있어 파악하기 쉽습니다. 위의 설정 코드를 한 줄씩 살펴보겠습니다.

먼저 slidingWindowType과 slidingWindowSize를 보면 실패율 계산 방식을 알 수 있습니다. COUNT_BASED는 최근 N개 호출을 보고, TIME_BASED는 최근 N초간의 호출을 봅니다.

일반적으로 COUNT_BASED가 더 예측 가능합니다. 이 부분이 핵심입니다.

minimumNumberOfCalls는 통계적 유의성을 보장합니다. 단 1~2개 호출만으로 서킷을 열면 안 되니까요.

failureRateThreshold와 slowCallRateThreshold는 서킷을 열 조건입니다. 둘 중 하나라도 초과하면 OPEN으로 전환됩니다.

slowCallDurationThreshold가 중요한데, 타임아웃은 아니지만 비정상적으로 느린 응답도 "실패"로 간주하기 위함입니다. recordExceptions와 ignoreExceptions는 예외 필터링입니다.

비즈니스 예외(잘못된 입력값 등)는 서비스 장애가 아니므로 ignoreExceptions에 추가하여 실패율에 포함시키지 않습니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 쿠팡 같은 이커머스를 개발한다고 가정해봅시다. 상품 상세 API는 트래픽이 매우 많으므로 slidingWindowSize를 1000으로 크게 설정합니다.

반면 결제 API는 중요도가 높으므로 failureRateThreshold를 30%로 낮춰 더 민감하게 반응하도록 합니다. 외부 배송 조회 API는 원래 느리므로 slowCallDurationThreshold를 10초로 여유있게 설정합니다.

이렇게 서비스마다 다른 특성을 반영하여 최적화합니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 failureRateThreshold를 너무 낮게 설정하는 것입니다. 10%나 20%로 설정하면 일시적인 네트워크 지연에도 서킷이 바로 열립니다.

일반적으로 50~70%가 적절합니다. 또한 waitDurationInOpenState를 너무 짧게하면 서비스가 복구되기도 전에 계속 재시도하여 부하를 가중시킵니다.

최소 5~10초는 주어야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 각 서비스마다 다른 설정을 쓰는 거군요!" 설정 파라미터를 제대로 이해하면 각 서비스의 특성에 맞게 서킷 브레이커를 최적화할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 처음에는 기본값(failureRateThreshold: 50, slidingWindowSize: 100)으로 시작하고 모니터링하며 점진적으로 튜닝하세요

  • slidingWindowType은 트래픽이 일정하면 COUNT_BASED, 시간대별로 변동이 크면 TIME_BASED를 사용하세요
  • Actuator의 /health 엔드포인트에서 현재 설정값과 서킷 상태를 확인할 수 있습니다

6. 상태 전이 모니터링

김개발 씨가 서킷 브레이커를 적용하고 일주일이 지났습니다. 모든 게 잘 돌아가는 것 같았는데, 어느 날 팀장님이 "서킷 브레이커가 실제로 잘 작동하고 있나요?

증거가 있나요?"라고 물었습니다. 로그를 뒤져보니 상태 전환이 일어났는지 확인할 방법이 없었습니다.

박시니어 씨가 "모니터링을 설정하지 않았군요. 서킷 브레이커는 보이지 않으면 없는 거나 마찬가지예요"라고 조언했습니다.

상태 전이 모니터링은 서킷 브레이커가 언제 CLOSED에서 OPEN으로, OPEN에서 HALF_OPEN으로 전환되었는지 추적합니다. Resilience4j는 이벤트 기반 아키텍처를 제공하여 모든 상태 변화를 이벤트로 발행합니다.

EventConsumerRegistry를 사용하여 이벤트를 구독하고, 로깅, 메트릭, 알림 등을 구현할 수 있습니다. Spring Boot Actuator와 통합하면 Prometheus, Grafana로 시각화가 가능합니다.

다음 코드를 살펴봅시다.

@Configuration
public class CircuitBreakerMonitoringConfig {

    @PostConstruct
    public void registerEventListeners(CircuitBreakerRegistry registry) {
        // 모든 서킷 브레이커에 이벤트 리스너 등록
        registry.getAllCircuitBreakers().forEach(circuitBreaker -> {

            circuitBreaker.getEventPublisher()
                // CLOSED -> OPEN 전환 이벤트
                .onStateTransition(event -> {
                    log.warn("서킷 상태 전환: {} -> {}, 서비스: {}",
                        event.getStateTransition().getFromState(),
                        event.getStateTransition().getToState(),
                        circuitBreaker.getName());

                    // 알림 전송 (Slack, Email 등)
                    if (event.getStateTransition().getToState() == OPEN) {
                        alertService.sendCritical(
                            "서킷 브레이커 작동: " + circuitBreaker.getName()
                        );
                    }
                })
                // 개별 호출 성공/실패 이벤트
                .onSuccess(event -> {
                    metrics.recordSuccess(circuitBreaker.getName());
                })
                .onError(event -> {
                    log.error("서킷 브레이커 호출 실패: {}, 예외: {}",
                        circuitBreaker.getName(),
                        event.getThrowable().getMessage());
                    metrics.recordFailure(circuitBreaker.getName());
                });
        });
    }
}

김개발 씨는 당황했습니다. 분명히 서킷 브레이커를 적용했는데, 실제로 작동했는지 증명할 방법이 없었습니다.

로그에는 평범한 에러 메시지만 있을 뿐, 서킷이 열렸는지 닫혔는지 알 수 없었습니다. 박시니어 씨가 화면을 열어 Grafana 대시보드를 보여줬습니다.

"보세요, 우리 팀은 모든 서킷 브레이커의 상태를 실시간으로 모니터링하고 있어요. 어제 새벽 2시에 결제 서비스 서킷이 열렸다가 5초 후 복구된 게 다 기록되어 있죠." 그렇다면 상태 전이 모니터링이란 정확히 무엇일까요?

쉽게 비유하자면, 상태 전이 모니터링은 마치 CCTV와 같습니다. 건물 출입구에 CCTV를 설치하면 누가 언제 들어오고 나갔는지 모두 녹화됩니다.

나중에 문제가 생기면 영상을 돌려봐서 원인을 찾을 수 있습니다. 서킷 브레이커도 마찬가지로, 모든 상태 변화를 기록해두면 장애 발생 시 "언제 서킷이 열렸고, 왜 열렸고, 얼마나 빨리 복구되었는지"를 정확히 알 수 있습니다.

모니터링이 없던 시절에는 어땠을까요? 서킷 브레이커를 적용하긴 했지만, 실제로 작동하는지 확인할 방법이 없었습니다.

사용자가 "아까 주문이 안 됐어요"라고 신고하면 그제야 로그를 뒤져보며 원인을 찾았습니다. 더 큰 문제는 서킷 브레이커가 너무 자주 열리는지, 아니면 충분히 빨리 열리지 않는지 판단할 데이터가 없었다는 점입니다.

이벤트 기반 모니터링이 등장하면서 상황이 달라졌습니다. 모든 상태 전환이 이벤트로 발행됩니다.

개발자는 이 이벤트를 구독하여 원하는 방식으로 처리할 수 있습니다. 로그로 남기거나, 메트릭으로 집계하거나, 슬랙으로 알림을 보내는 등 자유롭게 활용할 수 있습니다.

바로 이런 문제를 해결하기 위해 이벤트 기반 모니터링이 설계되었습니다. 이벤트 리스너를 사용하면 실시간 장애 감지가 가능해집니다.

서킷이 열리는 순간 슬랙이나 페이저듀티로 알림이 갑니다. 또한 데이터 기반 튜닝도 얻을 수 있습니다.

서킷이 너무 자주 열리면 임계치를 높이고, 너무 느리게 반응하면 낮추는 식으로 데이터를 보며 조정할 수 있습니다. 무엇보다 장애 사후 분석이라는 큰 이점이 있습니다.

타임스탬프와 함께 모든 이벤트가 기록되므로 장애 보고서를 작성하기 쉽습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 CircuitBreakerRegistry를 주입받아 모든 서킷 브레이커를 가져옵니다. Spring Boot가 자동으로 구성해주는 빈입니다.

다음으로 getEventPublisher()로 이벤트 발행자를 얻습니다. 이 부분이 핵심입니다.

onStateTransition()은 상태가 바뀔 때마다 호출됩니다. CLOSED -> OPEN, OPEN -> HALF_OPEN, HALF_OPEN -> CLOSED 등 모든 전환을 감지합니다.

이벤트 객체에서 getStateTransition()으로 "어디서 어디로" 바뀌었는지 알 수 있습니다. OPEN 상태로 전환되면 심각한 상황이므로 즉시 알림을 보냅니다.

onSuccess()와 onError()는 개별 호출 단위로 발생하는 이벤트입니다. 메트릭 집계에 유용합니다.

실제 현업에서는 어떤 식으로 활용할까요? 예를 들어 토스 같은 핀테크 서비스를 개발한다고 가정해봅시다.

결제 서비스 서킷이 열리면 즉시 운영팀에 슬랙 알림이 갑니다. "결제 서비스 서킷 브레이커 작동 - 외부 PG사 응답 없음".

운영팀은 PG사에 문의하고, 동시에 대체 PG사로 트래픽을 우회합니다. 5분 후 서킷이 HALF_OPEN으로 전환되었다는 알림이 오면, 복구 중임을 알 수 있습니다.

다시 CLOSED로 돌아가면 "정상화 완료" 알림이 옵니다. 모든 과정이 자동으로 기록되고 알림이 갑니다.

또한 Prometheus와 Grafana를 연동하면 더 강력합니다. Actuator 엔드포인트를 활성화하고 Prometheus가 메트릭을 수집하도록 설정합니다.

Grafana 대시보드에서는 서킷 브레이커별로 실패율 그래프, 상태 전환 타임라인, 응답 시간 분포를 한눈에 볼 수 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 onSuccess(), onError() 이벤트에서 무거운 작업을 수행하는 것입니다. 이 이벤트는 모든 호출마다 발생하므로, 여기서 외부 API를 호출하거나 복잡한 계산을 하면 성능이 크게 저하됩니다.

로그를 남기거나 간단한 카운터를 증가시키는 정도만 해야 합니다. 무거운 작업은 비동기 큐에 넣어 별도로 처리해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.

"아, 그래서 모니터링 없이는 서킷 브레이커를 제대로 운영할 수 없는 거군요!" 상태 전이 모니터링을 제대로 설정하면 서킷 브레이커를 가시적으로 관리하고 지속적으로 개선할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - Spring Boot Actuator를 활성화하면 /actuator/circuitbreakers와 /actuator/circuitbreakerevents 엔드포인트로 상태를 확인할 수 있습니다

  • Micrometer를 사용하면 Prometheus, Datadog, CloudWatch 등 다양한 모니터링 시스템과 통합할 수 있습니다
  • 알림 피로를 막기 위해 같은 서킷이 5분 내 재차 열리면 알림을 억제하는 로직을 추가하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Java#Resilience4j#CircuitBreaker#MicroserviceResilience#FaultTolerance#마이크로서비스

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.