Web/웹 상식

[Spring Cloud] Feign에서 메서드 별로 Hystrix 설정 분리하기

MarrRang 2022. 7. 3. 18:16

시작하는 이유

앞선 게시글에서 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

 

GitHub - MarrRang/feign-hystrix-study

Contribute to MarrRang/feign-hystrix-study development by creating an account on GitHub.

github.com

사용법

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 사용해보기
반응형