본문 바로가기
spring

[SpringBoot3] Resilience4j Java Config 설정

by moonsiri 2025. 5. 20.
728x90
반응형

프로젝트에서 Resilience4j의 CircuitBreaker, Bulkhead를 적용하면서 어노테이션 기반(@CircuitBreaker, @Bulkhead)이 아닌 Java Config + 직접 호출 방식으로 구조화한 경험을 공유합니다.

왜 AOP 방식을 사용하지 않았는가?

기존에는 아래와 같이 AOP 기반으로 사용하고 있었습니다.

@CircuitBreaker(name = "selectList", fallbackMethod = "selectListFallback")
@Bulkhead(name = "selectList", type = Bulkhead.Type.SEMAPHORE, fallbackMethod = "selectListFallback")
public List<String> selectList(RequestDTO param) { ... }

public List<String> selectListFallback(RequestDTO param, Throwable throwable) { ... }

하지만 다음과 같은 불편이 있었습니다.

  • 메서드 시그니처가 fallback 때문에 강제됨
  • 내부 메서드 호출 시 AOP 미적용 이슈 (this.method() 문제)
  • CircuitBreaker/Bulkhead 등 조합 순서 제어 어려움
  • 마지막 성공 응답을 이용한 캐싱 fallback이 불편함

 

직접 호출 방식(Java Config + Decorator 조합)으로 전환

핵심 설계 목표

  • @CircuitBreaker, @Bulkhead 없이 직접 제어
  • fallback 메서드 없이 마지막 성공값으로 자동 복구
  • 설정은 application.yml이 아닌 Java Config로 통일
  • actuator 상태 확인 가능

 

설정 구성 (Java Config 기반)

1. pom.xml

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

2. application.yml 설정

management:
  health:
    circuitbreakers:
      enabled: true
  metrics:
    tags:
      application: ${spring.application.name}
    distribution:
      percentiles-histogram:
        resilience4j.circuitbreaker.calls: true

resilience4j:
  circuitbreaker:
    configs:
      default:
        registerHealthIndicator: true
        eventConsumerBufferSize: 5
  • 위 설정은 Java Config로 변환 불가능

 
3. CircuitBreaker 설정

@Configuration
public class CircuitBreakerConfiguration {

    @Bean
    public CircuitBreakerConfig defaultCircuitBreakerConfig() {
        return CircuitBreakerConfig.custom()
            .failureRateThreshold(10)
            .slidingWindowSize(2)
            .minimumNumberOfCalls(2)
            .recordExceptions(RuntimeException.class)
            .ignoreExceptions(IllegalArgumentException.class)
            .build();
    }

    @Bean
    public CircuitBreakerRegistry circuitBreakerRegistry(CircuitBreakerConfig config) {
        return CircuitBreakerRegistry.of(config);
    }
}

 

Config 커스텀 설정 ▽

더보기
@Configuration
public class CircuitBreakerConfiguration {

    public CircuitBreakerConfig.Builder defaultCircuitBreakerConfigBuilder() {
        return CircuitBreakerConfig.custom()
            .failureRateThreshold(10)
            .slidingWindowSize(2)
            .minimumNumberOfCalls(2)
            .recordExceptions(RuntimeException.class)
            .ignoreExceptions(IllegalArgumentException.class)
            ;
    }

    @Bean
    public CircuitBreakerConfig defaultCircuitBreakerConfig() {
        return defaultCircuitBreakerConfigBuilder().build();
    }

    @Bean
    public CircuitBreakerConfig lowThresholdCircuitBreakerConfig() {
        return defaultCircuitBreakerConfigBuilder()
            .failureRateThreshold(5)
            .build();
    }

    @Bean
    public CircuitBreakerRegistry circuitBreakerRegistry(
        CircuitBreakerConfig defaultCircuitBreakerConfig,
        CircuitBreakerConfig lowThresholdCircuitBreakerConfig
    ) {
        Map<String, CircuitBreakerConfig> configs = new HashMap<>();
        configs.put("default", defaultCircuitBreakerConfig);
        configs.put("lowThreshold", lowThresholdCircuitBreakerConfig);
        return CircuitBreakerRegistry.of(configs);
    }
}

 

Event Listener 설정 ▽

더보기
    @Bean
    public CircuitBreakerRegistry circuitBreakerRegistry(CircuitBreakerConfig config) {
        CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);

        // 생성될 때마다 이벤트 리스너 붙이기
        registry.getEventPublisher().onEntryAdded(entry -> registerEventListeners(entry.getAddedEntry()));

        return registry;
    }

