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.
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/

'Spring' 카테고리의 다른 글
Zookeeper 이용해 Spring에서 동적으로 설정 값 변경하기 (0) | 2025.04.02 |
---|---|
WebClient를 이용한 외부 호출 관련 Metric 수집 (0) | 2025.03.02 |
Application Context Refresh 전에 로깅 설정하기 (1) | 2025.02.17 |
Spring Event UnderTheHood (0) | 2025.02.04 |
Spring @Transactional 동작원리 완벽 정리 (0) | 2024.03.02 |