OpenFeign Error Handling

na.ram·2024년 11월 29일

all in auction

목록 보기
13/14
post-thumbnail

MSA로 전환하며 만약 서버 간 통신에 실패한다면 어떻게 처리를 하는게 좋을지가 고민이었습니다.

서버 간 통신 실패의 이유는 다양합니다.
개발자의 실수 때문일 수도, 네트워크 통신 문제일 수도, 올바르지 않은 요청 문제일 수도 있습니다.

어떤 이유로 실패하든 공통 응답으로 일관성 있게 반환해주고 싶기 때문에 예외를 잡아 공통 응답으로 변환하려고 합니다.


ErrorDecoder

다른 서버와 정상적으로 통신에 성공했지만, 비지니스 로직에 맞지 않아 4XX 오류가 돌아왔을 때나 5XX 오류가 돌아왔을 때는 ErrorDecoder를 통해서 처리합니다.

저는 5XX 오류이면서 GET 메서드 요청이었던 경우에만 최대 2번 재시도를 하도록 설정해두었습니다.

그 외에는 재시도 없이 오류를 공통 응답으로 변환해 돌려주도록 설정해두었습니다.


@Slf4j
public class ApiErrorDecoder implements ErrorDecoder {

    @Override
    public Exception decode(String methodKey, Response response) {

        String responseBody = FeignClientUtil.getResponseBody(response);

        if (isRetry(response)) {
            return new RetryableException(
                    response.status(),
                    format("%s", responseBody),
                    response.request().httpMethod(),
                    1000L,
                    response.request()
            );
        }
        
        return new ApiException(getErrorStatus(responseBody));
    }

    /**
     * 5XX 에러이면서, GET 요청에 대해서만 retry
     */
    private boolean isRetry(Response response) {
        if (response.request().httpMethod() != Request.HttpMethod.GET) {
            return false;
        }

        return HttpStatusCode.valueOf(response.status()).is5xxServerError();
    }
}

FeignException

네트워크 오류나 타임아웃 등에 의해 오류가 돌아왔을 때에는 GlobalExceptionHandler에서 FeignException을 잡아 처리합니다.

위와 같이 오류를 공통 응답으로 변환해 돌려주도록 구현했습니다.

@ExceptionHandler(FeignException.class)
public ResponseEntity<ApiResponse<String>> handleFeignServerException(FeignException ex) {
    HttpStatus status;
    try {
        status = HttpStatus.valueOf(ex.status());
    } catch (IllegalArgumentException e) {
        status = HttpStatus.INTERNAL_SERVER_ERROR;
    }

    return ResponseEntity
        .status(HttpStatus.valueOf(status.value()))
        .body(new ApiResponse<>(
                    false,
                    String.valueOf(status.value()),
                    getErrorStatus(ex.getMessage()).getMessage(),
                    null
            )
        );
}

오류 테스트

설정한대로 오류 발생 시, 공통 응답으로 잘 변환해주는지를 확인하기 위해 테스트 코드로 확인해보았습니다.

5XX 오류이면서 GET 메서드 요청이었던 경우에 RetryableException을 throw 하는지,
5XX 오류이면서 GET 메서드 요청이 아니거나 4XX 오류인 경우에 ApiException을 throw 해주는지를 확인했습니다.

    @Test
    @DisplayName("쿠폰 조회 페잉 테스트 - 4XX 실패")
    public void getCouponFeignTest_failure_4XX() {
        // given
        long userId = 1L;
        long couponUserId = 2L;

        // 생략

        // when, then
        assertThrows(ApiException.class,
                () -> couponService.getValidCoupon(userId, couponUserId));
    }

    @Test
    @DisplayName("쿠폰 조회 페잉 테스트 - 5XX 실패")
    public void getCouponFeignTest_failure_5XX() {
        // given
        long userId = 1L;
        long couponUserId = 2L;
        
        // 생략

        // when, then
        assertThrows(RetryableException.class,
                () -> couponService.getValidCoupon(userId, couponUserId));
    }

테스트 코드 실행한 결과, 예상했던 예외를 정상적으로 throw 해줌을 확인할 수 있었습니다.

0개의 댓글