API를 개발하다보면 외부 API 서비스 혹은 다른 서비스 API를 호출해서 데이터를 송,수신 해야하는 경우가 있다.
RestTemplate는 Spring 3부터 지원하기 시작한 “동기 방식"의 REST API 호출 방법이다.
Http 요청을 작성할 수 있는 템플릿으로서 json, xml 데이터를 Jackson과 함께 간단하게 파싱해서 사용하는 것이 가능하다.
Spring 4부터는 비동기 방식인 AsyncRestTemplate를 지원하기 시작했고, 최근 Spring 5부터는 기존의 동기 방식을 포함할 뿐만 아니라 비동기, 넌블로킹 방식도 WebClient를 지원하기 시작했다.
메서드
RestTemplate의 메서드는 아래와 같다.
1. getForObject : GET 메서드를 이용해 요청한 URL로부터 객체로 응답을 받는다.
2. getForEntity : GET 메서드를 이용해 요청한 URL로부터 ResponseEntity로 응답받는다. ResponseEntity 표준은 String이며, Jackson을 이용해 파싱해 사용한다.
3. postForLocation : POST 메서드를 이용해 URL로 생성할 리소스를 보내고, 리소스가 생성되면 생성된 리소스의 URI를 헤더에 담아 반환한다.
4. postForObject : POST 메서드를 이용해 URL로 생성할 리소스를 보내고 객체로 응답을 받는다.
5. postForEntity : POST 메서드를 이용해 URL로 생성할 리소스를 보내고 ResposeEntity로 응답을 받는다.
6. delete : URL로 DELETE 메서드를 요청한다.
7. put : URL로 PUT 메서드를 요청한다.
8. headForHeaders : HEAD 메서드를 이용해 URL의 헤더 정보를 반환한다.
9. patchForObject : PATCH 메서드를 이용해 URL을 요청한다. 파라미터 일부만 업데이트 하는 경우 PATCH를 사용한다.
10. optionsForAllow : URL이 지원하는 Http 메서드를 반환한다.
11. exchange : 모든 HTTP 메서드를 지원하는 메서드이다. HTTP 메서드를 함께 보낸다.
12. execute : Request, Reponse에 대한 콜백을 수정할 수 있다.
모든 메서드를 살펴보진 않고 자주 사용되는 메서드만 간단하게 살펴보자.
1. getForObject
getForObject는 GET 메서드를 이용해 URL을 호출하며, JSON 형태의 문자열이 아닌 객체를 이용해 데이터를 응답받는다.
@Test
void getForObject() {
String url = "<http://localhost:8080/resttemplate/search>";
// 1. getForObject : URL을 호출하고 데이터를 지정한 객체 타입으로 받아온다.
Person response = restTemplate.getForObject(url, Person.class);
log.info("response : {}", response);
assertThat(response).isNotNull();
assertThat(response.getName()).isEqualTo("zayson");
assertThat(response.getAge()).isEqualTo(28);
assertThat(response.getInterest().size()).isEqualTo(3);
}
해당 URL과 매핑되는 Controller 로직을 확인해보면 Person 객체를 Builder를 이용해 리턴했고, 테스트 코드에서 데이터들을 제대로 읽어온 것을 확인할 수 있다.
@GetMapping("/search")
public ResponseEntity<Person> search() {
log.info("RestTemplate Get Request");
Person person = Person.builder().name("zayson")
.age(28)
.interest(List.of("Coding", "Soccer", "Develop"))
.build();
return ResponseEntity.ok(person);
}
2. getForEntity
getForEntity는 GET 메서드를 이용해 URL을 호출하며 응답받는다.
기본적으로 ResponseEntity 표준은 String이며, Jackson을 이용해 데이터를 파싱해 사용한다.
@Test
void getForEntity() throws JsonProcessingException {
// given
String url = "<http://localhost:8080/resttemplate/search>";
// when
// 2. getForEntity : URL을 호출하고 데이터를 ResponseEntity<String>으로 받아온다.
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
log.info("response : {}", response);
// objectMapper를 이용한 파싱
JsonNode person = objectMapper.readTree(response.getBody());
// then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(person.path("name").asText()).isEqualTo("zayson");
assertThat(person.path("age").asInt()).isEqualTo(28);
assertThat(person.path("interest").size()).isEqualTo(3);
}
위의 getForObject와 동일하게 Controller 로직을 통해 Person 객체를 가져온다.
테스트를 진행하며 찍은 로그를 확인하면 아래와 같이 JSON 타입의 문자열로 반환하는 것을 확인할 수 있다. (표준으로 코드 구현)
💡MultiValueMap을 이용해 Query Parmaeter를 추가해서 보내서 가져올 수도 있다.
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("name", "zayson")
params.add("age", "28");
restTemplate.getForEntity(url, String.class, params);
3. postForLocation
postForLocation은 POST 메서드를 이용해 URL로 생성할 리소스를 보내고, 리소스가 생성되면 리소스가 생성된 URI를 반환한다.
@Test
void postForLocation() {
// given
String url = "<http://localhost:8080/resttemplate/new/location>";
Person person = createPerson();
// when
// 3. postForLocation : 리소스를 생성하고 생성된 리소스의 URI 위치를 반환한다.
URI location = restTemplate.postForLocation(url, person);
log.info("Resoure Location : {}", location);
// then
assertThat(location).isNotNull();
}
// 회원 객체 생성
private Person createPerson() {
return Person.builder().name("zayson")
.age(28)
.interest(List.of("Coding", "Soccer", "Develop"))
.build();
}
Person 객체를 하나 생성하고 해당 URL로 보내면, Controller는 리소스를 생성하고 리소스가 생성된 URI와 함께 CREATED를 반환한다.
@PostMapping("/new/location")
public ResponseEntity<Void> joinPersonLocation(@RequestBody Person person, UriComponentsBuilder builder) {
log.info("RestTemplate Post Request");
log.info("Request Person : {}", person);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(builder.path("/location/{name}").buildAndExpand(person.getName()).toUri()); // 리소스 반환 URI
log.info("Reosurce Location : {}", headers.getLocation());
return ResponseEntity.status(HttpStatus.CREATED).headers(headers).build();
}
테스트 코드를 실행하면 요청된 Person 데이터가 로그에 찍히고, Controller에서 생성한 리소스 URI를 반환해주는 것을 확인할 수 있다.
4. postForEntity
postForEntity는 POST 메서드를 이용해 URL에 요청하고, 리소스가 ResponseEntity 타입으로 생성된 리소스를 반환한다.
@Test
void postForEntity() {
// given
String url = "<http://localhost:8080/resttemplate/new>";
Person person = createPerson();
// Http 헤더 설정
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("id", "guest");
HttpEntity<Person> request = new HttpEntity<>(person, httpHeaders);
// when
// 4. postForEntity : 리소스를 생성하고 ResponseEntity타입으로 데이터를 받는다. (헤더 사용)
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
log.info("Created Response : {}", response);
// then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
}
생성한 person 객체를 담아 보내는 것 또한 가능하지만, 헤더를 포함해서 Request하는 경우엔 HttpEntity를 이용할 수 있기 때문에 HttpEntity를 이용해 Request를 생성했다.
HttpServletRequest를 이용해 RestTemplate으로 요청한 헤더가 로그에 찍히는 것을 확인할 수 있다.
@PostMapping("/new")
public ResponseEntity<Person> joinPerson(@RequestBody Person person, HttpServletRequest servletRequest) {
log.info("RestTemplate Post Request");
log.info("Request Person : {}", person);
log.info("Request Header : {}", servletRequest.getHeader("id"));
return ResponseEntity.status(HttpStatus.CREATED).body(person);
}
테스트 코드를 실행하면 리소스가 생성되고, CREATED(201)를 반환해준다.
5. postForObject
postForObject도 postForEntity와 동일하게 POST 메서드를 사용하고 생성된 리소스를 반환한다. 반환된 리소스는 지정한 객체 타입으로 받을 수 있다.
@Test
void postForObject() {
// given
String url = "<http://localhost:8080/resttemplate/new>";
Person person = createPerson();
// Http 헤더 설정
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("id", "guest");
HttpEntity<Person> request = new HttpEntity<>(person, httpHeaders);
// when
// 5. postForObject : 리소스를 생성하고 지정한 객체 타입으로 데이터를 받는다. (헤더 사용)
Person createdPerson = restTemplate.postForObject(url, request, Person.class);
log.info("Created Response : {}", createdPerson.toString());
// then
assertThat(createdPerson).isNotNull();
assertThat(createdPerson.getName()).isEqualTo("zayson");
assertThat(createdPerson.getAge()).isEqualTo(28);
assertThat(createdPerson.getInterest().size()).isEqualTo(3);
}
6. delete
delete는 DELETE 메서드를 이용해 해당 URL에 요청한다. Void 타입으로 Return 되는 데이터가 없다.
@Test
void delete() {
// given
String url = "<http://localhost:8080/resttemplate/{name}/{age}>";
Map<String, String> params = new HashMap<>();
params.put("name", "zayson");
params.put("age", "28");
// when
// 6. delete : DELETE 메서드를 이용해 해당 URL에 요청한다.
restTemplate.delete(url, params);
}
7. put
put 메서드는 PUT 메서드를 이용해 해당 URL에 요청한다. void 타입으로 return 되는 데이터가 없다.
url과 수정할 데이터, 그리고 필요 시 map 타입으로 파라미터를 바인딩해서 요청한다.
@Test
void put() {
// given
String url = "<http://localhost:8080/resttemplate/{name}/{age}>";
Person person = createPerson();
Map<String, String> params = new HashMap<>();
params.put("name", "zayson");
params.put("age", "28");
// when
// 7. put : PUT 메소드를 요청한다.
restTemplate.put(url, person, params);
}
테스트 코드를 실행하면 Controller에서 Path Variable 파라미터와 Request Body 파라미터를 로그 찍는 것을 확인할 수 있다.
@PutMapping("/{name}/{age}")
public ResponseEntity<Void> put(@PathVariable("name") String name, @PathVariable("age") Integer age,
@RequestBody Person person) {
// logic...
log.info("Put Method Request");
log.info("Path Name : {}", name);
log.info("Path age : {}", age);
log.info("Request Body : {}", person);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
8. headForHeaders
headForHeaders는 HEAD 메서드를 이용해 URL에 포함된 Header 정보들을 반환한다.
@Test
void headForHeaders() {
// given
String url = "<http://localhost:8080/resttemplate/search>";
// when
// 8. headForHeaders : HEAD 메서드를 이용해 해당 URL이 포함한 헤더 정보를 반환한다.
HttpHeaders httpHeaders = restTemplate.headForHeaders(url);
log.info("Headers : {}", httpHeaders);
// then
assertThat(httpHeaders.getContentType()).isNotNull();
assertThat(httpHeaders.getContentType().includes(MediaType.APPLICATION_JSON)).isTrue();
}
테스트 코드를 실행하면 HEAD 메서드를 사용한 것을 확인할 수 있고, URL의 헤더정보를 반환하는 것을 확인할 수 있다.
9. exchange
exchange는 모든 Http 메서드를 이용해 요청할 수 있다.
@Test
void exchange_GET() {
// given
String url = "<http://localhost:8080/resttemplate/search>";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> reqeust = new HttpEntity<>(headers);
// when
// 9. exchange : 모든 HTTP 메서드로 요청하는 것이 가능하다. (GET 방식, String 타입으로 받기)
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, reqeust, String.class);
log.info("response : {}", response);
log.info("statusCode : {}", response.getStatusCode());
// then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
GET 방식으로 요청을 하는 테스트 코드이다. getForEntity를 요청한 것과 동일한 결과를 나타낸다.
String 타입 뿐만 아니라 객체 타입으로도 반환 받을 수 있다.
POST 방식으로 exchange를 요청해보자. 이번에는 String 타입이 아닌 지정한 Person 객체 타입으로 반환을 받을 것이다.
@Test
void exchange_POST() {
// given
String url = "<http://localhost:8080/resttemplate/new>";
Person person = createPerson();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Person> request = new HttpEntity<>(person, headers);
// when
// POST 방식 객체 타입으로 반환 받기
ResponseEntity<Person> response = restTemplate.exchange(url, HttpMethod.POST, request, Person.class);
log.info("response : {}", response);
log.info("statusCode : {}", response.getStatusCode());
// then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
}
URL을 통해 로직이 실행되고 Person 타입으로 객체가 반환되는 것을 확인할 수 있다.
10. execute
execute 메서드는 exchange와 동일하게 모든 Http Method를 사용하는 것이 가능하다.
execute의 가장 큰 특징은 RequestCallback과 ResponseExtractor를 이용해 요청 후 콜백, 응답 후 콜백을 통해 로직을 개발자가 컨트롤 할 수 있다는 점이다.
@Test
void exectue() throws JsonProcessingException {
// given
String url = "<http://localhost:8080/resttemplate/search>";
// Request Callback
RequestCallback requestCallback = request -> {
request.getHeaders().set("id", "manager");
log.info("Request Callback : {}", request.getHeaders());
};
// Response Callback
ResponseExtractor<Person> responseExtractor = response -> {
Person person = objectMapper.readValue(response.getBody(), Person.class);
log.info("Status Code : {}",response.getStatusCode());
log.info("Response Body : {}", person.toString());
return person;
};
// when
// 10. execute : 모든 HTTP 메서드를 사용할 수 있고, request Callback과 Response Extractor를 이용해 요청 후 로직과 응답 후 로직을 사용자가 컨트롤 할 수 있다.
Person person = restTemplate.execute(url, HttpMethod.GET, requestCallback, responseExtractor);
// then
assertThat(person).isNotNull();
assertThat(person.getName()).isEqualTo("zayson");
assertThat(person.getAge()).isEqualTo(28);
assertThat(person.getInterest().size()).isEqualTo(3);
}
위의 테스트 코드에선 Request Callback과 Response Extractor에 각각 로그를 찍어서 확인했다.
execute 메서드는 외부 API를 통해 파일같은 데이터를 가져오고 응답 직후에 다른 디렉토리에 저장( 응답 후 콜백) 하는 방식같이 응답 후에 콜백을 통해 로직을 처리하기에 유용할 것 같다.
11. optionsForAllow
optionsForAllow는 OPTIONS 메서드를 이용해 요청한 URL이 지원하는 HttpMethod를 확인할 수 있다.
@Test
void optionsForAllow() {
// given
String url = "<http://localhost:8080/resttemplate/{name}/{age}>";
Map<String, String> params = new HashMap<>();
params.put("name", "zayson");
params.put("age", "28");
// when
// 11. OPTIONS OPTIONS 메서드를 이용해 URL이 지원하는 HTTP METHOD를 반환받는다.
Set<HttpMethod> httpMethods = restTemplate.optionsForAllow(url, params);
log.info("response : {}", httpMethods);
// then
assertThat(httpMethods.containsAll(List.of(HttpMethod.PUT, HttpMethod.DELETE))).isTrue();
}
테스트 코드의 URL은 delete, put 메서드에 사용한 URL과 동일하다.
따라서, URL은 PUT, DELETE, OPTIONS 메서드를 지원할 것이다.
Timeout 설정
다른 서버와 통신을 하다보면 Timeout이 발생할 수 도 있다.
RestTemplate은 ClientHttpRequestFacotry를 이용해서 Timeout설정을 하는 것이 가능하다.
ClientHttpRequestFactory를 이용해 Timeout을 설정하고, RestTemplate을 이용해 요청을 해보자.
@Test
void timeout() {
// given
RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
String url = "<http://localhost:8080/resttemplate/timeout>";
// then
assertThrows(ResourceAccessException.class, () -> restTemplate.getForObject(url, String.class));
}
// Timeout 설정
private ClientHttpRequestFactory getClientHttpRequestFactory() {
int timeout = 1000;
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(timeout)
.setConnectionRequestTimeout(timeout)
.setSocketTimeout(timeout)
.build();
CloseableHttpClient client = HttpClientBuilder
.create()
.setDefaultRequestConfig(config)
.build();
return new HttpComponentsClientHttpRequestFactory(client);
}
요청 URL과 매핑되는 Controller 로직은 Thread.sleep(5000)을 통해 5초간 멈추게 해놓았다.
@GetMapping("/timeout")
public ResponseEntity<Person> timeout() throws InterruptedException {
log.info("Timeout Test");
Person person = Person.builder()
.name("zayson")
.age(28)
.interest(List.of("Coding", "Soccer", "Develop"))
.build();
// 타임아웃 발생!!
Thread.sleep(5000);
return ResponseEntity.ok(person);
}
RestTemplate에 설정한 Timeout은 1000이므로 예외가 발생하게 되는데, 기본적으로 ResourceAccessException을 발생시킨다.
📄 References
Baeldung : https://www.baeldung.com/rest-template
'Backend > Spring Boot' 카테고리의 다른 글
API 개발 기본 간단 정리 (0) | 2022.07.07 |
---|---|
도메인 개발 팁 간단 정리 (0) | 2022.07.05 |
Converter를 이용해 URI에 Enum 타입 매핑하기 (0) | 2022.06.26 |
Spring Cloud Config의 설정 파일 비대칭키로 암/복호화 (0) | 2022.06.03 |
RestControllerAdvice, ExceptionHandler를 이용한 전역 예외 처리 (0) | 2022.05.27 |