본문 바로가기

Spring

Feign Client 사용법 익히기

 

What is Feign Client

Feign Client는 Java에서 HTTP 요청 작업을 수행할때 사용할 수 있는 Http Client 중 하나이다.

Feign Client가 나오게 된 배경은 HTTP 요청의 복잡성을 줄여주기 위해 나왔다.

Feign's first goal was reducing the complexity of binding Denominator uniformly to HTTP APIs regardless of ReSTfulness.

ref - https://github.com/OpenFeign/feign?tab=readme-ov-file#feign-simplifies-the-process-of-writing-java-http-clients

 

GitHub - OpenFeign/feign: Feign makes writing java http clients easier

Feign makes writing java http clients easier. Contribute to OpenFeign/feign development by creating an account on GitHub.

github.com

 

Feign Client 기본 사용법과 동작 원리

 

Feign은 애노테이션을 이용해 Feign 구현체에 어떤 요청을 할지 알려주면, Feign 구현체가 그에 맞는 HTTP 요청을 하는 방식이다.

Feign works by processing annotations into a templatized request

 

Feign에게 어떤 요청을 하고 싶은지 알려주는 Interface

interface GitHub {
	@RequestLine("GET /repos/{owner}/{repo}/contributors")
	List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
}

 

Interface에 선언된 값들은 Feign 구현체에서 설정한 Contract 구현체에 의해 어떻게 해석될지 결정된다. 기본적 Feign Contract을 사용할 경우 애노테이션은 문서처럼 해석된다.

 

응답 DTO

@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Contributor {
	String login;
	int contributions;
}

 

 

로깅 남기기위한 Custom Logger 구현체

public class CustomLogger extends Logger {
	@Override
	protected void log(String configKey, String format, Object... args) {
		System.out.printf("[Feign Logger] " + format + "%n", args);
	}
}

 

 

Feign 구현체 생성 및 실행

public class FeignSample {
	public static void main(String... args) {
		GitHub github = Feign.builder()
			.decoder(new JacksonDecoder(new ObjectMapper()))
			.logger(new CustomLogger())
			.logLevel(Logger.Level.FULL)
			.target(GitHub.class, "https://api.github.com");
		// Fetch and print a list of the contributors to this library.
		List<Contributor> contributors = github.contributors("OpenFeign", "feign");
		for (Contributor contributor : contributors) {
			System.out.println(contributor.login + " (" + contributor.contributions + ")");
		}
	}
}

 

Spring Integration

Spring에서 Feign Client를 사용하는 방법과 그 원리를 알아보자.

Spring에서 Feign Client를 사용하기 위해서는 아래처럼 하면 된다.

 

의존성 등록

implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

 

Feign 기능 활성화

@EnableFeignClients
@SpringBootApplication
public class FeignSpringApplication {
    public static void main(String[] args) {
       ConfigurableApplicationContext context = SpringApplication.run(FeignSpringApplication.class, args);
       GithubClient client = context.getBean(GithubClient.class);
       List<Contributor> contributors = client.contributors("OpenFeign", "feign");
       System.out.println(contributors);
    }
}

 

 

Feign Client 정의

@FeignClient(name = "githubClient", url = "https://api.github.com", configuration = CustomFeignConfig.class)
public interface GithubClient {
	@RequestLine("GET /repos/{owner}/{repo}/contributors")
	List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
}

 

@Configuration
public class CustomFeignConfig {
	@Bean
	public Contract feignContract() {
		return new Contract.Default();
	}
	@Bean
	public Logger customLogger() {
		return new CustomLogger();
	}
	@Bean
	public Logger.Level feignLoggerLevel() {
		return Logger.Level.FULL;
	}
}

 

위 처럼 수정하면, Spring Context가 시작하면서, @FeignClient를 선언한 interface를 Spring Context에서 꺼내 사용할 수 있다.

 

 

Spring Context에 등록되는 과정은 아래와 같다.

 

1.

@EnableFeignClients 에 의해 FeignClientsRegistrar가 Spring Context에 등록된다.


