Notice
Recent Posts
Recent Comments
Link
«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
관리 메뉴

곰돌이형의 개발일지

Feign Client 적용기 본문

개발 관련 글

Feign Client 적용기

programming-polarbear 2022. 12. 18. 14:16

이번 년 3월에 도입시작해서 4월쯤에 도입을 완료한 feignClient에 대해서 이야기해보려고 합니다. 하지만 도입한 지 얼마 안 되어서 spring 쪽에서 공식적으로 http interface client를 제공해 준다는 발표가 나와서 피눈물을 흘리게 만들기도 했었죠 ㅋㅋ (참고 : https://www.youtube.com/watch?v=A1V71peRNn0) 

 

도입 이유

- RestTemplate Deprecated 예정

spring 5에 추가된 주석에 따르면 추후 버전 업그레이드를 위해서는 resttemplate을 전환할 필요가 있었습니다.

 

더보기

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.

 

사실 주석에서는 Webclient를 대신 사용하라고 되어있었지만 webclient는 기본적으로 non-blocking 방식을 기반으로 개발되었고, mono, flux등 stream 처리에 익숙하지 않다면 학습이나 적용 시에 어려움이 있을 것으로 예상되었습니다. 

또한 non-blocking 방식을 잘 사용하게 된다면 성능 개선이 이루어질 것은 분명했으나, 관련해서 이슈 발생시, 아무래도 blocking 방식보다는 해결이 어려운 점, resttemplate에서 webclient로 전환 시에 많은 코드 변경이 있을 텐데, 관련한 리스크를 감당하기 힘들겠다는 의견이 많았습니다.

 

- FeignClient에서 익숙한 향기가 느껴진다?!

FeignClient 예시

FeignClient의 선언 방식을 보면 spring controller와 거의 똑같이 사용이 가능하다는 것을 알 수 있습니다. 그래서 러닝커브도 낮고, 간편하게 사용이 가능하다고 판단되었습니다.

 

- 코드 간결화

FeignClient vs restTemplate

위의 method는 FeignClient를 사용할때의 예시이고, 아래의 method는 restTemplate를 사용할 때의 예시입니다.

간단한 예시라서 차이가 별로 없어보일 수도 있겠지만, resttemplate에서 일일이 정의해주어야 하는 url부분이나 statusCode에 대한 처리 부분을 FeignClient를 사용하면 분리해서 관리가 가능하여 코드를 더 간결하게 만들 수 있습니다.

전체적으로 써본 느낌은 FeignClient를 쓸때 살짝 RestTemplate을 JPA처럼 쓰는 듯한 느낌이 났습니다.

 

사용방법

- 의존성 추가 (build.gradle)

implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation 'io.github.openfeign:feign-httpclient:10.12'
implementation 'io.github.openfeign:feign-micrometer:10.12'

- spring boot 버전에 맞는 spring-cloud 의존성 추가

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

위의 사이트에서 확인 가능하며, 예제 프로젝트에서는 spring boot 2.7.6 버전을 사용중이었기에 2021.0.5 버전을 사용하였습니다.

 

- 호출 부분 interface 정의 방법

FeignClient annotation 예시

interface에 @FeignClient annotation을 붙여주신다음에, name을 지정해 주시고 (해당 name은 yaml에서 사용되며, 중복되어서는 안 됩니다), FeignClient에서 사용될 기본 url 즉 host를 넣어주시고, base config 이외에 추가로 설정이 필요하다면 config 파일을 정의 후에 configuration에 넣어주시면 됩니다.

 

spring boot application

추가로 application 위에 @EnableFeignClients annotation을 붙여주게 되면 spring에서 bean을 생성시에, @FeignClient annotation이 붙은 interface들을 찾아서 각각 client들을 설정에 맞게 bean으로 생성해 주게 됩니다.

 

Configuration 

설정 가능한 property 설명
loggerLevel none - no logging
basic - request method & url
headers - request, response header만 기록
full - header, body metadata 까지 전부 기록
(주의 : feign client는 모두 logger가 debug level이라 feignClient package를 debug로 해야 로그확인이 가능합니다)
connectTimeout connectTimeout
readTimeout readTimeout
retryer retry 관련 방법을 설정할 수 있는 retryer를 지정가능, 기본적으로는 FeignRetryableException에 대해서 한번 재실행하는 retryer가 들어가 있습니다.
errorDecoder error 발생시 실행하는 class
지정해주지 않았을때 status가 200대 응답이 아닌경우 응답을 에러로 판단하고 FeignException이 나가게 되니 관련해서 설정이 필요합니다.
requestInterceptors request를 보내기 전에 intercept하는 로직을 수행하는 class
(보통 client에서 공통으로 사용하는 헤더나 hmac처리, 인증처리등을 할 때 사용)
defaultRequestHeaders requestHeader에 기본적으로 붙여줄 값들
defaultQueryParameters query에 기본적으로 추가될 값들
decoder404 true : 404를 에러로 판단하지 않고 decode 진행
false : 404일시 errorDecoder 실행
decoder response를 받아서 decode하는 class
기본 : ResponseEntityDecoder<SpringDecoder>
encoder request를 보내기 전에 encode하는 class
기본 : SpringEncoder
contract feignClient 작성방식에 대한 설정
- SpringMvcContract : spring controller처럼 작성
- Feign.Default : feign에서 정의한 방식 (@RequestLine annotation 사용)
capabilities 어떤 추가기능을 사용가능한지에 대한 설정
기본적으로는 MicrometerCapability (metric 사용가능 여부), CachingCapability(Cacheable annotation을 붙여서 caching처리가 가능한지)
metric metric 사용 여부에 관한 boolean 값

 

 

위의 Property들은 아래의 세 가지 방법으로 정의가능하고 기본적으로 우선순위는 1->3 순입니다.

1. @FeignClient(configuration = Config.class)

2. @Configuration을 Config.class에 붙여서 모든 FeignClient에 적용하는 경우

3. yaml (feign.client.config)

 

만약에 yaml파일을 우선으로 적용하고 싶다면 feign.client.default-to-properties를 false로 해주시면 됩니다. (false로 한다면 3->1 순으로 적용)

 

Contract

contract는 Feign에서 사용할 수 있는 표현형식에 대한 설정이고, spring controller에서 사용되는 annotation들을 사용할 수 있는 SpringMVCContract가 있고, @RequestLine을 사용한 Feign.Default contract를 사용할 수 있습니다.

저희 서비스에서는 SpringMVCContract를 사용하였습니다.

SpringMVCContract
Feign.Default Contract

 

RequestInterceptor

RequestInterceptor는 request를 전송하기 전에 request를 변형해서 나갈 수 있는 class들을 정의할 수 있는 기능입니다. 주의점은 interceptor들은 spring에 bean으로 등록되어있어야 한다는 점이고, 제공되는 기본 Interceptor들 중에서는 BasicAuthRequestInterceptor (Authorization관련 헤더들을 붙여주는 interceptor), FeignAcceptGzipEncodingInterceptor(Accept-Encoding 헤더를 붙여주는 interceptor)등이 있습니다.

BasicAuthRequestInterceptor
requestTemplate lambda를 이용한 interceptor 정의 방법

추가로 RequestInterceptor interface를 구현해서 직접 사용자 정의 Interceptor 들을 만들 수도 있습니다. apply라는 method를 구현해서 requestTemplate의 query, url 등을 변경할 수 있습니다. 주로 Hmac 기반 인증 방식을 구현할 때 유용하게 사용하였습니다. 

 

Hmac 기반 인증 방식 구현시 interceptor 예시

 

Exception Handling 방식

FeignClient를 적용 시에 가장 어려웠던 부분이 Exception Handling 부분이었는데요. RestTemplate과 아무래도 처리방식이 조금 다르다고 할 수 있습니다. 

기존의 RestTemplate에서는 ResponseEntity 안에 httpStatus와 body가 들어 있어서 client에서 이를 받아서 처리를 자유롭게 할 수 있었으나, FeignClient에서는 이 부분이 Feign에서 정의한 객체인 Response라는 객체로 받아야 이것과 비슷하게 작동할 수 있었습니다. Response안의 body가 inputStream으로 되어있어서 handle 하기가 쉽지 않고, body를 사용하기 위해서 매번 inputstream으로부터 불러오는 중복코드가 생겨서 Response 객체를 사용하지는 않았습니다. 

 

FeignClient Exception Handling Flow


FeignClient의 Exception Handling 방식을 간단하게 살펴보면 httpRequest에 대해서 IOException이나 RetryableException이 반환된다면 Feign안에 정의된 Retryer를 이용해서 retry를 하게 되고, retry 횟수가 초과되면 propagationPolicy에 따라 exception이 wrapping 되어서 나가거나 Exception이 그대로 나가게 됩니다.

 

응답이 정상적으로 온 경우에는 Return type이 Response type이면 그대로 반환하게 되고, 아니라면 status를 확인해서 이 status가 200대가 아니라면 ErrorDecoder로 가서 exception을 반환하게 되고, 200대라면 Decoder에 가서 returnType으로 decode 되게 됩니다. 

 

따라서 기존에 restTemplate과 같이 status에 따라 처리하는 client나 service에 적용된 로직들을 ErrorDecoder에 옮겨야 되는 상황이었습니다. 또한 ErrorDecoder의 반환 형식은 언제나 Exception 이어야 되기 때문에 관련 로직들을 전부 검사하고 동일 작동을 보장하도록 꼼꼼하게 확인하였습니다.

 

FeignClient에서 기본으로 제공하는 ErrorDecoder

위 부분이 FeignClient에서 기본으로 제공하는 ErrorDecoder이고 해당 부분을 override 받아서 새로 정의해 줄 수도 있는데요. 똑같은 client안에서 method별로 status처리를 따로 처리하기 위해서는 methodKey, 즉 method이름을 가지고 handling 해야 되어서 조금은 적절하지 않은 방법이라 생각되었습니다. 

따라서 저희는 새로운 Annotation을 만들어서 method 별로 따로 처리가 필요할 때는 method위에 annotation을 붙여서 처리하게 했는데, 적용예시는 아래와 같습니다. 

 

exception 처리 관련 annotation 적용 예시

이렇게 class위에 붙이게 되면 class의 모든 method들이 안에 정의된 ExceptionHandler를 이용해서 Exception을 나가게 하였고, method위에 붙이면 해당 method가 정의된 ExceptionHandler를 이용해서 Exception이 나가게 하였습니다. 

그리고 여기는 나와있지는 않으나, 정의된 ExceptionHandler가 없다면 기본 ExceptionHandler를 이용해서 Exception이 나가도록 하였습니다.

(자세한 구현 방식은 https://github.com/hyunbeeds/feigndemo/blob/master/src/main/java/com/polarbear/feigndemo/config/infra/feign/ExceptionHandlingFeignErrorDecoder.java와 repo 참고 부탁드립니다! / 이 방식은 https://arnoldgalovics.com/feign-error-handling/  해당 article에서 참고해서 작성하였습니다.)

 

Monitoring 적용

FeignClient 적용하면서 아주 마음에 들었던 스펙 중에 하나인데, 모니터링을 아주 쉽게 적용할 수 있다는 장점이 있습니다.

아래의 implementation만 추가해 주시면 actuator를 이용해서 간단하게 아래의 속성들을 metric으로 추출할 수 있었는데요.

Client 단위가 아니라 호출하는 API Endpoint 즉 method 단위로 호출 횟수, 호출 최대시간, 에러 횟수등을 뽑아볼 수 있다는 것이 circuitbreaker등에서 뽑을 수 있는 metric들 보다 강력하다고 느껴졌었습니다.

implementation ‘io.github.openfeign:feign-micrometer:10.12’

feign_Client_seconds_(count, max, sum)
feign_Feign_(exception_total, http_error_total)
feign_codec_Decoder_response_size_(count, max, sum)
feign_codec_Encoder_request_size_(count, max, sum)

 

위의 metric 들을 이용해서 아래와 같이 dashboard를 구축할 수 있었습니다.

FeignClient metric 이용한 dashboard 예시

Test 방법

FeignClient 적용 시에 test를 할 수 있는 방법은 spring cloud에서 제공하는 wiremock으로 호출을 제대로 하는지, parameter는 제대로 넘어가는지, 응답 parsing은 제대로 하는지 테스트가 가능합니다.

wiremock 사용을 위해서는 아래 의존성을 추가해 주시면 되고, WireMock 사용 시에 서버가 하나 가상으로 뜨는 것처럼 만들 수 있고, 어떤 api에 대한 응답을 mocking 할 수 있습니다. 

testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'

 

보통 file안에 json이나 xml 응답 body를 넣어두고 이것을 반환하게 한 다음, FeignClient에서 파싱을 잘해서 적절한 값으로 변환해서 반환하는지 확인하거나, Status code에 따라 앞에서 설명드린 ErrorDecoder가 적절한 Exception을 반환하는지 테스트하게 됩니다.

 

아래코드는 저희가 테스트 때 사용하였던 코드라서 참고해 주시면 좋을 것 같습니다. stub으로 응답과 status를 mocking 하는 용도입니다.

@AutoConfigureWireMock(port = 0)
public abstract class AbstractFeignClientTest {

	protected void setStub(String prefix, String fileName) {
		stubFor(get(urlMatching(String.format("^%s.*", Pattern.quote(prefix))))
			.willReturn(aResponse()
				.withHeader("Content-Type", "application/json")
				.withBodyFile(fileName)
			)
		);
	}

	protected void setStubWithErrorBody(String prefix, int status, String body) {
		stubFor(get(urlMatching(String.format("^%s.*", Pattern.quote(prefix))))
			.willReturn(aResponse()
				.withStatus(status)
				.withHeader("Content-Type", "application/json")
				.withBody(body)
			)
		);
	}

	protected void setStubWithStatus(String prefix, int status) {
		stubFor(get(urlMatching(String.format("^%s.*", Pattern.quote(prefix))))
			.willReturn(aResponse().withStatus(status)));
	}
}

 

적용 시 겪었던 Issue들..

마지막으로 저희가 FeignClient를 적용하면서 한번 장애도 겪을뻔하고, 적용 시에 많은 삽질들을 겪었었는데, 그 부분들에 대해서 공유드리겠습니다. 

 

1. followRedirects 설정 관련 이슈

3xx redirect 가 응답으로 왔을 때 feign의 기본 설정이 2xx, 4xx, 5xx가 나올 때까지 redirect를 따라가는 것이 기본 설정이었던 이슈가 있었습니다. 이 부분을 수정하기 위해서는 feign.client.config.xx.followRedirects 설정을 false로 해주시면 3xx status 코드를 resttemplate처럼 적용이 가능했습니다.

 

2.LocalDateTime Parsing 이슈

이 부분은 우아한 형제들 블로그(참고 : https://techblog.woowahan.com/2630/)에서 발견했던 이슈인데, 적용 시에 이 부분을 놓치고 적용했다가 큰 고생을 한 이슈입니다. 아래의 설정 없이 적용할 시 RequestBody, RequestParam 부분에 LocalDateTime이 들어갔을 때, @DateTimeFormat annotation이 제대로 작동하지 않았습니다. 

저희 쪽에서는 아래 설정을 기본설정에 넣어두어서 모두 이 설정을 사용하게 하였습니다.

@Bean
public FeignFormatterRegistrar localDateFeignFormatterRegister() {
    return registry -> {
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setUseIsoFormat(true);
        registrar.registerFormatters(registry);
    };
}

 

3. Exception wrapping 관련 이슈.

저희 부서에서는 circuitbreaker를 적극적으로 활용하고 있었는데, 관련해서 circuitbreaker에서는 recordException을 잡아서 circuit을 여는 exception으로 판단할지 여부를 판단하는 로직이 있습니다.

FeignClient를 적용하면서 RestTemplate에서는 IOException이 발생할 때 ResourceAccessException이라는 exception으로 wrapping 해서 보내주어서 이를 recordException으로 circuitbreaker에서 잡고 있었는데, FeignClient에서는 RetryableException 또는 UNWRAP option인 경우에는 IOException 그대로 떨어져서 관련해서 처리가 필요했습니다.

저희 쪽에서는 Circuitbreaker에서 RetryableException을 Record 하는 것으로 수정하였습니다.

 

4. inputStream 관련 이슈

Feign의 Response에서 body를 받아서 logging을 하려고 했을 때 이 부분이 inputStream이라서 두 번 읽으려고 할시 에러가 나는 이슈였습니다.

이 부분을 찾는데 조금 시간이 걸렸었는데, 그 이유는 특이하게 logger 설정이 DEBUG이고, Feign log 설정이 full인 경우 logger에서 response.body() 부분을 ByteArrayStream으로 변경해서 local 설정에서는 여러 번 읽을 수 있어서 에러가 나지 않지만, 리얼망에서는 로깅설정이 다르기 때문에 읽지 못해서 에러가 났었습니다..

따라서 Feign의 Response의 body를 다룰 때는 이것이 InputStream임을 명심하고 처리해 주시면 좋을 것 같습니다.

 

 

적용 후기..

적용을 할때는 솔직히 말해서.. 기존에 있던 코드들을 전부 동작이 동일한 것을 보장하게 옮기느라 고생은 조금 많이 했던 것 같습니다 ㅎㅎ 하지만 다 옮기고 나서보니, exception처리나, request를 보내기 전처리 (RequestInterceptior), request를 보내는 부분이 확실하게 역할 분리가 되는 모습이라 코드가 간결하고 명확해지는 부분이 있었습니다.

 

또한 신규로 client부분을 작성할때, restTemplate보다는 확연하게 시간이 절약되는 것을 확인할 수 있었어서, 기존 코드를 바꾸는 것 보다는 신규 코드들에 대해서 먼저 적용을 해보시면서 적용 범위를 확대해보시는 건 어떨지 권장드립니다.

 

적용을 다하고 나니.. spring http interface가 나온다는 소식이 나와서 조금 허탈하긴 하였으나, 아직까지는 FeignClient의 편의성을 따라가기에는 발전할 부분이 많아 보였습니다. 추후 추가되는 기능들을 살펴보면서 적용 여부를 고민해보고 있습니다.

 

관련해서 질문이나 구현방식에 대한 문의점이 있으시면 편하게 댓글로 작성 부탁드립니다! 긴글 읽어주셔서 감사합니다.

 

참고 

 

우아한 feign 적용기 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요. 저는 비즈인프라개발팀에서 개발하고 있는 고정섭입니다. 이 글에서는 배달의민족 광고시스템 백엔드에서 feign 을 적용하면서 겪었던 것들에 대해서 공유 하고자 합니다

techblog.woowahan.com

 

Maintainable error handling with Feign clients? Not a dream anymore – Arnold Galovics

 

arnoldgalovics.com