아래처럼 openfeign의 설정만으로 CB를 활성화하여 사용할 수 있다.
Spring Cloud OpenFeign ↔ Resilience4j CircuitBreaker 통합. 실제 구현체는 보통 Resilience4j가 붙는다.
즉 Feign 호출을 Resilience4j로 감싸는 것이다.
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true
group:
enabled: true
feign:
circuitbreaker:
enabled: true
client:
config:
test-api:
... feign 설정 ...
resilience4j:
circuitbreaker:
configs:
default:
... circuitbreaker 설정 ...
instances:
test-api:
baseConfig: default
@CircuitBreaker(name = "test-api")
@FeignClient(name = "test-api", url = "${test-api.url}")
public interface TestAPIClient {
@GetMapping(value = "/v1/test/{code}")
@FeignRetry(maxAttempt = 2, backoff = @Backoff())
Response getCode(@RequestHeader("Authorization") String token,
@PathVariable("code") String code);
AOP 방식을 쓸때 FeignClient를 보통 interface로 구현해놓는 경우도 많을 텐데
보통은 코드 레벨에서 직접 래핑하는 경우는 잘 사용하지 않는다. 다만 여러 건의 외부 호출을 묶어서 하나의 CB로 관리하고 싶을 때는 직접 래핑이 필요하다.
openfeign 의 내장 CB 사용해라.
단, openfeign cb의 group 모드(spring.cloud.openfeign.circuitbreaker.group.enabled = true)가 사용 가능한지에 따라 추가 설정이 필요하다.
실패 집계 일원화: 같은 외부 시스템을 치는 여러 메서드가 한 CB로 묶여 실패율·오픈 상태가 함께 관리된다.
설정/운영 단순화: resilience4j.circuitbreaker.instances.<이름>을 한 번만 선언하면 끝. 메트릭·알람도 저비용으로 관리.
openfeign의 group 모드는 openfeign 4.x (Spring Cloud 2022.0.x+, Boot 3.x) 이상부터 지원하는데, 사용중인 서비스는 (Spring Cloud 2020.0.2, Boot 2.x) 로 openfeign 3.x 버전이다.
모든 메서드를 같은 feignClientName명으로 하나로 묶는 것이다.
@Configuration
public class FeignCircuitBreakerNameConfig {
@Bean
public CircuitBreakerNameResolver circuitBreakerNameResolver() {
return (String feignClientName, Target<?> target, Method method) -> feignClientName;
}
}
CircuitBreakerNameResolver도 Spring Cloud 2020.0.4 이상 부터 가능하니 버전 확인을 잘하자.
Feign 프록시 메서드 호출 → FeignCircuitBreakerInvocationHandler.invoke() → asSupplier()로 원격호출을 Supplier로 래핑 → Resilience4JCircuitBreaker.run() → 내부에서 Resilience4j CircuitBreaker에 위임
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
if (!"equals".equals(method.getName())) {
if ("hashCode".equals(method.getName())) {
return this.hashCode();
} else if ("toString".equals(method.getName())) {
return this.toString();
} else {
String circuitName = this.circuitBreakerNameResolver.resolveCircuitBreakerName(this.feignClientName, this.target, method);
CircuitBreaker circuitBreaker = this.circuitBreakerGroupEnabled ? this.factory.create(circuitName, this.feignClientName) : this.factory.create(circuitName);
Supplier<Object> supplier = this.asSupplier(method, args);
if (this.nullableFallbackFactory != null) {
Function<Throwable, Object> fallbackFunction = (throwable) -> {
Object fallback = this.nullableFallbackFactory.create(throwable);
try {
return ((Method)this.fallbackMethodMap.get(method)).invoke(fallback, args);
} catch (Exception e) {
throw new IllegalStateException(e);
}
};
return circuitBreaker.run(supplier, fallbackFunction);
} else {
return circuitBreaker.run(supplier);
}
}
} else {
try {
Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return this.equals(otherHandler);
} catch (IllegalArgumentException var8) {
return false;
}
}
}
비즈니스 메서드 호출 → CircuitBreakerAspect에서 감싸기 → Resilience4j CircuitBreaker.execute 위임 → 필요 시 fallback 메서드 탐색/호출.
public Object circuitBreakerAroundAdvice(ProceedingJoinPoint proceedingJoinPoint, @Nullable CircuitBreaker circuitBreakerAnnotation) throws Throwable {
Method method = ((MethodSignature)proceedingJoinPoint.getSignature()).getMethod();
String methodName = method.getDeclaringClass().getName() + "#" + method.getName();
if (circuitBreakerAnnotation == null) {
circuitBreakerAnnotation = this.getCircuitBreakerAnnotation(proceedingJoinPoint);
}
if (circuitBreakerAnnotation == null) {
return proceedingJoinPoint.proceed();
} else {
String backend = this.spelResolver.resolve(method, proceedingJoinPoint.getArgs(), circuitBreakerAnnotation.name());
io.github.resilience4j.circuitbreaker.CircuitBreaker circuitBreaker = this.getOrCreateCircuitBreaker(methodName, backend);
Class<?> returnType = method.getReturnType();
String fallbackMethodValue = this.spelResolver.resolve(method, proceedingJoinPoint.getArgs(), circuitBreakerAnnotation.fallbackMethod());
if (StringUtils.isEmpty(fallbackMethodValue)) {
return this.proceed(proceedingJoinPoint, methodName, circuitBreaker, returnType);
} else {
FallbackMethod fallbackMethod = FallbackMethod.create(fallbackMethodValue, method, proceedingJoinPoint.getArgs(), proceedingJoinPoint.getTarget());
return this.fallbackDecorators.decorate(fallbackMethod, () -> this.proceed(proceedingJoinPoint, methodName, circuitBreaker, returnType)).apply();
}
}
}
CircuitBreaker를 구현할 수 있는 다양한 방법들에 대해 알아봤고 사용하고 있는 버전에 따라 주의점들도 같이 살펴 봤다.
openfeign의 group 기능이 활성화 가능한 상태라면 해당 기능을 적극 사용하자.