Java http client - Feign 이용해보기

4 minute read

배경

기존에 RestTemplate을 이용해서 Spring에서 http client 라이브러리 역할을 많이 했다. 하지만 Spring에서 RestTemplate을 지속적으로 향상시키기보다 deprecated 한다는 이야기를 들었습니다. 그래서 다른 친구가 없을까 싶어 찾던 중 feign이라는 친구를 알게되었습니다. 간단한 사용 후기를 작성해보겠습니다.

의존성

ext {
    springCloudVersion = 'Hoxton.RELEASE'    
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}
    
dependencies {
    compile("org.springframework.cloud:spring-cloud-starter-openfeign")
}

spirng-cloud-version 위 사진은 linkRelease Trains의 table에서 가져왔습니다. 위에 내용와 함께해서 ext에 있는 변수를 잘 설정해주면 된다. 저는 spring boot 2.3 버전을 사용하고 있기 때문에 Hoxton으로 설정했습니다.

선언

@EnableFeignClients
@SpringBootApplication
public class FeignTestApplication() {
    public static void main(String[] args) {
        SpringApplication.run(ManagerApplication.class, args);
    }
}

---


@EnableFeignClients(basePackages = "com.test.feign")
@Configuration
public class Config() {

}

첫번째 보이는 것처럼 Main에다가 적용하는 경우에는 특별히 어떤 것들도 하지 않아도 잘 동작하게 된다. 하지만 저 같은 경우는 특정 config 파일을 만들어서 사용하고 싶어서 따로 @EnableFeignClients 어노테이션을 활용했습니다. 그런데 주의할 점은 저렇게 선언한 경우 Main과 같은 패키지에 존재하지 않은 경우에는 제대로 동작하지 않는다. 만약 저 같이 사용하고 싶은 경우 basePackages를 활용해야 한다.

기본 활용

i.can.get.properties.data == localhost:8080

@FeignClient(value = "feignTest", url = "${i.can.get.properties.data}")
public interface httpClient {

    @GetMapping("/test/{number}")
    String getHelloWorldMessage(@PathVariable("number") int number, @RequestParam("firstmsg") String firstMsg, @RequestParam("secondmsg") String secondMsg);
    // Call getHelloWorldMessage(1, hello, world);
    // GET localhost:8080/test/1?firstmsg=hello&secondmsg=world

    @PostMapping
    List<TestNode> createNewMessage(RequestNode requestNode);
    // POST localhost:8080
    // body requestNode

    @PostMapping
    List<TestNode> getMulitParam(@SpringQueryMap Map<String, Object> param);
    // ref: https://cloud.spring.io/spring-cloud-openfeign/reference/html/#feign-querymap-support

    @PostMapping
    List<TestNode> getMulitParamWithBody(@SpringQueryMap Map<String, Object> param, RequestNode requestNode);

}
  1. 기본적으로 interface를 이용해서 만들어야 합니다.
  2. 해당 interface에 @FeignClient를 사용해서 feign를 사용할 수 있는 친구를 만들 수 있습니다.
  3. @FeignClient 안에 여러 값을 넣을 수 있는데 value같은 경우 특정 설정을 할 때 사용할 수도 있고 고유한 값으로 지정해야됩니다. url같은 경우 basePath를 지정하는 방식으로 앞에 prefix를 붙인다고 생각하면 쉬울 것 같다. (properties에서 값을 바로 가져올 수 있다.)

선언된 모습들을 보면 SpringMvc를 생각할 수 있는데 feign에 존재하는 선언방식 중에 Contract라는 요소가 존재하는데 그 모습 중에 하나라고 생각하면 된다. SpringMvcspring-boot-cloud로 선언한 경우 defalut값으로 설정되어 있는데 feign.default라는 다른 Contract를 사용하게 되는 경우 다른 방식으로 선언해서 사용해야 합니다!! 저는 잘 몰랐지만 SpringMvc Contract를 사용하게 되는 경우 annotation도 SpringMvc를 사용해야되기 때문에 위에서 사용하는 것처럼 사용가능합니다!!!

여러개의 paramater를 주입하고 싶은 경우에는 @SpringQueryMap이라는 어노테이션을 사용하면 된다. Map 객체를 만들어서 넣어도 되며 Class를 따로 선언해서 만들어도 된다. 참고링크

기존적으로 feign는 어떤 값을 넣었는데 어떤 어노테이션도 없는 경우 Body라고 인식합니다. 그래서 만약 여러개 파라미터에 값을 넣는다고 해서 무조건 바디로 가는 것이 아닙니다. 꼭 명시적으로 @PathVariable인지, @RequestParam를 통해서 Query String인지 명시해주지 않으면 Method has too many Body parameters 라는 메시지를 아주 많이 보게 될 것 입니다.

