장애를 버티게 하는 힘, Resilience4j

개발자 팀·2026년 1월 20일

self-study-series

목록 보기
3/16
post-thumbnail

서비스는 태생적으로 장애가 발생할 수 밖에 없습니다.
장애가 없는 서비스란 존재하지 않는다고 해도 과언이 아니죠.
그런 의미에서 Resilience4j는 회복탄력성 패턴을 통해 장애가 발생해도 서비스가 버티게하는 역할을 합니다.
그 내용 그대로 정말 중요한 역할을 하는 패턴이죠.
자세히 한 번 알아보겠습니다.

Resilience4j가 필요한 이유

우리가 개발하는 서비스는 마이크로서비스일 확률이 매우 높습니다.
각각의 서비스는 서로 유기적인 연결을 통해 하나의 통합된 서비스를 제공하죠.
그 의미는 외부 의존성(결제, 배송, 추천, 검색 등 타 서비스 API를 호출)을 가진 서비스들의 통신은 언젠가 반드시 실패할 가능성이 매우 큽니다.

  • 네트워크 통신의 순간 오류
  • 일시적인 서버 장애(5xx)
  • 다운스트림 자체의 Latency 증가
  • 과부하(트래픽의 폭증)
  • 장애의 전파

여기서 Resilience4j는 회복탄력성 패턴으로 '실패를 없애는게 아니라 무너지지 않게 버티는 구조'를 만들어냅니다.

간단한 용어 정리

본격적으로 내용을 시작하기 전 용어를 간략하게 정리하고 넘어가겠습니다.

  • Upstream(업스트림): 호출하는 쪽(클라이언트/상위 서비스)
  • Downstream(다운스트림): 호출받는 쪽(외부 API/하위 서비스)
  • 실패(Failure): 예외/5xx/타임아웃 등 “정상 응답이 아닌 상태”
  • 느린 호출(Slow Call): 성공했어도 “너무 늦게” 온 호출(응답 지연)
  • Fallback: 실패/차단 시 “대체 응답”을 만드는 로직
  • 슬라이딩 윈도우: 최근 N건 또는 최근 T초 호출을 기준으로 통계를 내는 창

장애를 다루는 방법

우리가 일반적으로 장애를 격리하지 않고 그대로 전파되도록 둔다면 다음과 같은 연쇄적인 반응이 발생하게 될 것입니다.

C가 느려지면 B의 호출에서 병목이 생기고 B를 부르는 A의 스레드도 트랜잭션으로 묶여버립니다.
A의 스레드가 묶이면 사용자의 요청이 지속적으로 쌓이고, 결국 전체가 같이 느려지거나 다운되어버리는 연쇄적인 장애가 발생하게 됩니다.

이런 환경에서 Resilience4j를 활용한 회복탄력성 패턴을 적용시키면 아래와 같이 장애를 격리하여 업스트림으로의 전파를 막을 수 있습니다.

여기서 핵심은 느린 호출을 빨리 끊어내고(TimeLimiter/Timeout) 반복 실패는 일정 비율이 넘으면 차단하고(CircuitBreaker) 장애의 폭주가 전체를 잡아먹지 않도록 격리(Bulkhead/RateLimiter) 하도록 처리해야 합니다.

각각의 패턴을 간략하게 정리해보겠습니다.

패턴한 줄 설명주로 막는 문제대표 설정
CircuitBreaker“망가진 곳에 잠깐 연락 끊자”반복 실패/지연으로 인한 전파failureRateThreshold, waitDurationInOpenState
Retry“일시적이면 몇 번 더 시도”순간 오류/짧은 5xxmaxAttempts, waitDuration
TimeLimiter“너무 오래 걸리면 끊자”느림으로 인한 스레드 고갈timeoutDuration
RateLimiter“초당 N건까지만 허용”과도한 호출/보호limitForPeriod, limitRefreshPeriod
Bulkhead“동시 실행을 제한하거나 분리”특정 기능 폭주로 전체 장애maxConcurrentCalls / thread pool

제공되는 패턴을 실무에선 적절히 혼합하여 사용하는데 간단한 예시로는 Bulkhead + TimeLimiter + CircuitBreaker 패턴을 사용하는 방식 등을 들 수 있습니다.

설정 방식

Resilience4j는 다음과 같이 설정할 수 있습니다.
Maven 환경과 Gradle환경에서는 종속성을 추가해주면 됩니다.

<Maven 방식>

  <!-- Resilience4j for Spring Boot 3 -->
  <dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
  </dependency>

<Gradle 방식>

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  implementation 'io.github.resilience4j:resilience4j-spring-boot3'
  implementation 'org.springframework.boot:spring-boot-starter-actuator'
}

그럼 Resilience4j 설정은 어떤식으로 하게 될까요?
일반적으로 실무에선 application.yml에 작성하여 사용하며 Config Server와도 잘 맞습니다.

CircuitBreaker 패턴

Resilience4j에서 가장 유명하며 잘 알려져있는 패턴인 CircuitBreaker 패턴에 대해서 알아보도록 하겠습니다.

  • CLOSED: 평소 정상 상태.
  • OPEN: “장애상황”으로 판단하여 즉시 실패(빠른 실패)시키며, 다운스트림을 쉬게 합니다.
  • HALF_OPEN: 일정 시간 후 일부 요청만 통과시키며 회복 여부를 시험합니다.

