본문 바로가기
프로그래밍/Spring Boot

Spring Cloud OpenFeign 사용하기

by 카프카뮈 2022. 3. 7.

본 글은 아래의 링크를 바탕으로 작성되었다.


Intro

Spring Cloud OpenFeign Docs: https://spring.io/projects/spring-cloud-openfeign

about Feign

Feign은 Netflix에서 개발한 Http client binder이다.

  • Feign은 Spring Cloud echo 시스템에 통합되었다.
  • 그러나 Netflix에서 내부적으로 사용을 중단하며, 해당 프로젝트는 OpenFeign이라는 오픈 소스 커뮤니티의 프로젝트로 이전되었다.
  • 결론적으로, 이후 언급하는 Feign은 Spring Cloud OpenFeign 기반임을 염두에 두어야 한다.

강점

Feign은 기존 Http Client 라이브러리에 비해 여러 가지 강점을 가진다.

  • Feign 사용을 위해서는 인테페이스 작성이 필요하며, 해당 인터페이스는 JPA의 경우처럼 자동으로 구현체가 만들어진다.
  • annotation을 통해 다양한 옵션을 간단히 처리할 수 있다.
  • 구현 코드 없이 로직을 명확히 이해할 수 있다.

비교 대상

Feign 대신 대안으로 사용할 수 있는 여러 라이브러리가 있다. 다만 이 중 작성자가 사용해 본 것은 RestTemplate, WebClient 뿐이다.

  • RestTemplate: 현재 deprecated되어 장기적인 사용이 어려울 수 있다.
NOTE: As of 5.0 this class is in maintenance mode, with only minor requests for changes and bugs to be accepted going forward. Please, consider using the org.springframework.web.reactive.client.WebClient which has a more modern API and supports sync, async, and streaming scenarios.
  • Spring Reactive WebClient: Non-Blocking IO 기반의 비동기식 flow를 지원한다.
    그러나 일반적인 환경에서 사용하는데에 부담이 될 수 있다.
    • 비동기 처리 등에 대한 수요가 크지 않고, 기존의 MVC 구조를 가진 프로젝트에 이질적일 수 있다.
    • 로깅이나 테스트 등이 까다로운 측면이 있었다.
  • Retrofit2: squareup사의 라이브러리로, 안드로이드 환경에서 주로 사용된다.
    • 다만 Spring Boot의 기본 지원 범위가 아니고, 별도로 라이브러리를 추가해야 한다.
      • 그렇기 때문에 지원이나 호환 등에서 불리한 점을 가진다.
    • Feign의 구현 코드와 비교했을 때, 별도의 configuration 클래스가 필요하여 코드량이 증가한다.

개발 전 준비사항

종속성 추가

Maven을 사용할 경우, pom.xml에 아래 코드를 추가한다.

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

또한 spring-cloud-dependencies를 추가해 줘야 한다.

 <dependencyManagement>
     <dependencies>
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Application Class에 설정 추가

main 코드가 있는 클래스에 어노테이션을 추가해야 한다.

@SpringBootApplication
@EnableFeignClients //추가된 부분!!
public class ExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(ExampleApplication.class, args);
    }
}

개발

Feign을 통해 GET Request 보내기

우선 GET 요청을 보내고자 한다.

Feign을 통해 아래와 같이 코드를 작성해 보자.

@FeignClient(value = "test", url = "${api.test.url}")
public interface TestApiClient{
    
    @GetMapping("/api/v1/test/{testId}")
    TestResponse getTest(@PathVariable("testId") Long testId);
}

위처럼 코드를 작성하면, 해당 API에 get 호출을 수행할 수 있다!

아래처럼 controller에 코드를 작성해 보자.

@Controller
@RequestMapping("/api/v1")
public class TestController {
    @Autowired //생성자 주입으로 대체 가능
    private TestApiClient testApiClient;

    @GetMapping("/test/{testId}")
    public ResponseEntity<TestResponse> getTestData(@PathVariable final Long testId) {
        final TestResponse response = testApiClient.getTest(testId);

        return ResponseEntity.ok(response)
                .build();
    }
}