//    @Bean
//    public MeterRegistry meterRegistry(CircuitBreakerRegistry circuitBreakerRegistry) {
//        final SimpleMeterRegistry meterRegistry = new SimpleMeterRegistry();
//        TaggedCircuitBreakerMetrics.ofCircuitBreakerRegistry(circuitBreakerRegistry).bindTo(meterRegistry);
//        return meterRegistry;
//    }

    private void registerEventListeners(CircuitBreaker circuitBreaker) {

        final String name = circuitBreaker.getName();

        circuitBreaker.getEventPublisher()
            .onSuccess(event -> {
                log.debug("### CircuitBreaker [{}] onSuccess", name);
            })
            .onFailureRateExceeded(event -> {
                log.debug("### CircuitBreaker [{}] onFailureRateExceeded", name);
            })
            .onSlowCallRateExceeded(event -> {
                log.debug("### CircuitBreaker [{}] onSlowCallRateExceeded", name);
            })
            .onCallNotPermitted(event -> {
                log.debug("### CircuitBreaker [{}] onCallNotPermitted", name);
            })
            .onIgnoredError(event -> {
                log.debug("### CircuitBreaker [{}] onIgnoredError", name);
            })
            .onError(event -> {
                // recordExceptions 에 정의된 Exception 발생 시 실행
                final Throwable throwable = event.getThrowable();
                log.error("### CircuitBreaker [{}] | error message : {}", name, throwable.getMessage(), throwable);
            })
            .onStateTransition(event -> {
                // Circuit 상태 변화 시 실행
                final CircuitBreaker.State fromState = event.getStateTransition().getFromState();
                final CircuitBreaker.State toState = event.getStateTransition().getToState();
                CircuitBreaker.Metrics metrics = circuitBreaker.getMetrics();

                log.error("################ CircuitBreaker {} | fromState : {} | toState : {} | metrics : {}", name, fromState, toState, metrics);
            });
    }

 

AOP를 사용하고 싶다면?  

더보기

만약 @CircuitBreaker 어노테이션을 사용하고 싶다면, CircuitBreakerAspect 설정을 추가하면됩니다.

    @Bean
    public CircuitBreakerAspect circuitBreakerAspect(
        CircuitBreakerRegistry circuitBreakerRegistry,
        List<CircuitBreakerAspectExt> circuitBreakerAspectExtList,
        BeanFactory factory
    ) {
        final SpelResolver spelResolver = new DefaultSpelResolver(new SpelExpressionParser(), new DefaultParameterNameDiscoverer(), factory);
        return new CircuitBreakerAspect(
            new CircuitBreakerConfigurationProperties(),
            circuitBreakerRegistry,
            circuitBreakerAspectExtList,
            new FallbackExecutor(spelResolver, new FallbackDecorators(List.of(new DefaultFallbackDecorator()))),
            spelResolver
        );
    }

 

 


4. Bulkhead 설정

@Configuration
public class BulkheadConfiguration {

    @Bean
    public BulkheadConfig defaultBulkheadConfig() {
        return BulkheadConfig.custom()
            .maxConcurrentCalls(20)
            .maxWaitDuration(Duration.ofMillis(5))
            .build();
    }

    @Bean
    public BulkheadRegistry bulkheadRegistry(BulkheadConfig config) {
        return BulkheadRegistry.of(config);
    }
}


Event Listener 설정 ▽

더보기
    @Bean
    public BulkheadRegistry bulkheadRegistry(BulkheadConfig defaultBulkheadConfig) {
        BulkheadRegistry registry = BulkheadRegistry.of(defaultBulkheadConfig);

        // 생성될 때마다 이벤트 리스너 붙이기
        registry.getEventPublisher().onEntryAdded(entry -> registerEventListeners(entry.getAddedEntry()));

        return registry;
    }

    private void registerEventListeners(Bulkhead bulkhead) {

        bulkhead.getEventPublisher()
            .onCallPermitted(event ->
                log.debug("###### Bulkhead [{}] permitted", event.getBulkheadName())
            )
            .onCallRejected(event ->
                log.error("###### Bulkhead [{}] rejected", event.getBulkheadName())
            )
            .onCallFinished(event ->
                log.debug("###### Bulkhead [{}] finished", event.getBulkheadName())
            );
    }

 

공통 처리 모듈

ResilienceExecutor

@Slf4j
@Service
@RequiredArgsConstructor
public class ResilienceExecutor {

    private final CircuitBreakerRegistry circuitBreakerRegistry;
    private final BulkheadRegistry bulkheadRegistry;
    private final ResilienceCacheManager cacheManager;

