[Spring Cloud] Feign에서 메서드 별로 Hystrix 설정 분리하기
시작하는 이유
앞선 게시글에서 Feign에서 CircuitBreaker 역할로 Hystrix를 사용해보았고 편리하게 application.yml에 설정을 추가하여 메서드 별로 적용해보았습니다.
하지만 이걸로는 yml 파일이 계속 지저분해지고 분리해야 하는 메서드들이 많아질수록 관리가 적용이 불편해질 것 같았습니다.
그래서 간단하게 Custom Annotation 형식으로 적용하는 예제를 만들어보려 합니다.
개요
Feign Client에서 Hystrix를 이용한 Fallback 설정을 메서드 별로 설정하기 위해 Hystrix Configuration을 Custom Annotation형태로 등록하도록 함
예제 Github
https://github.com/MarrRang/feign-hystrix-study/tree/master
사용법
1. Dependency 등록
우선은 OpenFeign과 Hystrix 그리고 reflection을 활용해야 할 것 같으니 편리하게 하기 위해서 reflections 유틸 Dependency도 추가해줍니다.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
2. Feign CircuitBreaker 활성화
Feign의 CircuitBreaker를 우선 활성화 시켜줍니다. 이것이 활성화되어있어야 fallback이 작동합니다.
# application.yml
feign:
circuitbreaker:
enabled: true
3. Custom Annotation 작성
Hystrix 개별 적용을 위해 Custom Annotation을 작성합니다. 이 예시에서는 ElementType.TYPE에도 적용이 가능하도록 해두었습니다. 이는 추후에 Feign Client Interface에 적용 시 인터페이스 내부 메서드에 전체 적용이 가능하도록 추가 개발할 예정이어서 해두었습니다. 불편하시다면 ElementType.METHOD만 남겨두셔도 됩니다.
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface CustomFeignHystrix {
String value() default "";
int executionIsolationThreadTimeoutInMilliseconds() default 3000;
int executionTimeoutInMilliseconds() default 3000;
boolean executionTimeoutEnabled() default true;
int metricsRollingStatisticalWindowInMilliseconds() default 10000;
int circuitBreakerRequestVolumeThreshold() default 10;
int circuitBreakerErrorThresholdPercentage() default 50;
int circuitBreakerSleepWindowInMilliseconds() default 5000;
}
4. Configuration 작성
이제 Custom Annotation이 붙은 메서드들을 찾아서 제가 원하는 대로 설정을 추가하도록 Configuration을 작성해보겠습니다.
@Slf4j
@Configuration
public class CustomFeignHystrixConfiguration {
@Bean
public CircuitBreakerFactory CustomHystrixCircuitBreakerFactory() {
HystrixCircuitBreakerFactory circuitBreakerFactory = new HystrixCircuitBreakerFactory();
List<Customizer<HystrixCircuitBreakerFactory>> customizerList = getCircuitBreakerCustomizer();
customizerList.forEach(customizer -> customizer.customize(circuitBreakerFactory));
//default
circuitBreakerFactory.configureDefault(id -> HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(id))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionTimeoutEnabled(true)
.withExecutionTimeoutInMilliseconds(3000)
)
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(30))
);
return circuitBreakerFactory;
}
private List<Customizer<HystrixCircuitBreakerFactory>> getCircuitBreakerCustomizer() {
Reflections reflections = new Reflections(new ConfigurationBuilder()
.setUrls(ClasspathHelper.forPackage("com.example.feignhystrixstudy")).setScanners(Scanners.MethodsAnnotated));
Set<Method> methodSet = reflections.getMethodsAnnotatedWith(CustomFeignHystrix.class);
List<Customizer<HystrixCircuitBreakerFactory>> customizerList = new ArrayList<>();
try {
methodSet.forEach(method -> {
String superClassName = method.getDeclaringClass().getSimpleName();
String methodName = method.getName();
Class<?>[] methodParameterClasses = method.getParameterTypes();
List<String> parameterClassNameList = Arrays.stream(methodParameterClasses).map(Class::getSimpleName).toList();
CustomFeignHystrix customFeignHystrix = method.getAnnotation(CustomFeignHystrix.class);
HystrixCommandProperties.Setter setter = HystrixCommandProperties.Setter()
.withExecutionIsolationThreadTimeoutInMilliseconds(customFeignHystrix.executionIsolationThreadTimeoutInMilliseconds())
.withExecutionTimeoutInMilliseconds(customFeignHystrix.executionTimeoutInMilliseconds())
.withExecutionTimeoutEnabled(customFeignHystrix.executionTimeoutEnabled())
.withMetricsRollingStatisticalWindowInMilliseconds(customFeignHystrix.metricsRollingStatisticalWindowInMilliseconds())
.withCircuitBreakerEnabled(true)
.withCircuitBreakerRequestVolumeThreshold(customFeignHystrix.circuitBreakerRequestVolumeThreshold())
.withCircuitBreakerErrorThresholdPercentage(customFeignHystrix.circuitBreakerErrorThresholdPercentage())
.withCircuitBreakerSleepWindowInMilliseconds(customFeignHystrix.circuitBreakerSleepWindowInMilliseconds());
Customizer<HystrixCircuitBreakerFactory> factoryCustomizer = getCustomHystrixCustomizer(superClassName, methodName, parameterClassNameList, setter);
customizerList.add(factoryCustomizer);
});
} catch (RuntimeException e) {
log.error("CustomHystrix Bean Setting Error : ", e);
}
return customizerList;
}
private Customizer<HystrixCircuitBreakerFactory> getCustomHystrixCustomizer(
String className,
String methodName,
List<String> parameterClassList,
HystrixCommandProperties.Setter options
) {
// Feign에서 찾는 CommandKey가 {클래스명}#{메서드명}({파라미터 클래스명}...) 형태
String id = className + "#" + methodName + "(" + String.join(",", parameterClassList) + ")";
return HystrixCircuitBreakerFactory -> HystrixCircuitBreakerFactory.configure(
hystrixConfigBuilder -> hystrixConfigBuilder.commandProperties(options), id
);
}
}
- CustomHystrixCircuitBreakerFactory() : 각각의 커스텀 한 설정들과 Default 설정을 HystrixCircuitBreakerFactory에 등록하고 그 Factory Bean을 등록하는 메서드
- getCircuitBreakerCustomizer() : Custom Annotation이 붙은 메서드를 찾아서 각각의 설정 내용을 만드는 메서드
- getCustomHystrixCustomizer() : Feign에 맞는 Command Key 값을 만들고 해당 Key(Id)와 사용자 지정 설정을 가진 Customizer를 만들어서 반환
- Feign에서 찾는 Command Key 형태 : {클래스명}#{메서드명}({파라미터 클래스명}...)
5. 사용해보기
@FeignClient(...)
public interface ProviderApiClient {
@CustomFeignHystrix(
executionTimeoutInMilliseconds = 1000
)
@GetMapping(
value = "/sleep",
produces = "application/json",
headers = {"User-Agent=FeignClient", "Cache-Control=no-cache"}
)
String provide(@RequestParam int sleepTime);
}
정리
이 예제는 굉장히 불안정합니다. Feign에서 찾는 Circuit Breaker의 키 값을 하드코딩 형식으로 만들고 설정하는 부분에 있어서 Feign이 조금이라도 수정된다면 바로 사용하지 못하게 됩니다. 그리고 현재는 메서드에만 적용이 가능해서 클래스 전체에 적용하는 것도 추가해야 조금이라도 더 효용성이 높아질 것 같습니다.
그래서 추후에는 아래의 작업을 진행할 예정입니다.
- 클래스에도 적용하도록 추가
- Feign에서 찾는 Circuit Breaker의 키 값의 형식을 직접 지정할 수 있도록 변경해보기
- Hystrix 말고 resilience4j 사용해보기