BackEnd/SpringBoot

Feign Client ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์™ธ๋ถ€ํ†ต์‹  ์—ฐ๋™ํ•˜๊ธฐ

hyunki.Dev 2023. 4. 22. 18:07

๐Ÿ“Œ ๋“ค์–ด๊ฐ€๋ฉฐ

๋ฐฑ์—”๋“œ API ๊ฐœ๋ฐœ์„ ํ•˜๋‹ค๋ณด๋ฉด ์™ธ๋ถ€ ์„œ๋น„์Šค์™€ ์—ฐ๋™ํ•ด์•ผ ํ•˜๋Š” ์ผ์ด ๋งŽ์ด ์ƒ๊น๋‹ˆ๋‹ค. ํŠนํžˆ ํ˜„์žฌ ์ฃผ๋ฌธ/๊ฒฐ์ œ ๋„๋ฉ”์ธํŒ€์— ์žˆ๋‹ค๋ณด๋‹ˆ ๋”๋”์šฑ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ ์™ธ๋ถ€ํ†ต์‹ ์„ ํ•ด์•ผํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์ด ์ƒ๊ธฐ๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ์™ธ๋ถ€ํ†ต์‹ ์„ ๋„์™€์ฃผ๋Š” ๋งŽ์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค(HttpClient, WebClient) ์ค‘ Feign Client ์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค. 

 


๐Ÿ“Œ Feign Client 

Feign Client๋ž€ Netflix์—์„œ ๊ฐœ๋ฐœํ•œ Http Client์ž…๋‹ˆ๋‹ค.

(HttpClient๋Š” Http ์š”์ฒญ์„ ๊ฐ„ํŽธํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์„œ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ๊ฐ์ฒด)

์ฒ˜์Œ์—๋Š” Netflix์—์„œ ์ž์ฒด์ ์œผ๋กœ ๊ฐœ๋ฐœ์„ ์ง„ํ–‰(Spring Cloud Netflix Feign)ํ–ˆ์ง€๋งŒ ํ˜„์žฌ๋Š” ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ์ธ  OpenFeign์œผ๋กœ ์ „ํ™˜ํ–ˆ์œผ๋ฉฐ SpringCloud ํ”„๋ ˆ์ž„์›Œํฌ์˜ Spring Cloud OpenFeign์— ํ†ตํ•ฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

์ด๋Ÿฌํ•œ Feign Client์˜ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋Š” ์ด์œ ๋ฅผ ๊ทธ ํŠน์ง•๊ณผ ํ•จ๊ป˜ ์•Œ์•„๋ณด๋ฉด

  • SpringMvc์—์„œ ์ œ๊ณต๋˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (Spring Cloud์˜ starter-openfeign์„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ)
  • RestTemplate ๋ณด๋‹ค ๊ฐ„ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ(interface ์™€ annotation๋งŒ ์„ ์–ธํ•˜๋ฉด ๊ตฌํ˜„์ฒด๊ฐ€ ์ƒ๊น€) ๊ฐ€๋…์„ฑ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
  • ๋™๊ธฐ์ ์œผ๋กœ ์ž‘๋™ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์™ธ๋ถ€ ์„œ๋น„์Šค์˜ ์‘๋‹ต์ด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ํฌํ•จ๋˜์–ด ๋ฐ˜๋“œ์‹œ ์‘๋‹ต์„ ๋ฐ›์•„์•ผ API๋ฅผ ์„ฑ๊ณต ์ฒ˜๋ฆฌ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ ์‚ฌ์šฉํ•˜๊ธฐ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“Œ Feign Client  ์‹œ์ž‘ํ•˜๊ธฐ

์šฐ์„  Spring Cloud ๊ด€๋ จ ํŒจํ‚ค์ง€๋“ค์˜ ๋ฒ„์ „์— ๋งž๋Š” ์˜์กด์„ฑ ์ž๋™ ์„ค์ •์„ ์œ„ํ•ด spring-cloud-dependencies ๋ฅผ ๋“ฑ๋กํ•˜๊ณ  openfeign dependency๋„ ์ถ”๊ฐ€ํ•ด์ค๋‹ˆ๋‹ค.

ext {
	// [2021-07-16] Dependency management for Spring Cloud AWS.
	springCloudVersion = '2021.0.5'
	// spring cloud version (Spring Boot 2.7.x compatibility)
	springCloudAwsVersion = '2.4.2'
}

dependencyManagement {
	imports {
		mavenBom "io.awspring.cloud:spring-cloud-aws-dependencies:${springCloudAwsVersion}"
		mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
	}
}


