Spring 의 RestTemplate

Spring 의 RestTemplate

Spring 의 REST Client 인 RestTemplate 에 대해 알아보자

RestTemplate 을 사용하여 HTTP request 가 가능하며,

GET, POST, PUT, DELETE, HEAD 등의 method 를 사용할 수 있다.

본 글에서는 이 중에서도 GET, POST, HEAD 메소드의 사용 예를 알아본다.

또, RestTemplate 에서 Exception Handling 을 하는 방식에 대해서도 알아본다.

기본적인 사용법

GET 메소드

JSON 받아오기

getForEntity 메소드를 사용하여 JSON을 받아올 수 있다.

RestTemplate restTemplate = new RestTemplate();
String fooResourceUrl
  = "http://localhost:8080/spring-rest/foos";
ResponseEntity<String> response
  = restTemplate.getForEntity(fooResourceUrl + "/1", String.class);
assertThat(response.getStatusCode(), equalTo(HttpStatus.OK));

ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(response.getBody());
JsonNode name = root.path("name");
assertThat(name.asText(), notNullValue());

JsonNode 클래스는 Jackson 이 제공하는 JSON 노드 구조 클래스다.

JSON 대신 POJO 받아오기

Response 를 곧바로 DTO 에 매핑할 수 있다.

public class Foo implements Serializable {
private long id;

private String name;
// standard getters and setters
}

이런 POJO가 있을 때 getForObject 메소드를 쓸 수 있다.

Foo foo = restTemplate
.getForObject(fooResourceUrl + "/1", Foo.class);
assertThat(foo.getName(), notNullValue());
assertThat(foo.getId(), is(1L));

HEAD 메소드

headForHeaders 메소드로 간단하게 헤더만 가져올 수 있다.

HttpHeaders httpHeaders = restTemplate.headForHeaders(fooResourceUrl);
assertTrue(httpHeaders.getContentType().includes(MediaType.APPLICATION_JSON));

POST 메소드

postForLocation(), postForObject() 또는 postForEntity() 를 사용할 수 있다.

postForLocation() 는 생성된 리소스의 URI 를 반환하는 반면,
postForObject()postForEntity() 는 리소스 자체를 반환한다.

postForObject()

다음과 같이 사용한다.

RestTemplate restTemplate = new RestTemplate();

HttpEntity<Foo> request = new HttpEntity<>(new Foo("bar"));
Foo foo = restTemplate.postForObject(fooResourceUrl, request, Foo.class);
assertThat(foo, notNullValue());
assertThat(foo.getName(), is("bar"));

postForLocation()

다음과 같이 사용한다.

HttpEntity<Foo> request = new HttpEntity<>(new Foo("bar"));
URI location = restTemplate
.postForLocation(fooResourceUrl, request);
assertThat(location, notNullValue());

exchange()

좀 더 범용적인 exchange() 메소드를 사용할 수도 있다.

RestTemplate restTemplate = new RestTemplate();
HttpEntity<Foo> request = new HttpEntity<>(new Foo("bar"));
ResponseEntity<Foo> response = restTemplate
.exchange(fooResourceUrl, HttpMethod.POST, request, Foo.class);

assertThat(response.getStatusCode(), is(HttpStatus.CREATED));

Foo foo = response.getBody();

assertThat(foo, notNullValue());
assertThat(foo.getName(), is("bar"));

폼 데이터 전송하기

서버가 ‘&’로 연결된 여러개의 ‘키=밸류’ 쌍을 받을 수 있도록 ‘Content-Type’ 을 application/x-www-form-urlencoded 로 변경해줘야한다.

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

그리고 LinkedMultiValueMap 으로 폼 데이터를 감싼다.

MultiValueMap<String, String> map= new LinkedMultiValueMap<>();
map.add("id", "1");

다음은, HttpEntity 로 request를 빌드한다.

HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);

마지막으로 postForEntity() 를 호출한다.

ResponseEntity<String> response = restTemplate.postForEntity(
fooResourceUrl+"/form", request , String.class);

