Spring OpenFeign - 에러 코드 분기 처리 (ErrorDecoder)

한규주·2021년 8월 22일
5
post-thumbnail

지난 포스트에 이어서 이번에는 일반적인 응답이 아닌 Error 응답을 decoding하는 ErrorDecoder를 구현해보려고 한다.

그에 먼저 앞서서 알아둬야할 것은 Feign에서 응답 에러를 핸들링하는 방법에 대해서 알아둬야할 필요가 있다.

ErrorDecoder

spring cloud openfeign은 ErrorDecoder를 통해 응답값이 200이 아닐 경우 에러를 던지고 있다.

feign의 default ErrorDecoder는 ResponseEntityErrorDecoder 이고, 해당 ErrorDecoder에서는 FeignException을 발생시키고 있다.

FeignClientException과 FeignServerException

  • status code 400번대 → FeignClientException
  • status code 500번대 → FeignServerException

FeignClientException

FeignException을 상속하고 있는 FeignClientException은 각 status code에 따라 다른 하위 클래스들을 가지고 있다.

400이면 FeignException.BadRequest(), 401이면 FeignException.Unauthorized() 인 식인데 아래 코드에서 어떤 exception들이 있는지 확인할 수 있다.

// feign.FeignException#clientErrorStatus

switch (status) {
  case 400:
    return new BadRequest(message, request, body);
  case 401:
    return new Unauthorized(message, request, body);
  case 403:
    return new Forbidden(message, request, body);
  case 404:
    return new NotFound(message, request, body);
  case 405:
    return new MethodNotAllowed(message, request, body);
  case 406:
    return new NotAcceptable(message, request, body);
  case 409:
    return new Conflict(message, request, body);
  case 410:
    return new Gone(message, request, body);
  case 415:
    return new UnsupportedMediaType(message, request, body);
  case 429:
    return new TooManyRequests(message, request, body);
  case 422:
    return new UnprocessableEntity(message, request, body);
  default:
    return new FeignClientException(status, message, request, body);
}

FeignServerException

500번대 응답을 처리하는 FeignServerException의 경우엔 그 종류가 많지 않다.

// feign.FeignException#serverErrorStatus
switch (status) {
  case 500:
    return new InternalServerError(message, request, body);
  case 501:
    return new NotImplemented(message, request, body);
  case 502:
    return new BadGateway(message, request, body);
  case 503:
    return new ServiceUnavailable(message, request, body);
  case 504:
    return new GatewayTimeout(message, request, body);
  default:
    return new FeignServerException(status, message, request, body);
}

에러 코드 처리

간혹 외부 서비스가 특정 에러코드를 담아 응답을 내려줄때는 동작을 달리해야 할 수도 있다. 이런 경우에는 에러 응답의 body를 파싱해야 하는데, FallbackFactory에서 매번 그 동작을 수행하기는 번거로운 일이다.

이럴 때는 ErrorDecoder에서 기본적인 에러 응답 파싱을 처리할 수 있다.

에러 핸들링을 깔끔하게 할 수 없을까?

만약 아래와 같은 응답이 오는 경우에 에러를 뱉지 않고 빈 list를 담아서 응답해야 한다고 생각해보자.

지난 시리즈의 BeerClient를 바탕으로 한다.

status code 400
{
    "result": "FAILED",
    "data": null,
    "error": {
        "code": "FOO_ERROR",
        "message": "error message",
        "data": null
    }
}

이 경우에 원래대로라면 FeignException.BadRequest가 던져질테지만, FeignException에는 response body가 bytearray로 들어있기 때문에 매번 응답을 파싱하기는 번거로운 일이다.

아래 방법을 통해 개선해보자.

  1. 응답이 특정 포맷으로 오는 경우
  2. 에러 코드가 존재하는 경우

위 두가지를 모두 충족하면 BeerClientHandledException을 던진다.

테스트

테스트를 통해 위 요구사항을 표현하자면 다음과 같다.

@Test
fun handledError() {
    val responseBody = """
            {
                "result": "FAILED",
                "data": null,
                "error": {
                    "code": "FOO_ERROR",
                    "message": "error message",
                    "data": null
                }
            }
        """.trimIndent()

    mockServer.stubFor(
        get(urlPathEqualTo("/beers"))
            .withQueryParam("size", equalTo("2"))
            .withHeader("Authorization", equalTo("AUTH-KEY"))
            .withHeader("Content-Type", equalTo("application/json"))
            .willReturn(
                aResponse()
                    .withStatus(HttpStatus.BAD_REQUEST.value())
                    .withBody(responseBody)
                    .withHeader("Content-Type", "application/json")
            )
    )

		val then = shouldThrow<BeerClientHandledException> { sut.getBeer(2) }

    then.errorCode shouldBe "FOO_ERROR"
    then.message shouldBe "error message"
}

실행해보면 결과는 당연히 실패다

ErrorDecoder 구현

아래 BeerClientErrorDecoder를 통해 ErrorDecoder를 구현해보자.

class BeerClientErrorDecoder: ErrorDecoder {
  override fun decode(methodKey: String, response: Response): Exception {
      val errorData = parse(response)
      errorData?.code?.let {
          return BeerClientHandledException(it, errorData.message)
      }
      return ErrorDecoder.Default().decode(methodKey, response)
  }

  private fun parse(response: Response): CommonResponse.ErrorData? {
      return runCatching {
           objectMapper.readValue(response.body().asInputStream(), CommonResponse::class.java)?.error
      }.getOrNull()
  }

  private val objectMapper = jacksonObjectMapper()
      .findAndRegisterModules()
}

data class CommonResponse<T>(
    val result: String?,
    val data: T?,
    val error: ErrorData?
) {
    data class ErrorData(
        val code: String?,
        val message: String?,
        val data: Map<String, Any?>?
    )
}

그리고 Client가 해당 ErrorDecoder를 받을 수 있도록 설정한다.

class BeerClientConfiguration {
    @Bean
    fun errorDecoder(): ErrorDecoder {
        return BeerClientErrorDecoder()
    }
}

다시 테스트를 실행해보면 테스트가 통과한 것을 알 수 있다.

소스

위 예시는 아래 소스에서 자세히 확인할 수 있다.

https://github.com/hanqyu/feign-decoder-example

profile
토스페이먼츠의 서버개발자로 일하고 있습니다.

0개의 댓글