dependencies {
    //########## OpenFeign ##########//
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
    implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.10.0'
    testImplementation 'org.springframework.cloud:spring-cloud-contract-wiremock'
}

 

์Šคํ”„๋ง ๋ถ€ํŠธ ๋ฒ„์ „์— ๋งž๋Š” spring-cloud ๋ฒ„์ „์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋ฏ€๋กœ ์ž์‹ ์˜ ์Šคํ”„๋ง๋ถ€ํŠธ ๋ฒ„์ „์— ๋งž๋Š” depnedency ์„ค์ •์„ ํ•ด์ค๋‹ˆ๋‹ค.

https://spring.io/projects/spring-cloud

 

Spring | Home

Cloud Your code, any cloud—we’ve got you covered. Connect and scale your services, whatever your platform.

spring.io

๊ฐ์ž์˜ ํ”„๋กœ์ ํŠธ์— ์ •ํ™•ํ•˜๊ฒŒ ๋งž๋Š” ๋ฒ„์ „ ์ •๋ณด๋ฅผ ์–ป๊ณ ์ž ํ•œ๋‹ค๋ฉด ์œ„์˜ ๋งํฌ๋ฅผ ์ฐธ๊ณ  ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

build.gradle์„ ์ˆ˜์ •ํ•˜์—ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋‹ค์šด ๋ฐ›์•˜๋‹ค๋ฉด OpenFeign ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ ์Šค์บ”์„ ์œ„ํ•ด

Application์— @EnableFeignClients ๋ฅผ ๋ถ™์—ฌ์ค๋‹ˆ๋‹ค.

 

@EnableFeignClients()
@ServletComponentScan
@SpringBootApplication
public class SearchApplication {

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

}

 

๊ทธ ๋‹ค์Œ FeignClient ๋ฅผ ํ†ตํ•ด ์š”์ฒญํ•  ์™ธ๋ถ€ํ†ต์‹  ์ŠคํŽ™์— ๋งž๊ฒŒ interface๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

@FeignClient(name = "SearchNaverFeignClient", url = ENDPOINT_NAVER_URL)
public interface SearchNaverFeignClient {

  /**
   *
   * @param naverClientId
   * @param naverClientSecret
   * @param query
   * @param display
   * @param start
   * @param sort
   * @return
   */
  @GetMapping("/v1/search/blog.json")
  NaverResultDto getSearchResult(
      @RequestHeader(NAVER_CLIENT_ID) String naverClientId,
      @RequestHeader(NAVER_CLIENT_SECRET) String naverClientSecret,
      @RequestParam("query") String query,
      @RequestParam("display") Integer display,
      @RequestParam("start") Integer start,
      @RequestParam("sort") String sort);

}

 

EndPoint url์„ application.yaml ํŒŒ์ผ์—์„œ ๊ฐ€์ ธ์™€ ์ ์šฉํ•˜๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

external:
user-service:
  host: 'https://user.me'

@FeignClient(name = "userClient", url = "${external.user-service.host}")

 

๋˜ํ•œ feignClient์˜ ์žฅ์  ์ค‘ ํ•˜๋‚˜๋กœ Client์— ์ปค์Šคํ…€ Configuration์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ๋“ค์–ด ํ•ด๋‹น ์™ธ๋ถ€ ์„œ๋น„์Šค์— API ๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ๋ฌด์กฐ๊ฑด ๊ณตํ†ต์œผ๋กœ ๋“ค์–ด๊ฐ€์•ผํ•˜๋Š” header๊ฐ€ ์žˆ๊ฑฐ๋‚˜

๊ณตํ†ต Response๊ฐ€ ์žˆ์–ด์„œ ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ decodeํ•ด์˜ค๊ฑฐ๋‚˜ ๋“ฑ์˜ ํ–‰์œ„๋“ค์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

ex) ํ•ด๋‹น client์—์„œ api ํ˜ธ์ถœ ํ•  ๋•Œ๋งˆ๋‹ค header์— ๊ฐ’ ๋„ฃ๊ธฐ

  public class HumanFeignClientConfig {
      @Bean
      public RequestInterceptor requestInterceptor() throws InterruptedException {
          return requestTemplate -> requestTemplate.header("header-name", "header-value");
      }
  }

์ด๋ฅผ feignClient interface์— ์ ์šฉํ•˜๊ธฐ

  @FeignClient(name = "humanClient", url = "${external.human-service.host}", configuration = HumanFeignClientConfig.class)
  public interface HumanClient {

      @GetMapping(value = "/human/list")
      List<HumanInfo> getHumans(@RequestParam("name") String name);
  }

 

