Server/Spring Boot

FeignClient Decoder 탐구

taemin 2023. 10. 29.

배경

개발 시 http 통신을 위해 feign 클라이언트를 자주 사용합니다.

feign 은 Encoder, Decoder, Errordecoder 등을 빈으로 등록해 두고 우리가 따로 커스텀한 빈을 등록해서 사용하지 않은 이상 디폴트로 세팅된 빈을 가지고 동작합니다.

 커스텀해서 사용할 수 있는데 그중 Decoder 에 대해 알아보겠습니다.

 

버전

spring clude starter openfeign 3.0.6

 

decoder 역할

feignClient 사용 시 우리는 반환타입을 결정합니다. 보통 ResponseEntity 클래스로 받거나, 리턴 객체를 지정할 것입니다.

decoder는 http 응답을 우리가 지정한 반환객체 타입으로 변환해 주는 역할을 합니다

interface FooClient {
    //반환타입이 객체
    @GetMapping("/v1/foo")
    fun getFoo() : Foo
    
    //반환타입이 ResponseEntity
    @GetMapping("/v1/foo")
    fun getFoo() : ResponseEntity<Any>
    
    //반환타입이 Response
    @GetMapping("/v1/foo")
    fun getFoo() : Response
}

 

Decoder 영향 범위

Decodes an HTTP response into a single object of the given type.
Invoked when Response.status() is in the 2xx range and the return type is neither void nor Response.

Response.status()가 2xx 범위에 있고, 반환 유형이 void 또는 Response가 아닌 경우 사용됩니다.

즉 성공적인 응답일 경우에만 decoder를 통해 반환타입으로 디코딩시켜 주고, 나머지의 경우 decoder 가 아닌 (3xx,4xx,5xx) ErrorDecoder를 통해 처리됩니다. 

 

Default Decoder

디폴트로 등록되어있는 Decoder의 동작을 알아보겠습니다.

FeignClientsConfiguration 클래스를 확인해 보면 아래 코드를 확인할 수 있습니다.

public class FeignClientsConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public Decoder feignDecoder() {
        return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
    }
}

filter-chain 패턴이 적용되어 있고, decoding 이 위 순서대로 적용되게 되어있습니다.

일단 반환타입이 Optional 클래스인지 확인해서 맞다면 OptionalDecoder에서 처리하고 아니라면 다음 Decoder(ResponseEntityDecoder)로 넘깁니다. 

 

ResponseEntityDecoder는 아래와 같이 동작합니다

1. 반환타입이 ResponseEntityClass or HttpEntityClass 라면 SpringDecoder를 통해 반환객체를 만든 후 ResponseEntity 클래스로 래핑 해서 반환합니다.

2. 그 외라면 SpringDecoder를 통해 반환객체를 만든 후 반환객체 그대로 리턴합니다.

 

SpringDecoder는 빈으로 등록되어 있는 HttpMessageConverter 리스트를 통해 반환객체를 생성합니다.

 

 

Decoder를 커스텀해 사용한다면?

decoder를 하나 만들어 빈으로 등록한 후, feignClient에서 그 커스텀한 decoder 빈을 사용하도록 하면 됩니다.

하나의 방식 아래처럼 filter-chain 패턴 형태 그대로 앞단에 Custom Decoder 객체를 만드는 것 입니다.

public class FeignClientsCustomConfiguration {
    ...
    
    @Bean
    public Decoder feignDecoder() {
        return new CustomDecoder(new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters))));
    }
    
    ...
    
}
public class CustomDeCoder extends Decoder {
    private Decoder decoder;
    
    public CustomDeCoder(Decoder decoder) {
        this.decoder = decoder;
    }

    @Override
    public Object decode(Response response, Type type) throws IOException {
      
      //서비스 로직
      
      return this.decoder.decode(response, type);
      
    }
}

 

Decoder 커스텀 사용 예시

spring에서 제공되는 HttpStatus 클래스를 사용하면 커스텀한 status 코드를 사용할 수 없습니다.

예를 들어 HttpStatus로 정의되어있지 않은 233 같은 클래스를 만들려고 하면 IllegalArgumentException 가 발생합니다.

feign client를 사용해서 호출한 외부 api에서 커스텀한 status를 제공한다면 문제가 발생할 수 있습니다.

 

예를 들어 외부 api 성공응답이 233이고 우리 서비스에선 status를 통해 뭔가 로직을 컨트롤하고 싶습니다.

- (ex. 응답이 233 일 땐 요렇게 동작하고~ 아닐 경우 다르게 동작하고 싶은 요구사항 )

그러면 응답의 status를 알기 위해 반환타입을 ResponseEntity로 만들게 됩니다.

2xx 대 응답이기 때문에 ResponseEntityDecoder에서 처리하게 됩니다.

ResponseEntityDecoder는 위 설명처럼 디코딩 후 ResponseEntity를 만들어 리턴하도록 동작합니다.

여기서 문제는 ResponseEntityDecoder는 ResponseEntity를 만들 때 스프링이 제공하는 HttpStatus를 사용한다는 것입니다.

HttpStatus는 233 같은 커스텀 코드를 처리할 수 없어 에러를 발생시킵니다.

디코딩 실패로 DecodeException 이 발생하며 의도하지 않은 방향으로 서비스가 동작합니다.

 

해결방법으론 여러 가지가 있겠지만 위처럼 커스텀 디코더를 만들어서 해결했는데요.

스프링이 제공하는 응답코드가 HttpStatus 가 아닐 경우 ResponseEntityDecoder를 사용하지 않고 커스텀한 CustomResponseEntityDecoder로 처리되도록 했습니다.

CustomResponseEntityDecoder는 ResponseEntityDecoder과 로직이 모두 똑같고, 

단 하나 다른 것은, ResponseEntity를 만들 때 HttpStatus 클래스를 사용하지 않도록 하였습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

댓글