티스토리 뷰

 

꽤 오래전에 Http 통신을 하기 위한 Spring/Java 라이브러리들을 소개 한 적이 있다.

 

2022.12.18 - [개발/JAVA] - [JAVA] HttpURLConnection, HttpClient, okHttp

2022.12.26 - [개발/SPRING] - [Spring] RestTemplate, WebClient

 

당연한 이야기지만 Http 통신을 위한 client들은 정말 많다.

 

그중에서 최근에는 OpenFeign을 많이 쓰고 있다.(한동안은 RestTemplate을 썼었다)

 

OpenFeign은 Netflix에서 사용되다가 Spring Cloud에 추가된 Http 통신을 위한 라이브러리이다.

OpenFeign의 초기 모델인 Feign은 Eureka, Ribbon 등을 포함하는 Netflix OSS 프로젝트의 일부로써 Netflix에 의해 만들고 공개되었다. Netflix OSS가 공개되고 나서 Spring Cloud 진영은 Spring Cloud Netflix라는 프로젝트로 Netflix OSS를 Spring Cloud 생태계로 포함시켰는데, Feign은 단독으로 사용될 수 있도록 별도의 starter로 제공되었다. 이후에 넷플릭스는 내부적으로 feign의 사용 및 개발을 중단하기로 결정하였고, OpenFeign이라는 새로운 이름과 함께 오픈소스 커뮤니티로 넘겨졌다. Spring Cloud는 Open Feign 역시 스프링 클라우드 생태계로 통합하였고, 이 과정에서 기존의 Feign 자체 어노테이션과 JAX-RS 어노테이션만 사용 가능했던 부분을 Spring MVC 어노테이션을 지원하도록 추가했다.
출처: https://mangkyu.tistory.com/278

 

라이브러리가 오픈 소스화 된 초창기에는 딱히 장점을 못 찾았었는데, 현재는 많이 개선되서 가독성이나 사용성이 모두 좋아지면서 주로 사용하게 됐다.

 

사용법은 여러가지가 있는데, 공식 사이트에서 잘 설명해주고 있다.

 

사용법을 알아보기에 앞서 openfeign 버전과 스프링 버전 호환을 잘 확인하자.

 

 

간단 사용법

공식 사이트에 나와있지 않지만 난 아래와 같은 방식을 가장 선호한다.

@FeignClient(name = "OpenAiClient", url = "https://api.openai.com/v1", configuration = OpenAiHeaderConfig.class)
public interface OpenAiFeignClient {
    @PostMapping("/embeddings")
    EmbeddingResponseDto createEmbedding(@RequestBody EmbeddingRequestDto embeddingRequestDto);
    @PostMapping("/chat/completions")
    ResponseChatDto chatCompletion(@RequestBody ChatRequestDto chatRequestDto);
}

인터페이스로 client를 생성해서 서비스로직에서 해당 매서드를 가져다 쓰면 된다.

private final OpenAiFeignClient openAiFeignClient;

ResponseChatDto responseChatDto = openAiFeignClient.chatCompletion(...);

코드를 읽기도 쉽고 사용하기도 쉬워서 이렇게 사용한다.

 

그런데 문제가 하나 있다.

 

HTTP 요청이 성공할 때는 객체에 정상적으로 매핑된다.

 

그런데 성공 이외의 요청 즉, HTTP status code 200 이외의 모든 요청이 전부 익셉션으로 처리된다.

 

이 익셉션은 FeignException 분류되는데, 여러가지 처리 방법이 있다. 

 

1. try/catch

2. controllerAdvice

3. ErrorDecoder

 

1,2번은 사실 보편적인 익셉션 핸들링 방법이다. 3번을 위한 빌드업이랄까.

 

하나씩 알아보자.

 

1. try/catch

앞서 언급한 FeignException 을 받아서 처리하면 된다.

 

그리고 일반 Exception과는 다르게 HttpStatusCode와 에러 정보를 담고 있는 content를 받아올 수 있다.