์ด์™ธ์—๋„ ๋‹ค์–‘ํ•œ ์„ค์ •๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค. (Decoder, Encoder, Logger ๋“ฑ)

๐Ÿ“Œ Feign Client  readTimeout connectionTimeout

readTimeout ๊ณผ connectionTimeout์€ ์™ธ๋ถ€ ํ†ต์‹  ์—ฐ๋™ ์‹œ

๊ฐ€์žฅ ์‹ ๊ฒฝ์จ์•ผ ํ•˜๋Š” ํ•„์ˆ˜์ ์ธ ์„ค์ • ๋ถ€๋ถ„์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

FeignClient์—์„œ๋Š” ์ด๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

default๋กœ ๋ชจ๋“  FeingClient interface์— ๊ณตํ†ต์ ์œผ๋กœ ์ ์šฉํ•˜๊ฑฐ๋‚˜

์•„๋ž˜์™€ ๊ฐ™์ด ํŠน์ • feignClient interface ๋งˆ๋‹ค ๊ฐ’์„ ๋‹ค๋ฅด๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

feign:
  httpclient:
    enabled: true
    maxConnections: 1000
    maxConnectionsPerRoute: 1000
  client:
    config:
      default:
        retry-period: 500
        retry-max-period: 1000
        max-attempts: 3
        connectTimeout: 3000
        readTimeout: 30000
      inicis:
        retry-period: 500
        retry-max-period: 1000
        max-attempts: 1 #(์‹ค์ œ ์‹œ๋„ + ์žฌ์‹œ๋„ ํšŸ์ˆ˜์ด๋ฏ€๋กœ ์žฌ์‹œ๋„ ์—†์Œ)
      naver:
        retry-period: 500
        retry-max-period: 1000
        max-attempts: 2

 

CustomRetryer ์ƒ์„ฑ ํ•˜์—ฌ readTimeout ๋ฐœ์ƒ ์‹œ์—๋Š” retry ํ•˜์ง€ ์•Š๊ณ 

connectionTimeout ๋ฐœ์ƒ ์‹œ์—๋งŒ retry ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

public interface CustomRetry extends Retryer {

    @Slf4j
    class CustomRetryer implements CustomRetry {
        private final int readTimeoutRetries;
        private final int connectTimeoutRetries;

        private int attempt;

        public CustomRetryer(int readTimeoutRetries, int connectTimeoutRetries) {
            this.readTimeoutRetries = readTimeoutRetries;
            this.connectTimeoutRetries = connectTimeoutRetries;
        }

        @Override
        public void continueOrPropagate(RetryableException e) {
            if (e.getCause() instanceof IOException && e.getMessage().toLowerCase(Locale.ROOT).contains("read")) {
                log.error("Read timed out occurred. count: {}", attempt);
                throw new ApiException(ResponseCode.READ_TIMEOUT);
            } else if (e.getCause() instanceof IOException && e.getMessage().toLowerCase(Locale.ROOT).contains("connect")) {
                log.error("Connect timed out occurred. init: {}", attempt);
                if (attempt++ < connectTimeoutRetries) {
                    log.error("Connect timed out occurred. Retry count: {}", attempt);
                    return;
                }
            }
            throw e;
        }

        public Retryer clone() {
            return new CustomRetryer(readTimeoutRetries, connectTimeoutRetries);
        }
    }
}

 

 

@Slf4j
@Configuration
public class FeignConfig {

    @Value("${feign.client.config.max-attempts}")
    private int maxAttempts;

    /**
     * Debug log level
     */
    @Bean
    Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }

    @Bean
    public RequestInterceptor requestInterceptor(){
        return requestTemplate -> {
            requestTemplate.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
            requestTemplate.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
        };
    }

   
    /**
     * retry ์„ค์ •
     */
    @Bean
    public Retryer retryer(){
        return new CustomRetry.CustomRetryer(MAX_READ_TIMEOUT_TRY_COUNT, maxAttempts);
    }

}

 

์ด ์™ธ์—๋„ FeingClient ์‚ฌ์šฉ์„ ์œ„ํ•œ ๋‹ค์–‘ํ•œ ์„ค์ • ๋ฐฉ๋ฒ•๋“ค์— ๋Œ€ํ•ด์„œ๋Š” ๊ธฐํšŒ๊ฐ€ ๋˜๋Š”๋Œ€๋กœ ์ •๋ฆฌํ•˜์—ฌ ํฌ์ŠคํŒ…ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

 

์ฐธ๊ณ :

https://isntyet.github.io/java/feign-client-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0/

https://mangkyu.tistory.com/279

https://forkyy.tistory.com/10