circuit breaker를 설정하는 방법

1. openfeign 내장 CB 사용

아래처럼 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
  • Feign 호출이 자동으로 Resilience4j CB로 래핑됨.
  • 그룹 모드가 없으면 메서드 단위 이름으로 CB가 생성됨.
  • 설정은 configs.default(전역 기본)로 끝내거나, 필요한 메서드만 instances."<인터페이스#메서드(시그니처)>"로 오버라이드.
  • Fallback은 @FeignClient(fallback|fallbackFactory) 사용.

2. resilience4j CB 사용

1. AOP 방식인 @CircuitBreaker() 사용, openfeign 통합 설정은 off

  • 대신 AOP(@CircuitBreaker) 또는 코드 래핑(CircuitBreakerFactory/Decorators) 으로 보호.
  • Fallback은 AOP의 fallbackMethod(같은 클래스 내) 또는 코드에서 람다로 처리.
@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로 구현해놓는 경우도 많을 텐데

2. CircuitBreakerFactory 코드 직접 래핑

보통은 코드 레벨에서 직접 래핑하는 경우는 잘 사용하지 않는다. 다만 여러 건의 외부 호출을 묶어서 하나의 CB로 관리하고 싶을 때는 직접 래핑이 필요하다.

그럼 권장 설정은?

openfeign 의 내장 CB 사용해라.

단, openfeign cb의 group 모드(spring.cloud.openfeign.circuitbreaker.group.enabled = true)가 사용 가능한지에 따라 추가 설정이 필요하다.

openfeign의 group 모드는?

  • 기본값에선 Feign + CircuitBreaker가 메서드 단위로 만들어져서, CB 이름이 <Feign인터페이스>#<메서드>(<파라미터타입>) 형태로 각 메서드마다 따로 생긴다.
  • spring.cloud.openfeign.circuitbreaker.group.enabled=true를 켜면 클라이언트(그룹) 단위로 묶어서 CB를 만든다(같은 Feign 클라이언트의 여러 메서드가 하나의 CB를 공유).

왜 필요한가?

  • 실패 집계 일원화: 같은 외부 시스템을 치는 여러 메서드가 한 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 버전이다.

openfeign의 group 모드가 지원되지 않을 때 해결 방안

1. CircuitBreakerNameResolver 직접 구현하기

모든 메서드를 같은 feignClientName명으로 하나로 묶는 것이다.

@Configuration
public class FeignCircuitBreakerNameConfig {
    @Bean
    public CircuitBreakerNameResolver circuitBreakerNameResolver() {
        return (String feignClientName, Target<?> target, Method method) -> feignClientName;
    }
}

CircuitBreakerNameResolver도 Spring Cloud 2020.0.4 이상 부터 가능하니 버전 확인을 잘하자.

실제 동작 차이

openfeign cb 통합

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;
            }
        }
    }

resilience4j cb

비즈니스 메서드 호출 → 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 기능이 활성화 가능한 상태라면 해당 기능을 적극 사용하자.

profile
Java 백엔드 개발자입니다. 제가 생각하는 개발자로서 가져야하는 업무적인 기본 소양과 현업에서 가지는 고민들을 같이 공유하고 소통하려고 합니다.

0개의 댓글