[Spring] FeignClient 에러 리스폰스 버블링

나르·2022년 2월 18일
0

Spring

목록 보기
15/25

❗️ ISSUE

Spring Feign Client를 사용하면 일반적으로 다음과 같은 코드를 작성합니다.

@FeignClient(name = "user-service", path = "/api/user")
public interface UserFeignClient {

    @PostMapping(value = "/login")
    ResponseEntity<LoginResponseDto> login(@RequestBody LoginRequestDto loginRequest);

    @PutMapping(value = "/access")
    ResponseEntity<Void> updateUserAccessAt(@RequestBody UpdateAccessAtRequestDto updateAccessAtRequest);

}

요청이 성공적으로 처리된 경우에는 크게 문제가 되지 않지만, Feign Server에서 처리가 실패된 경우에는 문제가 발생하게 됩니다.

우리 서비스에서는 에러 발생 시 exception advice를 통해 에러 타입에 맞는 failure response entity를 반환하는 구조입니다.
때문에 Feign Server에서 exception이 발생하면 Feign Client에서 해당 exception을 응답해주지 못하는 이슈가 발생했습니다.

Feign Client의 경우 리스폰스 타입을 특정 DTO로 지정해두었고, failure response는 data가 null이기 때문에 아래와 같은 에러가 발생하는 것입니다.
또한 상위 메소드에서 getter을 쓰기 때문에 와일드카드를 쓸 수도 없는 상황이었습니다.

✅ SOLUTION

이를 해결하기 위해 FeignErrorDecoder를 정의하여 Feign Server에서 실패 응답이 전달되면 Feign Client에서 실패 데이터를 파싱하여 다시 exception을 발생시켜 실패 응답을 내려주도록 처리했습니다.

  1. FeignClientConfig를 생성해 FeignErrorDecoder을 bean 등록하여 사용
// FeignClientConfig.java
public class FeignClientConfig {

    @Bean
    public ErrorDecoder errorDecoder() {
        return new FeignErrorDecoder();
    }

}

// FeignErrorDecoder.java
public class FeignErrorDecoder implements ErrorDecoder {

    private static final String LOGIN_PATH = "/api/user/login";

    @SneakyThrows
    @Override
    public Exception decode(String methodKey, Response response) {
        Reader reader = response.body().asReader(StandardCharsets.UTF_8);
        String errorResult = IOUtils.toString(reader);
        String path = response.request().url();
        ObjectMapper objectMapper = new ObjectMapper();

        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
        ResponseEntity errorResponse = objectMapper.readValue(errorResult, ResponseEntity.class);
        int status = response.status();
       

        if (StringUtils.endsWithIgnoreCase(path, LOGIN_PATH) && isLoginError(errorResponse.getResponseCode())) {
            status = HttpStatus.OK.value();
        }
        return new FeignClientException(status, errorResponse.getResponseCode(), errorResponse.getMessage());
    }

	// User Server에서 유효한 failure가 발생한 경우 (존재하지 않는 계정, 비밀번호 오류 등)
    private boolean isLoginError(String responseCode) {
        String code = responseCode.substring(2);
        String emailErrorCode = ResponseType.USER_NOT_EXIST_EMAIL.getCode();
        String accountNameErrorCode = ResponseType.USER_NOT_EXIST_ACCOUNT_NAME.getCode();
        String passwordErrorCode = ResponseType.USER_NOT_MATCH_PASSWORD.getCode();

        return emailErrorCode.equals(code) || accountNameErrorCode.equals(code) || passwordErrorCode.equals(code);
    }

}
  1. FeignClient 통신에서 발생하는 에러를 처리하기 위한 exception 클래서 정의 및 핸들링
// ExceptionControllerAdvice.java
...
    @ExceptionHandler(FeignClientException.class)
    public org.springframework.http.ResponseEntity
            <ResponseEntity<Void>> handleFeignClientException(FeignClientException exception) {
        printLog(exception.getClass().getName(), exception.getMessage());
        return org.springframework.http.ResponseEntity
                .status(exception.getStatus())
                .body(ResponseEntity.failureResponse(exception.getResponseCode(), exception.getMessage()));
    }
...
  1. FeignClient에 configuration 추가
@FeignClient(name = "user-service", path = "/api/user", configuration = FeignClientConfig.class)
public interface UserFeignClient {

   ...
}
profile
💻 + ☕ = </>

0개의 댓글