이를 통해 요청을 보내고 그 값을 적절히 반환받을 수 있다.

Feign을 통해 POST/DELETE Request 보내기

아래 코드는 spring cloud open-fegin docs의 예제 코드이다.

@FeignClient("stores")
public interface StoreClient {
    @RequestMapping(method = RequestMethod.GET, value = "/stores")
    List<Store> getStores();

    @RequestMapping(method = RequestMethod.GET, value = "/stores")
    Page<Store> getStores(Pageable pageable);

    @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
    Store update(@PathVariable("storeId") Long storeId, Store store);

    @RequestMapping(method = RequestMethod.DELETE, value = "/stores/{storeId:\\\\d+}")
    void delete(@PathVariable Long storeId);
}
  • 여기에서 RequestMapping의 경우 method 인자값에 따라 XXXMapping(ex: GetMapping)으로 대체할 수 있다.
  • consumes의 경우 들어오는 값을 어떻게 매핑할지를 지정한다. 링크: https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestmapping-consumes
  • Request Body를 넣을 경우, 별도의 어노테이션 없이 객체를 파라미터로 넣어주면 된다. 다만 여러 개의 어노테이션 없는 객체가 인자로 존재하면, 예외가 발생한다.(body는 1개만 가능)

Feign 요청에 header 값 추가하기

아래의 코드를 참고해 보자. (출처: https://github.com/woowabros/feign-apply-experience-sample/blob/master/src/main/java/io/github/mayaul/feign/basic/Step2Client.java)

/**
 * header 를 넣어서 호출 하기
 */
@FeignClient(value = "step2", url = "<https://httpbin.org>", configuration = {HeaderConfiguration.class})
public interface Step2Client {

    @GetMapping(value = "/status/{status}")
    void status(@PathVariable("status") int status);

    @GetMapping(value = "/status/{status}", headers = "key2=value2")
    void status2(@PathVariable("status") int status);

    @GetMapping(value = "/status/{status}")
    void status3(@RequestHeader("key3") String headers, @PathVariable("status") int status);
}

헤더를 넣는데에는 3가지 방법이 있다.

  1. Configuration 클래스를 만들고 추가하기 : 제일 확실한 방법
  2. headers 속성을 Mapping시 넣어주기 : 별도의 Contract 설정이 없다면 적용 안됨
  3. 메소드의 파라미터로 헤더를 넣어주기 : 성공하는 것 확인

더하여, 인증이 필요한 경우 아래처럼 처리해줄 수 있다.

//인증을 위한 Configuration
public class BasicAuthConfiguration {

    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
        return new BasicAuthRequestInterceptor("mayaul", "1234567890");
    }
}

//Feign Client

/**
 * Basic Auth 인증 호출 하기
 */
@FeignClient(value = "step3", url = "<https://httpbin.org>", configuration = {BasicAuthConfiguration.class})
public interface Step3Client {

    @GetMapping("/status/{status}")
    void status(@PathVariable("status") int status);
}

Feign 설정 수정하기(로깅, 연결시간)

application.yml에 아래의 코드를 추가할 수 있다.

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

feign:loggerlevel의 경우, full로 설정하면 아래와 같은 상세한 로그가 나온다.

FULL 옵션시 이렇게 디테일한 로그가 출력된다.

마무리하며

Feign을 사용하면서 여러가지 점에서 만족스러웠다.

  • 코드가 깔끔해서 관리하기 편하고, 요구사항을 명확히 알 수 있었다.
  • 로깅, 옵션 설정, 헤더 설정 등에서 어려움이 적었다.
  • 사내 프로젝트를 진행하면서 Kotlin으로 Feign 코드를 작성했는데, 이때는 더 깔끔해져서 만족했다 :)

다만, 한가지 어려움을 느꼈던 부분이 있다.

  • 테스트가 어렵다. 테스트 관련 문서도 적고, 뭔가 원하는대로 구현하는데에 어려움이 있었다.

그래서 다음 글에서는, Feign을 테스트하는 것에 대해 간단히 생각을 정리해보려고 한다.

 

잘못된 내용이 있다면 댓글 부탁드립니다. 늘 감사합니다.

 

반응형

댓글