REST API Client Library 알아보기

박진선·2022년 12월 17일
0
post-custom-banner

REST API 사용자 입장(Client)에서 활용할 수 있는 JAVA Library를 알아보려 한다.
REST API Client에 JAVA Library로는 HttpURLConnection, HttpClient, OkHttp, Retrofit, RestTemplate를 대표적으로 들 수 있다. 이외에도 많은 Library가 존재하지만 많은 개발자가 선택하고 사용하고 있는 Library에 대해 간략하게 알아보고 RestTemplate 에 대해 기술하려 한다.

1. HttpURLConnection

  • 기본 JDK에 포함되어 있음. (jdk1.2부터 내장되어 있으며 java.net 패키지에 있다.)
  • 상대적으로 가벼우며 핵심적인 API만 지원하고 있음.
  • HttpClient 보다 성능이 좋음
  • 서버로부터 전달 받은 Response 결과를 Stream으로 직접 처리해야 하는 등.. 개발 생산성이 떨어지는 요소가 다소 있음.

2. HttpClient

  • Apache에서 제공한다.
  • HttpClient는 3버전과 4버전이 있으며 4버전부터는 HttpComponents로 불린다.(단, 3버전과 4버전은 둘간 직접적인 호환은 되지 않음)
  • HttpComponents(4버전) 부터는 Thread에 안정적인 기능들을 많이 제공한다.
  • 상대적으로 무거움
  • HttpURLConnection 대비 다양한 API를 지원함.

3. OKHttp

  • Square의 오픈소스 프로젝트
  • OKHttp는 통신을 동기화로 할지 비동기로 처리 할지 선택하여 사용할 수 있음.
  • 단 스레드를 넘나들 수 없음. (스레드간에 데이터를 공유하기 위해서는 Handler를 활용해야함)

4. Retrofit

  • Square의 오픈소스 프로젝트
  • 어노테이션을 사용하여 개발할 수 있으므로 개발의 생산성 및 가독성이 올라감.
  • 어노테이션을 사용하여 코드를 생성하기 때문에 인터페이스를 적용하여 주로 개발함.

5. RestTemplate

  • RestTemplate은 간편하게 Rest방식의 API를 호출할 수 있는 Spring 내장 클래스 이다.
  • Spring 3.0부터 지원하는 Spring의 HTTP 통신 템플릿이다.
  • Restful의 원칙을 지킬 수 있으며 HTTP 메소드들에 적합한 여러 메소드 제공한다.
  • JSON, XML, String 응답을 모두 받는다.
  • Blocking I/O 기반의 동기방식을 사용한다.(Rest API 호출 후 응답을 받을 때까지 기다림)
  • Header + Content-Type을 설정해서 외부 API 호출이 가능 하다.
  • java.net.HttpURLConnection, Apache HttpComponents, OkHttp 3, Netty 같은 HTTP Client 라이브러리 중 하나를 유연하게 사용할 수 있다.

6. WebClient

  • WebClient는 RestTemplate를 대체하는 HTTP 클라이언트이다. 기존의 동기 API를 제공할 뿐만 아니라, 논블로킹 및 비동기 접근 방식을 지원해서 효율적인 통신이 가능하다.
  • WebClient는 요청을 나타내고 전송하게 해주는 빌더 방식의 인터페이스를 사용하며, 외부 API로 요청을 할 때 리액티브 타입의 전송과 수신을 합니다. (Mono, Flux)
  • WebClient 특징 3가지 싱글 스레드 방식을 사용, Non-Blocking 방식을 사용, JSON, XML을 쉽게 응답받는다.

RestTemplate 메소드

RestTemplate의 동작원리

  1. 애플리케이션이 RestTemplate을 생성하고 URI, HTTP 메소드 등을 헤더에 담아 요청
  2. RestTemplate은 HttpMessageConverter를 사용하여 RequestEntity를 요청메세지로 변환
  3. RestTemplate은 ClitentHttpRequestFactory로 부터 ClientHttpRequest를 가져온후 요청을 보냄
  4. ClientHttpRequest는 요청메세지를 만들어 HTTP 프로토콜을 통해 서버와 통신
  5. RestTemplate은 ReponseErrorHandler로 오류를 확인
  6. ResponseErrorHandler는 오류가 있다면 ClientHttpResponse에서 응답 데이터를 가져와서 처리
  7. RestTemplate은 HttpMessageConverter를 이용해서 응답메세지를 Java Object(Response Type)로 변환
  8. 애플리케이션에 반환

코드에 사용되는 공용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()

  • URI 템플릿을 먼저 인코딩한 후, URI 템플릿 변수 위치에 URI 변수 값을 삽입할 때 그 값을 인코딩한다. 즉 Reserved 문자가 Percent-Encoding 된다.

② UriComponents 클래스의 encode()

  • URI에 포함된 템플릿 변수 위치에 URI 변수 값을 삽입한 후에 URI 컴포넌트를 인코딩한다.
    즉 Reserved 문자가 Percent-Encoding되지 않는다.

두 가지 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 : 응답으로 전달 받을 클래스의 타입을 지정한다.

RestTemplate 설정

보통 아래와 같이 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에서 사용된다.

ConnectionTimeout은 socket.connect에서 사용된다.

TCP 네트워크 통신을 위해서는 먼저 상대방(서버)과 커넥션을 맺기위한 TCP 3-Way Handshake가 수행되어야 한다.
커넥션을 맺기 위해 Socket 인스턴스에서 socket.connect 함수를 호출하게 되는데 이때 설정한 ConnectionTimeout 값을 사용하게 된다.
만약 커넥션을 맺는 시간이 ConnectionTimeout을 초과하게 되면 SocketTimeoutExcpetion(Connect timed out)이 발생한다.

ReadTimeout은 socket.setSoTimeout에서 사용된다.

setSoTimeout은 서버의 응답 데이터를 읽을 때의 타임아웃을 지정하기 위해 사용된다.
서버가 데이터를 반환하면 해당 데이터는 Socket 내부의 SocketInputStream.read()를 통해서 바이트 형태로 읽을 수 있다.
각 HTTP Client 구현체들은 요청을 보내고 응답 데이터를 읽기 위해 SocketInputStream.read()를 호출하게 되는데 ReadTimeout이 지날 때 동안 서버가 응답을 하지 않아 SocketInputStream에 아무런 데이터도 쌓이지 않는다면 SocketTimeoutException(Read timed out)이 발생한다.

profile
주니어 개발자 입니다
post-custom-banner

0개의 댓글