openfeign 소개와 운영 서버 적용기

sangminLeesangminLee
5 min read

Monolithic Architecture에서 MSA로 전환 시 다수의 WAS 간 통신이 복잡해지면 코드 가독성이 저하됩니다. 이를 해결하기 위해 feign을 사용하면 선언적 웹 서비스 클라이언트로 코드 가독성을 개선할 수 있습니다. Feign 설정은 Configuration 클래스나 yml을 통해 가능하며, Client-side Loadbalancing, Retry, Timeout, Request Header 설정을 손쉽게 관리할 수 있습니다. eureka와 resilience4j를 연동해 서비스 등록과 서킷 브레이커 기능을 구현할 수 있습니다. 이 글에서는 feign의 기본 사용법, 구성, 주의사항, 연동 방법 등을 다룹니다.

소개

많은 서비스들이 Monolithic Architecture에서 MSA로 전환함에 따라, 다수의 WAS 간 통신이 빈번해졌습니다. 이러한 통신의 증가로 인해 기존의 RestTemplate과 같은 방법을 사용하여 다양한 요청에 대한 설정을 관리하는 것이 점점 더 복잡해졌고 이로 인해 코드의 가독성도 현저히 저하되곤 합니다.

RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.set("Accept", "application/json");

HttpEntity<String> entity = new HttpEntity<>(headers);

try {
    ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
    return response.getBody();
} catch (HttpClientErrorException e) {
    // Client-side error (4xx)
    System.out.println("Client error: " + e.getStatusCode());
    return null;
} catch (HttpServerErrorException e) {
    // Server-side error (5xx)
    System.out.println("Server error: " + e.getStatusCode());
    return null;
} catch (Exception e) {
    // General error
    System.out.println("Error: " + e.getMessage());
    return null;
}

이러한 코드가 서로 다른 Request 마다 작성해야한다면..?

이러한 문제점을 해결하기 위해 feign이 등장합니다. feign의 특징은 아래와 같습니다.

  • feign은 선언적 웹 서비스 클라이언트

  • 인터페이스 생성하고 어노테이션달고 메서드와 파라미터 지정하는 것으로 사용 가능

  • 선언만 하면 되어서 러닝 커브가 낮음

  • Spring Cloud Loadbalancer 기반으로 Client-side Loadbalancing 가능

의존성

dependencies {
        ...
    implementation(platform('org.springframework.cloud:spring-cloud-dependencies:2022.0.4'))
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
        ...
}

기본 사용

간단한 예제

@SpringBootApplication
@EnableFeignClients
public class MyTestApiApplication {

  public static void main(String[] args) {
    SpringApplication.run(SsgSearchRecommendApiApplication.class, args);
  }
}
  • SpringBootApplication에 @EnableFeignClients 등록
@FeignClient(value = "test", url = "ssg.com")
public interface ForTechPediaApiClient {

  @GetMapping(value = "/")
  String call();

    @GetMapping(value = "/aggregate/inventory/front/user/items/{itemIds}")
  @CollectionFormat(feign.CollectionFormat.CSV)
  List<InventoryResult> inventoryItems(@PathVariable List<String> itemIds, @SpringQueryMap UserParameter userParameter);
}
  • @GetMapping, @PostMapping, @PutMapping, @DeleteMapping 등 spring MVC annotation 사용 가능

  • @PathVariable을 이용해 path에 값을 넣거나 @SpringQueryMap을 이용해 query string을 넣을 수 있음

@RestController
@RequiredArgsConstructor
public class ForTechPediaApiTestController {

  private final ForTechPediaApiClient client;

  @PostMapping(value = "/test/techpedia/")
  public String test() {
    return client.call();
  }

    @PostMapping(value = "/test/techpedia2/")
  public List<InventoryResult> test2(@RequestBody BaseRequest baseRequest) {
        return client.inventoryItems(List.of("1000547588070", "1000554084667", "1000555215927"), baseRequest.getUserInfo());
  }
}
  • 사용하는 쪽에서는 해당 Client 주입받고 그냥 메서드 호출만 하면 끝!

Configuration

  • 구성속성 yml에서 설정하는 방법, Configuration 클래스 생성하는 방법 있음

  • 환경별로 설정이 다르게 하진 않을 것 같아서 보다 명시적인 클래스 생성 방식 택함

  • 각 FeignClient 별로 Configuration을 지정해줘서 상이하게 설정 가능

public class DefaultFeignConfiguration {
  @Bean
  public RequestInterceptor requestInterceptor() {
    return requestTemplate -> {
      requestTemplate.header("Content-Type", "application/json");
      requestTemplate.header("Connection", "Keep-alive");
    };
  }

  @Bean
  public Retryer retryer() {
    // 1s 대기, 0ms 반복 3번
    return new Retryer.Default(0, SECONDS.toMillis(1), 3);
  }

  @Bean
  public Request.Options options() {
    return new Request.Options(Duration.ofMillis(350), Duration.ofMillis(550), true);
  }

  @Bean
  public Logger.Level logger() {
    return Logger.Level.BASIC;
  }
}
@FeignClient(url = "ssg.com", configuration = DefaultFeignConfiguration.class)
public interface ForTechPediaApiClient {...}

아래서 설명하겠지만 @Configuration이 붙으면 default로 설정

Request Header 설정

  1. Configuration

     public class DefaultFeignConfiguration {
    
       @Bean
       public RequestInterceptor requestInterceptor() {
         return requestTemplate -> {
           requestTemplate.header("Content-Type", "application/json");
           requestTemplate.header("Connection", "Keep-alive");
         };
       }
       ...
     }
    
  2. Annotation

     @FeignClient(url = "ssg.com")
     public interface ForTechPediaApiClient {
    
       @GetMapping(value = "/", headers = "Content-Type=application/json")
       String call();
    
     }
    