assertThat(response.getStatusCode(), is(HttpStatus.CREATED));

Exception Handling

RestTemplate 은 내부적으로 errorHandler 라는 필드를 가지고 있다.

errorHandler 라는 녀석이 바로 HTTP 에러가 발생했을 때, 이를 처리하는 로직이

들어있는 객체이며, DefaultResponseErrorHandler 라는 클래스의 인스턴스로 초기화 되어있다.

DefaultResponseErrorHandler 는 기본적으로 HTTP 에러가 발생했을 때

다음 3가지 중 하나의 Exception 을 던진다.

  1. HttpClientErrorException - 4xx HTTP 상태 코드가 응답 됐을 때
  2. HttpServerErrorException - 5xx HTTP 상태 코드강 응답 됐을 때
  3. UnknownHttpStatusCodeException - 알 수 없는 HTTP 상태 코드가 응답 됐을 때

위의 예외들은 모두 RestClientResponseException의 서브 클래스다.

물론 가장 단순한 방법은 try/catch 문으로 모든 HTTP 메소드 호출부를 감싸는 것이다.

하지만 이 방법은 API 의 종류와 호출부가 많아지면 코드의 품질이 좋지 않아질 것이다.

다른 좋은 방법은 없을까? 다행히도 스프링이 좋은 방법을 제공한다.

ResponseErrorHandler 인터페이스

ResponseErrorHandler 를 구현하는 클래스를 작성하여

RestTemplatesetErrorHandler() 로 앞서 작성한 에러 핸들러의 인스턴스를 세팅해줄 수 있다.

일반적으로 에러 핸들러에서는 다음 둘 중 한가지 동작을 수행하게 된다.

  1. HTTP 응답에 따라서 우리의 앱에 맞는, 의미있는 Exception 을 던져준다.
  2. Exception 을 던지지 않고 HTTP 응답을 무시한 채 프로그램이 계속 실행 되도록 한다.

참고로, ResponseErrorHandler 인터페이스를 구현하는 대신

이미 ResponseErrorHandler 를 구현하고 있는 DefaultResponseErrorHandler 클래스를 상속받아도 된다.

이러면 이미 DefaultResponseErrorHandler 에서 hasError() 메소드가 4xx/5xx 상태 코드를 받는 경우

true 를 반환하도록 구현되어 있기 때문에 직접 구현해줄 필요가 없어서 편하다.

다음의 예제 코드를 보자.

public class MyErrorHandler implements ResponseErrorHandler {

@Override
public boolean hasError(ClientHttpResponse httpResponse)
throws IOException {
return (
httpResponse.getStatusCode().series() == CLIENT_ERROR
|| httpResponse.getStatusCode().series() == SERVER_ERROR);
}

@Override
public void handleError(ClientHttpResponse httpResponse)
throws IOException {
if (httpResponse.getStatusCode()
.series() == HttpStatus.Series.SERVER_ERROR) {
// handle SEVER_ERROR
throw new My500ErrorException();
} else if (httpResponse.getStatusCode()
.series() == HttpStatus.Series.CLIENT_ERROR) {
// handle CLIENT_ERROR
if (httpResponse.getStatusCode() == HttpStatus.NOT_FOUND) {
throw new NotFoundException();
} else {
throw new My400ErrorException();
}
}
}
}

이렇게 작성한 에러 핸들러를 다음과 같이 RestTemplate 에 셋팅해 줄 수 있다.

@Service
public class BarConsumerService {
private RestTemplate restTemplate;

@Autowired
public BarConsumerService(RestTemplateBuilder restTemplateBuilder) {
RestTemplate restTemplate = restTemplateBuilder
.errorHandler(new MyErrorHandler())
.build();
}

public Bar fetchBarById(String barId) {
return restTemplate.getForObject("/bars/4242", Bar.class);
}
}

TestRestTemplate

TestRestTemplate 은 RestTemplate 의 유용한 대체제로서 Integration Test 시 유용하다.

