REST API 사용자 입장(Client)에서 활용할 수 있는 JAVA Library를 알아보려 한다.
REST API Client에 JAVA Library로는 HttpURLConnection, HttpClient, OkHttp, Retrofit, RestTemplate를 대표적으로 들 수 있다. 이외에도 많은 Library가 존재하지만 많은 개발자가 선택하고 사용하고 있는 Library에 대해 간략하게 알아보고 RestTemplate 에 대해 기술하려 한다.
RestTemplate 메소드
RestTemplate의 동작원리
코드에 사용되는 공용API는 현지 시간을 plain-text or JSON 으로 반환해주는 World Time API 이다.
public class RestClientExample01 {
public static void main(String[] args) {
// (1) RestTemplate 객체 생성하며 생성자에 HttpComponentsClientHttpRequestFactory를 파라미터로 전달
RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
// (2) URI 생성
URI uri = UriComponentBuilder
.newInstance() // UriComponentsBuilder 객체를 생성
.scheme("http") // URI의 scheme을 설정
.host("worldtimeapi.org") // 호스트 정보를 설정
.port(80) //포트번호를 설정 기본값은 80이므로 80 포트를 사용하는 호스트라면 생략 가능
.path("/api/timezone/{continents}/{city}") // URI의 경로(path)를 설정 두 개의 템플릿 변수를 사용했음
.encode() // URI에 사용된 템플릿 변수들을 인코딩함, non-ASCII 문자와 URI에 적절하지 않은 문자를 Percent Encoding 한다는 의미
.buildAndExpand("Asia", "Seoul") // UriComponents 객체를 생성 및 파라미터 값을 템플릿 변수의 값으로 대체함
.toUri(); // URI 객체를 생성함
//(3) getForObject 메소드를 호출하여 인자값으로 uri 및 API 응답데이터를 반환받을 타입을 String으로 설정함
String result = restTemplate.getForObject(uri, String.class);
System.out.println(result);
// (4) getForObject 메소드 인자값을 WorldTime으로 설정하여 API 응답데이터를 WorldTime 으로 받음
WorldTime worldTime = restTemplate.getForObject(uri, WorldTime.class);
System.out.println(worldTime.getDatetime);
System.out.println(worldTime.getTimeZone);
System.out.println(worldTime.getDay_of_week);
// (5) getForEntity 메소드를 호출하여 API 응답데이터를 ResponseEntity<WorldTime> 반환 받음
ResponseEntity<WorldTime> response = restTemplate.getForEntity(uri, WorldTime.class);
System.out.println("# datatime: " + response.getBody().getDatetime());
System.out.println("# timezone: " + response.getBody().getTimezone()());
System.out.println("# day_of_week: " + response.getBody().getDay_of_week());
System.out.println("# HTTP Status Code: " + response.getStatusCode());
System.out.println("# HTTP Status Value: " + response.getStatusCodeValue());
System.out.println("# Content Type: " + response.getHeaders().getContentType());
System.out.println(response.getHeaders().entrySet());
// (6) exchange() 메소드를 호출하여 API 응답데이터를 ResponseEntity<WorldTime> 반환 받음
ResponseEntity<WorldTime> response = restTemplate.exchange(uri, HttpMethod.GET, null, WorldTime.Class);
System.out.println("# datatime: " + response.getBody().getDatetime());
System.out.println("# timezone: " + response.getBody().getTimezone());
System.out.println("# day_of_week: " + response.getBody().getDay_of_week());
System.out.println("# HTTP Status Code: " + response.getStatusCode());
System.out.println("# HTTP Status Value: " + response.getStatusCodeValue());
}
@Getter
public static class WorldTime {
private String datetime;
private String timezone;
private int day_of_week;
}
}
(1) RestTemplate의 객체를 생성하기 위해서는 RestTemplate의 생성자 파라미터로 HTTP Client 라이브러리의 구현 객체를 전달해야한다.
코드에서는 HttpComponentsClientHttpRequestFactory 클래스를 통해 Apache HttpComponents를 전달한다. Apache HttpComponents를 사용하기 위해서는 builde.gradle의 dependencies 항목에 아래와 같이 의존 라이브러리를 추가해야한다.
dependencies {
...
...
implementation 'org.apache.httpcomponents:httpclient'
}
(2) 스프링에서 지원하는 UriComponentBuilder를 이용해 UriComponents의 인스턴스를 생성하여 원하는 URI를 손쉽게 생성한다. java.net 패키지의 URI 클래스와 비슷한 면이 있지만, Encoding 옵션과 URI 템플릿 변수를 다룰 수 있기 때문에 더 강력하다.
UriComponentsBuilder는 URI를 인코딩함에 있어 두 가지 메서드를 제공한다.
① UriComponentsBuilder 클래스의 encode()
② UriComponents 클래스의 encode()
두 가지 encode() 메서드는 모두 non-ASCII 문자와 URI에 적절하지 않은 문자를 인코딩한다.
그러나, ①의 encode()는 URI 변수에 포함된 reserved 문자까지 인코딩하게 된다.
URI에 포함되는 Reserved 문자는 문법적 의미를 가지기 때문에 그 의미로 사용할 것이라면 Percent-Encoding 되면 안된다. 따라서, 이러한 값들이 인코딩되지 않게 하기 위해서는 ②의 encode() 를 이용해야만 한다.
URL, URI 인코딩의 공식 명칭은 Percent-encoding이다. URI에 허용되는 문자는 크게 reserved와 unreserved로 나뉜다.
reserved 문자
! ' ( ) ; : @ & = + $ , / ? # [ ]
unreserved 문자
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
a b c d e f g h i j k l m n o p q r s t u v w x y z
0 1 2 3 4 5 6 7 8 9 - _ . ~
reserved와 unreserved에 해당하지 않는다면 모두 Percent-encoding되어야 한다.
reserved 문자는 특별한 의미를 갖는데 특별한 의미없이 사용하려면, Percent-encoding되어야 한다. Percent-encoding은 ASCII인 경우 ASCII 값의 16진수 표현 앞에 %를 붙인다. non-ASCII인 경우 인코딩에 따라 각 바이트를 16진수 표현으로 바꾸고 앞에 %를 붙인다.
다음은 각 reserved 문자의 Percent-encoding 값이다.
! # $ & ' ( ) + , / : ; = ? @ [ ]
%21 %23 %24 %26 %27 %28 %29 %2A %2B %2C %2F %3A %3B %3D %3F %40 %5B %5D
(3,4) getForObject(URI uri, Class responseType)
메소드를 호출하여 원하는 클래스 타입으로 API 응답 데이터를 받을 수 있다 커스텀 클래스를 생성하여 응답 데이터를 전달 받기 위해서는 응답 데이터의 JSON 프로퍼티 이름과 클래스의 필드명이 동일해야 하고 해당 필드에 접근하기 위한 getter 메서드 역시 동일한 이름이어야 한다.
(5) <T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType)
메소드를 호출하여 URI와 반환받을 클래스 타입을 지정하여 응답 데이터는 ResponseEntity 클래스로 래핑되어서 전달 되며 예제 코드와 같이 getBody(), getHeaders() 메서드 등을 이용해서 바디와 헤더 정보를 얻을 수 있다.
(6) <T> ResponseEntity<T> exchange(URI uri, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType)
getForObject(), getForEntity() 등과 달리 exchange() 메서드는 HTTP Method, RequestEntity, ResponseEntity를 직접 지정해서 HTTP Request를 전송할 수 있는 가장 일반적인 방식이다.
URI url : Request를 전송할 엔드포인트의 URI 객체를 지정한다.
HttpMethod method : HTTP Method 타입을 지정한다.
HttpEntity<?> requestEntity : HttpEntity 객체를 통해 헤더 및 바디, 파라미터 등을 설정할 수 있다.
Class<T> responseType : 응답으로 전달 받을 클래스의 타입을 지정한다.
보통 아래와 같이 Bean 으로 설정하여 사용한다.
@Configuration
public class RestTemplateConfig{
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(5))
.build();
}
}
RestTemplateBuilder.build 시점에 실제 HTTP 통신을 위한 HTTP Client 구현체를 선택하게 된다.
스프링 부트에서는 기본적으로 3개의 구현체를 기반으로 선택하는데 선택 규칙은 ClientHttpRequestFactories.java에 정의되어 있다.
만약 프로젝트에 Apache Http Client가 존재한다면 Apache Http Client를 사용한다.
만약 프로젝트에 OkHttp Client가 존재한다면 OkHttp Client를 사용한다.
마지막으로 표준 JDK가 제공하는 java.net.HttpURLConnection를 사용한다.
RestTemplate를 일반 생성자로 만드는 경우 디폴트로 java.net.HttpURLConnection를 사용한다.
필요하다면 ClientHttpRequestFactory를 직접 구현해서 새로운 Http Client 구현체를 사용할 수 있다.
모든 HTTP Client 구현체들은 결국 Socket.java 을 사용한다.
해당 클래스는 네트워크 통신을 위해 제공하는 클라이언트 소켓 구현체이다.
그러므로 RestTemplateBuilder에서 설정한 Timeout 설정 값들은 Socket에서 사용된다.
TCP 네트워크 통신을 위해서는 먼저 상대방(서버)과 커넥션을 맺기위한 TCP 3-Way Handshake가 수행되어야 한다.
커넥션을 맺기 위해 Socket 인스턴스에서 socket.connect 함수를 호출하게 되는데 이때 설정한 ConnectionTimeout 값을 사용하게 된다.
만약 커넥션을 맺는 시간이 ConnectionTimeout을 초과하게 되면 SocketTimeoutExcpetion(Connect timed out)이 발생한다.
setSoTimeout은 서버의 응답 데이터를 읽을 때의 타임아웃을 지정하기 위해 사용된다.
서버가 데이터를 반환하면 해당 데이터는 Socket 내부의 SocketInputStream.read()를 통해서 바이트 형태로 읽을 수 있다.
각 HTTP Client 구현체들은 요청을 보내고 응답 데이터를 읽기 위해 SocketInputStream.read()를 호출하게 되는데 ReadTimeout이 지날 때 동안 서버가 응답을 하지 않아 SocketInputStream에 아무런 데이터도 쌓이지 않는다면 SocketTimeoutException(Read timed out)이 발생한다.