DolphaGo / TIL

TIL & issues
0 stars 1 forks source link

[Feign] ControllerAdvice로는 Feign의 ErrorDecoder에서 발생한 Exception을 잡지 못한다? #41

Open DolphaGo opened 3 years ago

DolphaGo commented 3 years ago

우선 이와 같은 질문을 구글링해보면 아래와 같은 방법이 StackOverFlow에 소개되고 있긴 하다. Feign ErrorDecoder에서 발생한 Exception은 ControllerAdvice에서 캐치를 하지 못하니 다음과 같이 try ~ catch로 feign 바깥에서 처리하라는 의견.

try{
   feignClient.method();
} catch(Exception ex){
  //throw exceptions you want
  throw new YourException();
}
@ControllerAdvice
public class GlobalControllerAdvice {

    private final Logger log = LoggerFactory.getLogger(getClass());

    @ExceptionHandler(YourException.class)
    public ResponseEntity<RestApiException> handleException(RestApiException exception, HttpServletRequest req) {
        //impl
    }
}

그런데 나는 위와 같이 했음에도, 나의 ControllerAdvice가 동작하지 않았다. 분명히 등록되어 있는 CustomException인데도 말이다 -_-; 좀 더 파보면 원인이 나오겠지만, 나는 이걸 트라이캐치로 분기하고 있으려니 너무 너무 코드가 맘에들지 않았다. 그래서 다른 방법을 생각해봤다.

ControllerAdvice로는 Feign의 ErrorDecoder에서 발생한 Exception을 잡지 못한다?

No! 잡을 수 있다. 대신 좀 다르게 해야한다.

@Slf4j
@RequiredArgsConstructor
public class FeignClientExceptionErrorDecoder implements ErrorDecoder {
    private final ObjectMapper objectMapper;
    private final StringDecoder stringDecoder;

    @Override
    public FeignClientException decode(String methodKey, Response response) {
        String message = null;
        Error errorForm = null;
        if (response.body() != null) {
            try {
                message = stringDecoder.decode(response, String.class).toString();
                errorForm = objectMapper.readValue(message, ErrorResponseDto.class).getError();
            } catch (IOException e) {
                log.error("[{}] Error Deserializing response body from failed feign request response. = {}", methodKey, e);
            }
        }
        return new FeignClientException(response.status(), errorForm.getMessage(), response.request(), message.getBytes(StandardCharsets.UTF_8));
    }
}

이 Decoder를 Bean으로 등록해주었다.

@Configuration
@RequiredArgsConstructor
public class FeignClientErrorDecoderConfiguration {

    private final ObjectMapper objectMapper;

    @Bean
    public FeignClientExceptionErrorDecoder errorDecoder() {
        return new FeignClientExceptionErrorDecoder(objectMapper, new StringDecoder());
    }
}

그러면 A server -> B server 로 요청을 보내는 FeignClient가 어떤 문제가 발생하여 B server로부터 [400] ~~~ is invalid. 와 같은 메세지를 받게 되었다면, A서버는 FeignClientException이 발생하게 될 것이고, 이를 ErrorDecoder에서 B로 부터 받은 메세지를 ErrorResponseDto 와 같은 임의의 Dto로 Mapping하여, 원하는 메세지만 FeignClientException에 담아서 리턴한다.

그렇다면? 이를 처리하는 ControllerAdvice를 등록해주면 된다! 나는 다음과 같이 ControllerAdvice를 설정했다.

@Slf4j
@RestControllerAdvice(basePackages = "{{ your package }}")
public class MarketingControllerAdvice {

    @ExceptionHandler(FeignException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponseDto feignExceptionHandler(FeignException ex) {
        return ErrorResponseDto.error(ErrorCode.INTERNAL_SERVER_ERROR.getCode(), ex.getMessage());
    }

    @ExceptionHandler(FeignClientException.class)
    public ErrorResponseDto FeignClientExceptionHandler(FeignClientException ex) {
        final ErrorCode resolve = ErrorCode.resolve(ex.status());
        return ErrorResponseDto.error(resolve.getCode(), ex.getMessage());
    }
}

아 물론, ErrorCode는 Enum으로 임의로 설정했다. 즉, 정리하자면, B server로부터 status와 message를 가지고, A server에서 다시 원하는 형태로 리턴하기 위해서 ControllerAdvice를 적용한 것이고, 이 때 ErrorDecoder를 활용하여, 원하는 메시지를 실제 A server에 Request를 보낸 Client에게 Response로 B server의 message도 안전하게 출력하고, ErrorCode 및, Status도 재 설정할 수 있게 만들었다.

@AllArgsConstructor
@Getter
public enum ErrorCode {
    BAD_REQUEST(400, "E001"),
    UNAUTHORIZED(401, "E002"),
    NOT_FOUND(404, "E003"),
    INTERNAL_SERVER_ERROR(500, "E004");

    private Integer status;
    private String code;

    public static ErrorCode resolve(Integer status) {
        return EnumSet.allOf(ErrorCode.class)
                      .stream()
                      .filter(errorCode -> errorCode.status == status)
                      .findFirst()
                      .orElse(INTERNAL_SERVER_ERROR);
    }
}
goodGid commented 3 years ago

잘 보고 갑니다 !