@EnableFeignClients 코드를 보면 FeignClientsRegistrar를 등록함을 알 수 있다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

 

2.

FeignClientRegistrar가 Spring Context refresh 시점에 @FeignClient 가 선언된 Interface들을 정보들을 가져와

Bean 생성 방법을 저장해둔다. 이때 Bean 생성은 FeignClientFactoryBean에게 위임한다.

 

@FeignClient가 붙은 Interface 정보를 가져온다.

if (clients == null || clients.length == 0) {
    ClassPathScanningCandidateComponentProvider scanner = getScanner();
    scanner.setResourceLoader(this.resourceLoader);
    scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
    Set<String> basePackages = getBasePackages(metadata);
    for (String basePackage : basePackages) {
        candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
    }
}

 

그 후 Feign 객체들의 BeneDefinition을 저장한다.

private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
			Map<String, Object> attributes) {
		String className = annotationMetadata.getClassName();
		Class clazz = ClassUtils.resolveClassName(className, null);
		ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
				? (ConfigurableBeanFactory) registry : null;
		String contextId = getContextId(beanFactory, attributes);
		String name = getName(attributes);
		FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
		factoryBean.setBeanFactory(beanFactory);
		factoryBean.setName(name);
		factoryBean.setContextId(contextId);
		factoryBean.setType(clazz);
		factoryBean.setRefreshableClient(isClientRefreshEnabled());
		// 생성 역할을 FeignClientFactoryBean에게 위임한다.
		BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
			factoryBean.setUrl(getUrl(beanFactory, attributes));
			factoryBean.setPath(getPath(beanFactory, attributes));
			factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
			Object fallback = attributes.get("fallback");
			if (fallback != null) {
				factoryBean.setFallback(fallback instanceof Class ? (Class<?>) fallback
						: ClassUtils.resolveClassName(fallback.toString(), null));
			}
			Object fallbackFactory = attributes.get("fallbackFactory");
			if (fallbackFactory != null) {
				factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class<?>) fallbackFactory
						: ClassUtils.resolveClassName(fallbackFactory.toString(), null));
			}
			return factoryBean.getObject();
		});
		definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
		definition.setLazyInit(true);
		validate(attributes);

		AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
		beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
		beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);

		// has a default, won't be null
		boolean primary = (Boolean) attributes.get("primary");

		beanDefinition.setPrimary(primary);

		String[] qualifiers = getQualifiers(attributes);
		if (ObjectUtils.isEmpty(qualifiers)) {
			qualifiers = new String[] { contextId + "FeignClient" };
		}

		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);

		registerOptionsBeanDefinition(registry, contextId);
	}

 

 

Tips

Customing

위에서 알 수 있듯이 @FeignClient를 선언할때 Custom 설정하고 싶은 Configuration을 설정해주면, Customizing한 객체를 생성할 수 있다.

 

Metric

Feign Client를 사용할때 외부 api 응답속도, 최대 호출 횟수등을 metric으로 쌓아 모니터링 할 수 있다.

 

implementation 'io.github.openfeign:feign-micrometer:13.6'

 

GitHub github = Feign.builder()
            .addCapability(new MicrometerCapability(meterRegistry))
			.decoder(new JacksonDecoder(new ObjectMapper()))
			.logger(new CustomLogger())
			.logLevel(Logger.Level.FULL)
			.target(GitHub.class, "https://api.github.com");

 

GUIDE : https://github.com/OpenFeign/feign?tab=readme-ov-file#metrics

 

GitHub - OpenFeign/feign: Feign makes writing java http clients easier

Feign makes writing java http clients easier. Contribute to OpenFeign/feign development by creating an account on GitHub.

github.com

Spring Integration도 적용된 듯 싶다. (https://github.com/spring-cloud/spring-cloud-openfeign/pull/462)

 

 

아래 문서를 참고하면 더 다양한 Tip들을 알 수 있다.

https://techblog.woowahan.com/2630/