    public Builder start(String name, String key) {
        return new Builder(circuitBreakerRegistry, bulkheadRegistry, cacheManager, name, key);
    }

    public static class Builder {
        private final CircuitBreakerRegistry cbRegistry;
        private final BulkheadRegistry bhRegistry;
        private final ResilienceCacheManager cacheManager;

        private final List<ResilienceDecorator<?>> decorators = new ArrayList<>();
        private final String name;
        private final String key;

        private Builder(CircuitBreakerRegistry cbRegistry, BulkheadRegistry bhRegistry, ResilienceCacheManager cacheManager, String name, String key) {
            this.cbRegistry = cbRegistry;
            this.bhRegistry = bhRegistry;
            this.cacheManager = cacheManager;
            this.name = name;
            this.key = key;
        }

        public Builder circuitBreaker() {
            CircuitBreaker cb = cbRegistry.circuitBreaker(name);
            decorators.add(supplier -> CircuitBreaker.decorateSupplier(cb, supplier));
            return this;
        }

        // Config 커스텀 사용 시 사용
//        public Builder circuitBreaker(String... configNames) {
//            if (configNames.length > 1) {
//                throw new IllegalArgumentException("circuitBreaker requires at most one config");
//            }
//            final String configName = (configNames.length == 1) ? configNames[0] : "default";
//
//            CircuitBreaker cb = cbRegistry.circuitBreaker(name, configName);
//            decorators.add(supplier -> CircuitBreaker.decorateSupplier(cb, supplier));
//            return this;
//        }

        public Builder bulkhead() {
            Bulkhead bh = bhRegistry.bulkhead(name);
            decorators.add(supplier -> Bulkhead.decorateSupplier(bh, supplier));
            return this;
        }

        public <T> T run(Supplier<T> logic) {

            Supplier<T> wrapped = logic;
            for (int i = decorators.size() - 1; i >= 0; i--) {
                wrapped = ((ResilienceDecorator<T>) decorators.get(i)).apply(wrapped);
            }

            try {
                T result = wrapped.get();
                if (name != null && result != null) {
                    cacheManager.put(name, key, result);
                }
                return result;

            } catch (Exception e) {
                log.warn("### fallback: name={}, key={}, error={}", name, key, e.toString());
                return cacheManager.get(name, key);
            }
        }
    }

    @FunctionalInterface
    private interface ResilienceDecorator<T> {
        Supplier<T> apply(Supplier<T> supplier);
    }

}
  • 추천 동작 순위 : TargetFunction > BulkHead > TimeLimiter > RateLimiter > CircuitBreaker > Retry

 

마지막 성공값 저장소

ResilienceCacheManager

@Component
public class ResilienceCacheManager {

    private final Map<String, Map<String, Object>> cache = new ConcurrentHashMap<>();

    public <K, V> void put(String name, K key, V value) {
        cache.computeIfAbsent(name, n -> new ConcurrentHashMap<>())
              .put(key.toString(), value);
    }

    @SuppressWarnings("unchecked")
    public <K, V> V get(String name, K key) {
        Map<String, Object> inner = cache.get(name);
        return inner != null ? (V) inner.get(key.toString()) : null;
    }
}

 

사용 예시

public List<String> selectListWithResilience(RequestDTO param) {
	return resilienceExecutor
		.start("selectList", param.toString())
		.bulkhead()
		.circuitBreaker()
		.run(
			() -> this.selectList(param)
		);
}
  • CircuitBreaker + Bulkhead 적용
  • 실패 시 마지막 성공 값으로 fallback
  • AOP 없이 설정값 적용 및 actuator 확인 가능

 
 

결과 및 장점

항목 효과
설정 적용 Java Config로 직접 제어 가능 (YML보다 강력)
fallback 구조 별도 메서드 없이 캐시 자동 복원
actuator 연동 /actuator/health/circuitBreakers 확인 가능
테스트 용이성 직접 호출 구조로 디버깅/테스트 쉬움
재사용성 모든 호출에 일관된 패턴으로 적용 가능

 


 
 
[Reference]
https://resilience4j.readme.io/docs/circuitbreaker
https://mein-figur.tistory.com/entry/resilience4j-circuit-breaker-setting
https://docs.spring.io/spring-cloud-circuitbreaker/docs/current/reference/html/spring-cloud-circuitbreaker-resilience4j.html
https://godekdls.github.io/Resilience4j/spring-boot-2-getting-started/

728x90
반응형

댓글