커스텀

properties를 이용한 방법, code를 이용한 방법, builder을 이용한 방법. 세 가지가 존재한다. 참고링크

Code

@Configuration
public class FooConfiguration {
    @Bean
    public Contract feignContract() {
        return new feign.Contract.Default();
    }

    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
        return new BasicAuthRequestInterceptor("user", "password");
    }
}

---

public class Foo2Configuration {
    @Bean
    public Contract feignContract() {
        return new SpringMvcContract();
    }

    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
        return new BasicAuthRequestInterceptor("user2", "password2");
    }
}

위와 같은 방식으로 진행하는 경우 특별한 feign client을 지정하지 않고도 모든 feign client에 대해서 설정이 돤다. 그 이유에 대해서는 Spring cloud에서 잘 해주는 것 같다…

만약 특정 clinet에게 적용하고 싶은 밑에 방법을 통해서 사용하면 된다.

@FeignClient(value = "feignTest", url = "${i.can.get.properties.data}")
public interface httpClient1 {
    @GetMapping("/test/{number}")
    String getHelloWorldMessage(@PathVariable("number") int number, @RequestParam("firstmsg") String firstMsg, @RequestParam("secondmsg") String secondMsg);
}

---

@FeignClient(value = "feignTest2", url = "${i.can.get.properties.data}", configuration = {Foo2Configuration.class})
public interface httpClient2 {
    @GetMapping("/test/{number}")
    String getHelloWorldMessage(@PathVariable("number") int number, @RequestParam("firstmsg") String firstMsg, @RequestParam("secondmsg") String secondMsg);
}

이런 방식을 통해서 아무 configuration을 설정하지 않은 경우 @Configuration으로 설정된 FooConfiguration class에서 정의된 설정이 될 것이며 그 외로 자기가 지정을 원하는 경우 밑에 방식처럼 annotation안에 있는 configuration키워드를 통해서 override를 할 수 있다.

Properties

feign:
  client:
    config:
      feignName:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full
        errorDecoder: com.example.SimpleErrorDecoder
        retryer: com.example.SimpleRetryer
        requestInterceptors:
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        decode404: false
        encoder: com.example.SimpleEncoder
        decoder: com.example.SimpleDecoder
        contract: com.example.SimpleContract

feignName@FeignClient(name = "feignName")에 존재하는 특정 client를 지정해서 할 수 있다.

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic

위와 같은 방식으로 선언하는 경우 defalut로 전체적으로 정의하여서 사용할 수가 있다.

만약 @Configurationproperties를 이용해서 설정을 했다고 한다면 properties가 이기게 되는 @Configuration이 이기도록 하고 싶은 경우 feign.client.default-to-properties 값을 false로 바꾸면 된다.

builder

참고자료를 보면 어떤 느낌인지 알 수 있을 것이다.

@Import(FeignClientsConfiguration.class)
class FooController {

    private FooClient fooClient;

    private FooClient adminClient;

        @Autowired
    public FooController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
        this.fooClient = Feign.builder().client(client)
                .encoder(encoder)
                .decoder(decoder)
                .contract(contract)
                .requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
                .target(FooClient.class, "https://PROD-SVC");

        this.adminClient = Feign.builder().client(client)
                .encoder(encoder)
                .decoder(decoder)
                .contract(contract)
                .requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
                .target(FooClient.class, "https://PROD-SVC");
    }
}

@FeignClient를 내부적으로 선언하는 것이 아닌 값을 불러오는 경우에는 설정값을 넣어서 대입하는 경우이다. 변수로 가져오는 FooClient는 모두 현재 @FeignClient로 선언되어 있으면 그 전체적인 설정으로 이런 방식으로 따로따로 설정해서 사용할 수도 있다.

정리

RestTemplate에 조금 익숙해져있었는데 이 친구는 매우 가볍게 설정할 수 있고 사용할 수 있다. 무엇보다 공식 문서가 매우 잘 작성되어 있기에 가볍게 읽고 사용하는대도 사용 중 발생하는 문제들을 금방 해결할 수 있다는 장점이 있다. 공식 문서 외에도 활용성에 대해서 다른 블로그에서도 멋있게 정리해주셔서 참고해서 사용해보면 참 좋을 것 같다.

Ref

우아한형제들 개발자 블로그 - feign 1

우아한형제들 개발자 블로그 - feign 2

spring-cloud-openfeign

openfeign github

snack님의 블로그

Leave a comment