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 메서드로 라우팅할 수 있습니다.