Spring Boot 마이크로서비스를 위한 완전한 복원력 가이드 — 모든 Resilience4j 어노테이션 사용

발행: (2025년 12월 2일 오후 06:44 GMT+9)
5 min read
원문: Dev.to

Source: Dev.to

가정

Spring Boot와 resilience4j-spring-boot2/resilience4j-spring-boot3 통합을 사용하고 있다고 가정합니다 (Resilience4j 1.x/2.x도 동작 방식이 유사합니다). 예제는 순수 Java + Spring (비‑reactive) 기반입니다.

Maven (pom.xml)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-spring-boot3</artifactId>
        <version>1.7.1</version>
    </dependency>

    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-all</artifactId>
        <version>1.7.1</version>
    </dependency>

    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>com.github.tomakehurst</groupId>
        <artifactId>wiremock-jre8</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Gradle (Kotlin DSL)

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("io.github.resilience4j:resilience4j-spring-boot3:1.7.1")
    implementation("io.github.resilience4j:resilience4j-all:1.7.1")
    implementation("io.micrometer:micrometer-registry-prometheus")
    implementation("org.springframework.boot:spring-boot-starter-actuator")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("com.github.tomakehurst:wiremock-jre8:2.27.2")
}

Spring Boot 버전에 맞는 버전을 선택하세요.

application.yml – 설정 예시

resilience4j:
  circuitbreaker:
    configs:
      default:
        registerHealthIndicator: true
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 20
        minimumNumberOfCalls: 10
        permittedNumberOfCallsInHalfOpenState: 5
        waitDurationInOpenState: 30s
        failureRateThreshold: 50
        automaticTransitionFromOpenToHalfOpenEnabled: false
    instances:
      externalServiceCB:
        baseConfig: default
        waitDurationInOpenState: 10s
        failureRateThreshold: 40

  retry:
    instances:
      externalServiceRetry:
        maxAttempts: 3
        waitDuration: 500ms
        retryExceptions:
          - java.io.IOException
          - java.util.concurrent.TimeoutException

  timelimiter:
    instances:
      externalServiceTL:
        timeoutDuration: 2s
        cancelRunningFuture: true

  bulkhead:
    configs:
      default:
        maxConcurrentCalls: 10
        maxWaitDuration: 0ms    # for semaphore bulkhead
      threadpool-default:
        maxThreadPoolSize: 10
        coreThreadPoolSize: 5
        queueCapacity: 50
        keepAliveDuration: 30s
    instances:
      semaphoreBulkhead:
        baseConfig: default
        maxConcurrentCalls: 20
      threadPoolBulkhead:
        baseConfig: threadpool-default

  ratelimiter:
    instances:
      externalServiceRateLimiter:
        limitForPeriod: 10
        limitRefreshPeriod: 1s
        timeoutDuration: 0

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      show-details: always

예제 컴포넌트

ExternalClient.java – 얇은 HTTP 클라이언트 (RestTemplate 사용)

@Service
public class ExternalClient {

    private final RestTemplate restTemplate;

    public ExternalClient(RestTemplateBuilder builder) {
        this.restTemplate = builder
            .setReadTimeout(Duration.ofSeconds(5))
            .setConnectTimeout(Duration.ofSeconds(2))
            .build();
    }

    public String getRemoteData(String id) {
        String url = "https://external.service/api/resource/" + id;
        return restTemplate.getForObject(url, String.class);
    }
}

ResilientService.java – Resilience4j 어노테이션 적용

@Service
public class ResilientService {

    private final ExternalClient externalClient;

    public ResilientService(ExternalClient externalClient) {
        this.externalClient = externalClient;
    }

    // RateLimiter → Semaphore Bulkhead → Retry → CircuitBreaker
    @RateLimiter(name = "externalServiceRateLimiter", fallbackMethod = "rateLimiterFallback")
    @Bulkhead(name = "semaphoreBulkhead", type = Bulkhead.Type.SEMAPHORE, fallbackMethod = "bulkheadFallback")
    @Retry(name = "externalServiceRetry", fallbackMethod = "retryFallback")
    @CircuitBreaker(name = "externalServiceCB", fallbackMethod = "circuitFallback")
    public String getData(String id) {
        return externalClient.getRemoteData(id);
    }

    // --- Fallback methods (signatures must match) ---
    public String circuitFallback(String id, Throwable t) {
        return "circuit-fallback: cached-or-default";
    }

    public String retryFallback(String id, Throwable t) {
        return "retry-fallback: sorry";
    }

    public String bulkheadFallback(String id, BulkheadFullException ex) {
        return "bulkhead-fallback: overloaded";
    }

    public String rateLimiterFallback(String id, RequestNotPermitted ex) {
        return "rate-limited-fallback: try-later";
    }
}

AsyncResilientService.java – 비동기 패턴 (TimeLimiter + ThreadPool Bulkhead)

@Service
public class AsyncResilientService {

    private final ExternalClient externalClient;
    private final ExecutorService executor = Executors.newFixedThreadPool(10);

    public AsyncResilientService(ExternalClient externalClient) {
        this.externalClient = externalClient;
    }

    // TimeLimiter expects a CompletableFuture (async)
    @Bulkhead(name = "threadPoolBulkhead", type = Bulkhead.Type.THREADPOOL, fallbackMethod = "tpbFallback")
    @TimeLimiter(name = "externalServiceTL", fallbackMethod = "tlFallback")
    public CompletableFuture<String> getDataAsync(String id) {
        return CompletableFuture.supplyAsync(() -> externalClient.getRemoteData(id), executor);
    }

    public CompletableFuture<String> tpbFallback(String id, BulkheadFullException ex) {
        return CompletableFuture.completedFuture("threadpool-bulkhead-fallback");
    }

    public CompletableFuture<String> tlFallback(String id, TimeoutException ex) {
        return CompletableFuture.completedFuture("time-limiter-fallback");
    }
}

어노테이션 결합 – 일반적인 순서

보통 다음과 같은 순서를 사용합니다:

RateLimiter → Bulkhead → CircuitBreaker → TimeLimiter → Retry
  • RateLimiter: 하위 서비스가 과부하되는 것을 방지합니다.
  • Bulkhead (세마포어 또는 스레드‑풀): 프로세스 내부에서 동시 사용을 제한합니다.
  • CircuitBreaker: 실패한 서비스에 대한 호출을 빠르게 차단합니다.
  • TimeLimiter: 비동기 호출의 지연 시간을 제한합니다.
  • Retry: 일시적인 실패를 재시도합니다 (보통 CircuitBreaker 내부에 두어 부하가 증폭되는 것을 방지합니다).

필요에 따라 순서를 조정하세요. 예를 들어, 각 시도마다 동일한 보호를 적용하고 싶다면 CircuitBreaker 바깥쪽에 Retry를 배치할 수 있습니다.

Fallback 메서드 요구사항

  • fallbackMethod 속성에 지정한 이름과 정확히 일치해야 합니다.
  • 반환 타입은 보호되는 메서드의 반환 타입과 동일해야 합니다.
  • 매개변수: 원본 메서드 매개변수 플러스 선택적인 마지막 Throwable/Exception (또는 BulkheadFullException과 같은 특정 예외 타입)

이 규칙을 지키면 Resilience4j 이벤트 발생 시 Spring이 올바르게 fallback 메서드로 라우팅할 수 있습니다.

Back to Blog

관련 글

더 보기 »

계정 전환

@blink_c5eb0afe3975https://dev.to/blink_c5eb0afe3975 여러분도 알다시피 저는 다시 제 진행 상황을 기록하기 시작했으니, 이것을 다른…