public ResponseEntity<Object> requestFeignClient() {
    try {
        FeignResponseDto feignResponseDto = feignClient.someRequest();

    } catch (FeignException e) {
        if(HttpStatus.BAD_REQUEST.value() == e.status()) {
            return new ResponseEntity<>("실패 사유를 아는 실패 : " + e.contentUTF8(), HttpStatus.BAD_REQUEST);
        } else {
            throw new ResponseEntity<>("실패 사유를 모르는 실패", HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    return ResponseEntity.ok("성공");
}

contentUTF8 매서드로 스트링 형태로 결과물을 가져온다. content 매서드는 deprecated 되었다.

 

위 코드는 해당 요청 하나에서 발생하는 FeignException을 처리하기 위한 방법이다.

 

공통적으로 처리하기 위해선 ControllerAdvice에서 동일한 처리를 해주면 된다.

 

2. ControllerAdvice

@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {
   ...
    
    @ExceptionHandler(FeignException.class)
    public ResponseEntity<Object> feignException(FeignException e) {
        if(HttpStatus.BAD_REQUEST.value() == e.status()) {
            return new ResponseEntity<>("실패 사유를 아는 실패 : " + e.contentUTF8(), HttpStatus.BAD_REQUEST);
        } else {
            return new ResponseEntity<>("실패 사유를 모르는 실패", HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

FeignException 익셉션 핸들링을 ControllerAdvice에서 처리하도록하면 FeignException  을 한 곳에서 핸들링할 수 있을 것이다.

 

단, 서버가 하나의 서버와 요청을 주고 받을 때만 이와 같이 구성하는게 좋다.

 

모든 FeignException에 동일한 응답을 하게 되기 때문이다.

 

하지만 보통 다양한 외부 서버로 요청을 주고 받기 때문에, 일반적인 상황에서 사용하기엔 적절하지 않다.

 

매번 서비스 로직에 익셉션 핸들링을 위한 코드를 남기지 않는 있을까? 하고 알아보다가 다음 방법을 알게 됐다.  

 

3. ErrorDecoder

Feign 클라이언트를 어노테이션 방식이 아니라 수동으로 구성하게되면 에러 디코더를 설정해 줄 수 있다.

public class Example {
  public static void main(String[] args) {
    MyApi myApi = Feign.builder()
                 .errorDecoder(new MyErrorDecoder())
                 .target(MyApi.class, "https://api.hostname.com");
  }
}

에러 핸들링을 서비스로직 외부에서 하도록 클라이언트에 지정해 줄 수 있다는 의미다.

 

이런 저런방법을 찾아보다가, configuration 옵션을 주면 ErrorDecoder를 받아올 수 있었다.

@FeignClient(name = "testClient", url = "localhost:8080", configuration = TestDecoderConfig.class)
public interface TestClient {
    @PostMapping(value = "/test")
    Object test();
}

클라이언트에 위와같이 DecoderConfig를 설정해준다.

 

아래는 DecoderConfig 코드.

public class TestDecoderConfig {
    @Bean
    public ErrorDecoder worldAuthErrorDecoder() {
        return this::decode;
    }
    private Exception decode(String methodKey, Response response) {
        String responseBody = extractResponseBody(response);
        if(HttpStatus.BAD_REQUEST.value() == response.status()) {
            return new BadRequestException("실패 사유를 아는 실패 : " + responseBody);
        } else {
            return new Exception("실패 사유를 모르는 실패");
        }
    }

    private String extractResponseBody(Response response) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().asInputStream(), StandardCharsets.UTF_8))) {
            return reader.lines().collect(Collectors.joining("\n"));
        } catch (IOException e) {
            throw new RuntimeException("변환 실패");
        }
    }
}

 

이렇게 설정해주면, 서비스 로직에는 FeignException을 핸들링하는 코드를 남기지 않고도 에러를 핸들링할 수 있게 된다.

 

현재 내가 생각하는 가장 Best Practice이다.

 

주의할 점

1. 익셉션 처리 순서는 ErrorDecoder -> try/catch -> ControllerAdvice이다. 

 

ErrorDecoder를 사용하면 try/catch, ControllerAdvice는 사용되지 않는다.

 

2. ErrorDecoder에 @Configuration을 지정하면 이 ErrorDecoder는 글로벌로 사용되게 된다.

스택오버플로우를 통해 겨우 문제를 찾을 수 있었다.

https://stackoverflow.com/questions/56352215/how-implement-error-decoder-for-multiple-feign-clients

 

마치며

해당 포스팅을 쓰다보면서 사람들이 왜 본인의 입맛에 맞게끔 라이브러리를 만들게 되는가에 대해 알게 됐다.

 

클라이언트를 구성하고 요청을 보내는 자체는 다른 요청 라이브러리보다는 깔끔하지만

 

400, 500 응답을 바로 익셉션으로 보내버리는 방식이 맞는지 잘 모르겠다.

 

400, 500 응답이 왔다는 건 외부 서버로의 응답이 문제가 있지만, 우리 서버에서는 프로세스가 정상 수행 중이기 때문이다.

 

이렇게 하나씩 본인의 입맛을 맞추지 못하는 뭔가를 발견할 때, 최종적으로는 오픈소스에도 기여하게되지 않을까싶다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
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
글 보관함