[Spring] RestTemplate Error Handling 하기

들어가며

Spring에서 다른 서버에 api를 호출하기 위해 자주 사용되는 RestTemplate에 error가 발생했을 때 원하는 동작으로 error handling을 하는 방법에 대해서 알아본다.

Setting

간단하게 테스트를 할 예정이라 해당 dependency만 추가하고 진행 한다.

  • lombok
  • spring boot web

MockServer를 이용해도 되지만 local에서 간단하게 서버를 띄워두고 테스트를 진행할 예정이라 간단한 controller를 추가한다.

ExampleController

@RestController
class ExampleController {

    @GetMapping("/example")
    fun example(): String {
        return "success"
    }

    @PostMapping("/badRequest")
    fun badRequest(): ResponseEntity<ExampleResponse> {
        val response = ExampleResponse("message", "1", UUID.randomUUID())
        return ResponseEntity(response, HttpStatus.BAD_REQUEST)
    }

    class ExampleResponse(var message: String?, var version: String?, var uuid: UUID?)
}
  • http://localhost:8080/example로 요청이 들어오는 경우 success로 response를 내려준다.
  • http://localhost:8080/badRequest로 요청이 들어오는 경우 StatusCode를 400에 원하는 response를 내려 준다.

ExampleRestTemplate

  • RestTemplate 정상 동작하고 있는지 확인 하기 위해 /example을 호출하는 테스트를 작성 한다.
@SpringBootTest
internal class ExampleRestTemplateTest(@Autowired private val exampleRestTemplate: ExampleRestTemplate) {
    private val log = LoggerFactory.getLogger(javaClass)
    
    @Test
    internal fun exampleTest() {
        val result = exampleRestTemplate.successExample();

        assertThat(result).isEqualTo("success")
    }
}
  • 해당 테스트를 성공 시키기 위해 successExample 메소드를 구현한다.
@Component
class ExampleRestTemplate {
    private val restTemplate: RestTemplate = RestTemplate()
    private val log = LoggerFactory.getLogger(javaClass)

    fun successExample(): String {
        val result = restTemplate.getForEntity<String>("http://localhost:8080/example")
        print(result)

        return result.body.toString()
    }
}
  • 성공 케이스가 아닌 실패 케이스가 발생했을 때 어떻게 동작하는지 확인
@Test
internal fun badRequestTest() {
    assertThrows<BadRequest> {
        exampleRestTemplate.failExample()
    }
}
  • 해당 테스트를 성공 시키기 위해서 /badRequest를 호출하는 failExample을 구현
    @Throws(BadRequest::class)
    fun failExample(): String? {
        var result: ResponseEntity<String>? = null
        try {
            result = restTemplate.getForEntity<String>("http://localhost:8080/badRequest")
        } catch (e: HttpClientErrorException) {
            log.error("exception {}", e.message, e)
            throw BadRequest(e)
        }
        print(result)

        return result.body?.toString()
    }
  • 기본적인 RestTemplate의 경우 2xx의 케이스가 아닌 경우 에러로 판단하고 다음 라인이 실행되지 않음
  • 따라서 HttpClientErrorExceptione.getStatusCode()를 통해서 error를 판단해야 한다.
  • 매번 try catch를 통해서 할 순 없으니 RestTemplateerrorHandle을 추가하여 관리한다.

RestTemplateBuilder

class RestTemplateResponseErrorHandler : DefaultResponseErrorHandler() {
    @Throws(IOException::class)
    override fun hasError(httpResponse: ClientHttpResponse): Boolean {
        return super.hasError(httpResponse)
    }

    @Throws(IOException::class)
    override fun handleError(httpResponse: ClientHttpResponse) {
        if (httpResponse.statusCode.series() === HttpStatus.Series.SERVER_ERROR) {
            throw RuntimeException()
        } else if (httpResponse.statusCode.series() === HttpStatus.Series.CLIENT_ERROR) {
            // handle CLIENT_ERROR
        }
    }
}
private var errorHandlerRestTemplate: RestTemplate? = null

constructor(requestBuilder: RestTemplateBuilder) {
    errorHandlerRestTemplate = requestBuilder.errorHandler(RestTemplateResponseErrorHandler()).build()
}
  • RestTemplateBuilder를 이용해 RestTemplateerrorHandler를 추가 해준다.
  • errorHandlerResponseErrorHandler를 상속받아서 구현해야 하며 DefaultResponseErrorHandler가 이미 존재하고 있다.
  • client error(4xx)와 server error(5xx)의 경우 error로 판단하고 handleError에서 처리해준다.
  • server error의 경우는 딱히 처리해줄 것이 없어 RuntimeException으로 하고 Client Error에 대해서 throw를 하지 않으면 문제 없이 다음 행으로 진행 가능하다.
@Test
internal fun errorHandlerTest() {
    val actual = exampleRestTemplate.failExampleByErrorHandler()
    assertThat(actual).isNotNull()
}
  • error가 발생하기에 다음 행 진행이 안되었는데 error handler를 추가하여 4xx일때 무시하도록 셋팅
  • status code가 4xx 지만 try catch를 하지 않고도 다음 행으로 진행할 수 있다.

마치며

RestTemplate에 대해서 매번 간단하게만 사용하고 있어서 커스텀할 수 있는 부분에 대해서 모르고 있었는데 간단하게 나마 공부해보고 정리해보았다.

나중에는 RestTemplate이 아니라 넷플릭스에서 오픈소스로 공개한 feign 라이브러리를 이용해서 개발을 하게 될거라 생각한다.

따라서 다음엔 feign에 대해서 공부해보려고 한다~!


RestTemplateExample 관련 example code는 github에 올려두었으니 참고~!

Leave a comment