그럼 CircuitBreaker가 하는 일에 대해서 알아보겠습니다.
슬라이딩 윈도우 설정에 따라 최근 N개의 호출을 관찰하게 됩니다. 이후 특정 조건 상에서 실패율이나 Latency 비율이 임계치 이상이면 OPEN으로 상태가 전환됩니다.
상태가 OPEN인 동안 호출을 막아 추가 피해를 줄이고 일정 시간 후 HALF_OPEN으로 전환하여 시스템이 회복되었는지를 테스트 하게 됩니다.

여기서 슬라이딩 윈도우(slidingWindowSize)와 minimumNumberOfCells 이해

  • slidingWindowSize=10, minimumNumberOfCalls=5라면
    첫 4번 호출은 통계에만 쌓이고, 5번째부터 실패율 계산이 시작됩니다.
  • 계산 대상은 “최근 10건”입니다. 즉, 오래된 호출은 자동으로 사라집니다.

COUNT_BASED vs TIME_BASED

  • COUNT_BASED는 설정한 최근 N건의 상태를 본다는 설정입니다.
  • TIME_BASED는 최근 T초 동안의 호출을 보면서 증상을 진단하게 됩니다.

설정 예시(application.yml)

일반적으로 application.yml설정을 하게 되면 아래와 같이 설정을 하게 됩니다.
참고로 instances의 이름과 어노테이션의 name은 정확히 일치해야 합니다.

resilience4j:
  circuitbreaker:
    configs:
      default:
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 50
        minimumNumberOfCalls: 20
        failureRateThreshold: 50
        slowCallRateThreshold: 50
        slowCallDurationThreshold: 2s
        waitDurationInOpenState: 10s
        permittedNumberOfCallsInHalfOpenState: 10
        automaticTransitionFromOpenToHalfOpenEnabled: true
        recordExceptions:
          - java.io.IOException
          - org.springframework.web.client.ResourceAccessException
        ignoreExceptions:
          - com.example.demo.exception.BusinessException
    instances:
      paymentApi:
        baseConfig: default

간단하게 설정 필드들을 확인해보면 아래와 같습니다.

  • slidingWindowSize: 관찰할 호출 개수(예: 최근 50번)
  • minimumNumberOfCalls: 최소 몇 번은 호출되어야 판단 시작할 것인지에 대한 값 설정
  • failureRateThreshold: 실패 비율 % 기준
  • slowCallDurationThreshold: 지연으로 볼 시간 기준
  • waitDurationInOpenState: OPEN 유지 시간
  • permittedNumberOfCallsInHalfOpenState: HALF_OPEN에서 테스트 호출 수
  • recordExceptions/ignoreExceptions실패율 계산에 어떤 예외를 포함할지를 결정합니다.
  • 비즈니스 예외(검증 실패, 권한 없음 등)는 보통 ignoreExceptions에 둡니다.

위와 같이 설정을 해두면 Resilience4jCircuitBreaker는 사용할 준비가 끝나게 됩니다.
이를 실제로 사용할 때엔 다음과 같이 어노테이션 기반으로 사용하게 됩니다.

@Service
public class PaymentClient {

    private final RestTemplate restTemplate;

    public PaymentClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @CircuitBreaker(
            name = "paymentApi", // 어노테이션의 name은 여기를 말합니다.
            fallbackMethod = "fallback"
    )
    public PaymentResponse requestPayment(PaymentRequest req) {
        return restTemplate.postForObject(
                "http://payment-service/pay",
                req,
                PaymentResponse.class
        );
    }

    // fallbackMethod 규칙:
    // - 원본 파라미터 그대로 + 마지막에 Throwable(선택)
    private PaymentResponse fallback(PaymentRequest req, Throwable t) {
        return PaymentResponse.fail("PAYMENT_TEMPORARILY_UNAVAILABLE");
    }
}

만약 이전 상황으로 인해 CircuitBreaker가 OPEN상태라면 실제 호출은 실행되지 않고 즉시 차단되게 됩니다.
이때 fallbackMethod가 호출되게되고 Throwable에는 보통 CallNotPermittedException이 발생됩니다.

이후 설정해놓은 차단시간(waitDurationInOpenState)이 지나면 Resilience4j는 자동으로 상태를 OPEN에서 HALE_OPEN으로 변경하게 됩니다.
다만 automaticTransitionFromOpenToHalfOpenEnabled=false로 설정되어있다면 이땐 다음 호출이 들어올 때 HALF_OPEN으로 상태가 변경됨을 뜻합니다.


마치며

오늘은 Resilience4j의 가장 대표적인 패턴인 CircuitBreaker 패턴에 대해 스터디를 해보았습니다.
각 설정 필드들이 어떤 효과를 가지고 있는지 알 수 있었습니다 😃
다음 글에선 Retry나 TimeLimiter, Bulkhead 방식 등 좀 더 다양한 패턴에 대해 정리를 해보도록 하겠습니다!
읽어주셔서 감사합니다 🫡

profile
공부하고 기록하고 공유하는 개발자 팀(Tim) 입니다. 늘끄적입니다.

0개의 댓글