timeout 설정

public class DefaultFeignConfiguration {

    @Bean
  public Request.Options options() {
    return new Request.Options(Duration.ofMillis(350), Duration.ofMillis(550), true);
  }
  ...
}
  • 첫번째 파라미터: connection timeout

  • 두번째 파라미터: read timeout (socket timeout)

  • 세번째 파라미터: status code 3xx 일때 redirect 여부 (default: true)

retry 설정

public class DefaultFeignConfiguration {

    @Bean
  public Retryer retryer() {
    return new Retryer.Default(0, SECONDS.toMillis(1), 3);
  }
  ...
}
  • 최대 3번 retry

  • retry interval 0ms 씩 증가

  • retry interval 최대 1000ms까지 증가

로그 설정

public class DefaultFeignConfiguration {

    @Bean
  public Logger.Level logger() {
    return Logger.Level.BASIC;
  }
  ...
}
  • NONE : 로깅 X (DEFAULT)

  • BASIC: Request Method, URL, HTTP Status code, 실행 시간

  • HEADERS : BASIC + Response Header

  • FULL : BASIC + HEADERS + Body + 메타데이터

주의사항

  • 로그 설정 시 yml 설정

      logging.level.<packageName>.<className> = DEBUG
    

  • @Configuration 붙이는 경우 전역 설정

기타

  • Decoder Bean을 등록해서 HttpMessageConverter에 다른 ObjectMapper를 사용할 수 있음 링크

  • BasicAuthRequestInterceptor Bean을 등록해 auth 설정 가능

  • @Cacheable를 feign client 인터페이스의 메서드에 붙여 연동 가능하다고함

  • 공식적인 reactive client는 제공하지 않음

  • refresh 사용으로 되어있으면, connectionTimeout, readTimeout을 POST /actuator/refresh 로 변경 가능

운영 환경에서 활용

eureka discovery 연동

  • eureka와 연동 시, @FeignClient에서 url을 별도로 입력하지 않아도 된다

  •           @Feignclient(value = "TEST-API", configuration = QueryApiFeignConfiguration.cLass)
              public interface TestApiclient {
                @CircuitBreaker(name ="default", fallbackMethod ="falLback") 
                @PostMapping(value ="/ys/search/target/reconmend_advert_item")
                SearchResponse searchResultForAdvert(SearchRequest searchRequest);
              }
    

사용 예시설명
1@FeignClient(name="testClient", url="http://localhost:8081")
or
@FeignClient(name="testClient", url="${external-url.testClient}") external-url.testClient=http://localhost:8081
- 지정한 url로 request
- 로드밸런싱 X
2@FeignClient(name="testClient")spring.cloud.openfeign.client.config.testClient.url=http://localhost:8081- 구성속성에 지정된 url로 request
- 로드밸런싱 X
- refresh-enabled인 경우, POST /actuator/refresh 로 실행 중 변경 가능
3@FeignClient(name="testClient")- name에 지정된 서비스명을 eureka로 부터 받아와 request
- client-side 로드밸런싱 O

주의사항

eureka:
  client:
    enabled: true
    service-url:
      defaultZone: <http://11.111.11.111:8761/eureka/>
    register-with-eureka: false  # false로 하여 로컬이 유레카에 등록되지 않도록 함
    healthcheck:
      enabled: true
  instance:
    instanceId: ${spring.application.name}-qa
    preferIpAddress: true
  • eureka enable 켜져있어야하는지 확인 필요

  • euraka를 통하기 때문에 service-url에서 eureka 값이 지정되어있어야함

  • 로컬 IDE에서 실행할 때, eureka.client.register-with-eureka: false

resilience4j 연동

fallback

  • feign의 fallback

  • resilience4j의 fallback

  • CircuitBreaker, TimeLimiter 설정은 기본 resilience4j와 동일하게 yml에서 구성설정

  • 메서드 별로 CircuitBreaker, TimeLimiter을 지정하려면 <feignClientClassName>#<calledMethod>(<parameterTypes>) 에 맞춰 구성설정 instance에 지정

circuitbreaker

spring:
  cloud:
    openfeign:
      circuitbreaker:
        enabled: true
        alphanumeric-ids:
          enabled: false
      httpclient:
        max-connections: 200
        max-connections-per-route: 60
        time-to-live: 2000

resilience4j:
  circuitbreaker:
    configs:
      default:
        slidingWindowSize: 10
        permittedNumberOfCallsInHalfOpenState: 1
        minimumNumberOfCalls: 5
        waitDurationInOpenState: 10s
        failureRateThreshold: 50
        eventConsumerBufferSize: 10
        registerHealthIndicator: true
    instances:
      PersonalizationApiClient#personalizationItems(String,List):
        slidingWindowSize: 50
        permittedNumberOfCallsInHalfOpenState: 10
        minimumNumberOfCalls: 50
        waitDurationInOpenState: 30s
        failureRateThreshold: 30
        eventConsumerBufferSize: 10
        registerHealthIndicator: true
  timelimiter:
    configs:
      default:
        timeoutDuration: 5000ms

참고

https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-feign.html

https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/

resilience4j

https://mangkyu.tistory.com/289

https://arnoldgalovics.com/spring-cloud-feign-resilience4j-timelimiter/

https://arnoldgalovics.com/tag/spring-cloud-openfeign/

1
Subscribe to my newsletter

Read articles from sangminLee directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

sangminLee
sangminLee