내장 서버와 함께 @SpringBootTest 어노테이션을 사용중이라면, Test 클래스 내에서 @Autowired 되어 곧바로 사용할 수 있다.

커스터마이징이 필요하다면 RestTemplateBuilder @Bean 을 사용하면 된다.

RestTemplate과 다른점은, TestRestTemplate 은 RestTemplate 을 내장(composition)하며

다음과 같은 추가 기능을 제공한다는 것이다.

Authentication

  • 템플릿 생성 시 입력

생성자 인자로 credentials 를 줄 수 있다.

TestRestTemplate testRestTemplate
= new TestRestTemplate("user", "passwd");
ResponseEntity<String> response = testRestTemplate.
getForEntity(URL_SECURED_BY_AUTHENTICATION, String.class);

assertThat(response.getStatusCode(), equalTo(HttpStatus.OK));
  • 템플릿 생성 후 입력

이미 템플릿을 생성한 후에도 가능하다.

TestRestTemplate testRestTemplate = new TestRestTemplate();
ResponseEntity<String> response = testRestTemplate.withBasicAuth(
"user", "passwd").getForEntity(URL_SECURED_BY_AUTHENTICATION,
String.class);

assertThat(response.getStatusCode(), equalTo(HttpStatus.OK));

HttpClientOption

TestRestTemplate 내 ENUM 형태로 존재하는 HttpClientOption 을 통해 내부의 HTTP Client 를 커스터마이징할 수 있다.

TestRestTemplate testRestTemplate = new TestRestTemplate("user", 
"passwd", TestRestTemplate.HttpClientOption.ENABLE_COOKIES);
ResponseEntity<String> response = testRestTemplate.
getForEntity(URL_SECURED_BY_AUTHENTICATION, String.class);

assertThat(response.getStatusCode(), equalTo(HttpStatus.OK))

만약 인증이 필요하지 않다면 단순히 이렇게 할 수도 있다.

TestRestTemplate(TestRestTemplate.HttpClientOption.ENABLE_COOKIES)

RestTemplate 의 Wrapper

몇가지 생성자와 메소드를 통한 추가적인 기능 뿐만 아니라, TestRestTemplate 은 RestTemplate 의 Wrapper 의 역할도 할 수 있다.
이는 레거시 코드로 인해 꼭 RestTemplate 을 써야만 하는 경우에 유용하다.

WebClient vs RestTemplate

WebClient 는 Spring5 에서 새롭게 등장한 클래스다.
Spring3 부터 지원되는 RestTemplate 가 블로킹 방식으로만 동작하는 반면,
WebClient 는 블로킹 뿐만 아니라 논블로킹으로도 동작하여 RestTemplate 의 대체제로 사용되고있다.

RestTemplate

RestTemplate 은 블로킹 방식이며, 내부적으로 Java Servlet API 를 사용하여 하나의 request 당 하나의 스레드를 점유하기 때문에

요청에 대한 응답이 늦어지는 경우 불필요하게 CPU 와 메모리 자원이 낭비될 수가 있다.

또한, 잦은 컨텍스트 스위칭으로 성능에도 좋지않은 영향을 미칠 수도 있고

스레드 풀의 스레드가 고갈될 수도 있다.

WebClient

반면, WebClient 는 논블로킹 방식이기 때문에, 응답이 늦어지더라도 큰 문제가 없다.
WebClient 는 Spring WebFlux 라이브러리에 포함 되어있다.
내부적으로, WebClient 가 만든 task 를 Reactive framework 가 큐에 넣고, 응답이 왔을 때만 꺼내서 실행한다.
Reactive framework 는 Java 9의 Reactive Streams API를 통해 비동기 로직을 조립할 수 있게 해준다.

참고1: https://www.baeldung.com/rest-template
참고2: https://www.baeldung.com/spring-webclient-resttemplate
참고3: https://www.baeldung.com/spring-rest-template-error-handling
참고4: https://www.baeldung.com/spring-boot-testresttemplate

Comments