인생을 코딩하다.

[Spring] RestTemplate 본문

Spring

[Spring] RestTemplate

Hyung1 2020. 12. 17. 05:31
728x90
반응형

open API 활용을 위해 서비스 인증 키를 이용하여 raw 데이터를 받아오면서 공부하게된 RestTemplate

 

우선 서비스 인증키를 이용하기 위해

String decodeServiceKey = URLDecoder.decode(serviceKey, "UTF-8");

디코딩하고

 

UriComponents 클래스를 이용하였다.

        UriComponents uri = UriComponentsBuilder.fromHttpUrl(url).queryParam("serviceKey", decodeServiceKey)
                .queryParam("pageNo", pageNo)
                .queryParam("numOfRows", numOfRows)
                .queryParam("startCreateDt", startCreateDt)
                .queryParam("endCreateDt", endCreateDt)
                .build(false);

URI를 동적으로 생성해주는 클래스다. 파라미턱가 조합된 서비스 인증키 URI를 손쉽게 만들어 준다. 

UriComponents를 사용하면 파라미터 값을 지정하거나 변경하는 일이 쉬워진다. 원하는 URI로 링크를 생성할 수 있어 RestFul하다.

 

이제 생성한 uri를 이용해 customize하여 raw 데이터를 받아와야 한다. 그러기 위해서는 HTTP 서버와 통신을 해야한다.

 

HTTP 서버와 통신하는 방법 중에는 URLConnection, HttpClient 및 RestTemplate가 있다.

 

우선 URLConnection 통신 방법은 아래와 같은 문제점이 있다.

- 응답코드가 4xx 거나 5xx면 IOEception 이 터진다.

- 타임아웃을 설정할 수 없다.

- 쿠키 제어가 불가

 

이에 비해 Apache에서 제공하는 HttpClient에는 위에 말한 문제점들을 해결하여 준다. 하지만 아래와 같은 문제점들이 있다.

- 여전히 반복적이고 코드들이 길다.

- 스트림 처리 로직을 별도로 짜야한다.

- 응답의 컨텐츠 타입에 따라 별도로 로직이 필요하다.


마지막으로 spring 3.0부터 지원하는 http 통신에 유용하게 쓸 수 있는 RestTemplate가 있다. HTTP 서버와의 통신을 단순화하고 RESTFul 원칙을 지킨다. 기계계적이고 반복적인 코드들을 깔끔하게 정리해준다.

 

HttpClient보다 RestTemplate가 요청을 보내고 받는 일에 있어서 코드가 훨씬 짧고 가독성이 좋았다.

즉 Boilerplate code(재사용 가능)를 줄여준다. 특히 응답의 컨텐츠 타입에 따라 별도로 로직이 필요한 HttpClien에비해

훨씬 효율적이다. 그래서 open API 활용을 위해 서비스 인증 키를 이용하여 raw 데이터를 받는 작업을 하기 위해 spring에서 제공하는 RestTemplate을 이용하기로 하였다.

 

 

RestTemplate의 동작원리

 

 

  • 어플리케이션이 RestTemplate를 생성하고, URI, HTTP메소드 등의 헤더를 담아 요청한다.
  • RestTemplate 는 HttpMessageConverter 를 사용하여 requestEntity 를 요청메세지로 변환한다.
  • RestTemplate 는 ClientHttpRequestFactory 로 부터 ClientHttpRequest 를 가져와서 요청을 보낸다.
  • ClientHttpRequest 는 요청메세지를 만들어 HTTP 프로토콜을 통해 서버와 통신한다.
  • RestTemplate 는 ResponseErrorHandler 로 오류를 확인하고 있다면 처리로직을 태운다.
  • ResponseErrorHandler 는 오류가 있다면 ClientHttpResponse 에서 응답데이터를 가져와서 처리한다.
  • RestTemplate 는 HttpMessageConverter 를 이용해서 응답메세지를 java object(Class responseType) 로 변환한다.
  • 어플리케이션에 반환된다.

RestTemplate는 connection pool을 사용하지 않는다. 연결할때마다 로컬 포트를 열고 tcp connection을 맺는다. 이때  

문제는 close() 이후에 사용된 소켓은 TiME_WAIT 상태가 된다. 진행중인 프로젝트에 요청량이 많다면 이런 소켓들을    재사용하지 못하고 소켓이 오링나서 응답이 지연될 것이다.

 

예를 들면, 한 번에 수많은 사람들이 코로나 일일 확진자를 확인하기 위해 사이트에서 데이터를 요청하게 된다. 하지만 connection pool 하지 않으면 부하에 견디지 못해 서버에러가 생길 것이다 것이다.

 

요청한 서버에서 응답을 늦게 줄 때, 서버의 Thread도 급격히 증가하는 문제가 발생할 수 있다. 이는 서버의 전체적인 장애로 연결되는데, 이러한 문제를 차단하기 위해 Connetion수를 제한해야한다.
(RestTemplate는 동기 방식이다. Thread의 개념을 알고 있어야 위에 말한 내용을 이해 할 수 있을 것이고, 카테고리 자바의 쓰레드 정리해논 내용 참고)

 

그래서 connection pool을 이용하여 위의 문제를 해결해야한다. RestTemplate에서 connection pool을 이용할 수 있다. 이유는 RestTemplate 내부적으로 사용되는 HttpClient에 의해서다.

 

 

Spring에서는 HttpComponentsClientHttpRequestFactory를 제공한다. 이것을 이용해 timeout등을 설정 할 수 있다. 하지만 HttpComponentsClientHttpRequestFactory에는 connection의 수를 제한 할 수 있는 것이 없었다.

 

하지만,

 

HttpComponentsClientHttpRequestFactory에는 connection

대신에 import org.apache.http.impl.client.CloseableHttpClient을 매개변수로 받을 수 있는 method를 제공한다.
HttpClient는 Bulider을 이용해 새로운 객체를 만들어서 위의 매개변수로 받을 수 있는 merhod의 인자로 넣어주면서 추가적인 설정을 할 수있다.

 

connection pool을 적용하기 위한  HttpClientBuilder를 사용하기 위해서는 dependency 라이브러리가 필요하다.

compileOnly 'org.apache.httpcomponents:httpclient:4.5'

 

아래 코드를 보면 커넥션 수(MaxConnTotal)을 제한하고 IP, 포트 1쌍 당 동시 수행할 연결 수(MaxConnPerRoute)를 제한하는 설정이 포함되어 있다. 이런식으로 최대 커넥션 수를 100개로 제한하여 100개의 자원내에서 모든 일을 수행하게 된다. 

       HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    
       CloseableHttpClient httpClient = HttpClientBuilder.create()
                .setMaxConnTotal(100) // maxConnTotal은 연결을 유지할 최대 숫자
                .setMaxConnPerRoute(50) // maxConnPerRoute는 특정 경로당 최대 숫자
                .build();

        factory.setReadTimeout(5000); // 읽기 시간 초과
        factory.setConnectTimeout(3000); // 연결 시간 초과
        factory.setHttpClient(httpClient); // 동기실행에 사용될 HttpClient 세팅

요청한 서버에서 응답을 늦게 줄 때, 서버의 Thread도 급격히 증가하는 문제가 발생할 수 있다. 이는 서버의 전체적인 장애로 연결되는데, 이러한 문제를 차단하기 위해 Connetion수를 제한하였다.

 

이제 raw 데이터를 받기 위해 위에서 변환한 서비스 인증키 url를 통해 GET 요청을 해야한다.

RestTemplate으로 Http GET 요청을 하는 방법에는 여러가지가 있다. 나는 그중 결과를 객체로 받는

restTemplate.getForObject()를 사용하였다. 

 

Object response = restTemplate.getForObject(uri.toUriString(), String.class);

Object obj = restTemplate.getForObject("요청할 URI 주소", "응답내용과 자동으로 매핑시킬 java object");

 

get 요청을 보내고 java object로 매핑받아서 반환받는다.

결과 반환 값을 JSON 문자열을 받기 위해서 String.class로 지정하였다. String.class로 지정하면 JSON을 자동으로 반환되는 객체로 매핑해준다.

 

    @GetMapping("/rawDataList")
    public Object rawData(RawDataDto rawDataDto) throws UnsupportedEncodingException {
        
        Object response = rawDataService.rawDataCode(rawDataDto.getPageNo(), rawDataDto.getNumOfRows(),
                rawDataDto.getStartCreateDt(), rawDataDto.getEndCreateDt());

        return response;

    }

이제 controller에서 query string 방식으로 특정 리소스들에 관해 get 요청을 하였더니 

 

 

raw 데이터로 잘 들어왔다.

 

이제 에러 처리를 위해 Spring에서 제공하는 DefaultResponseErrorHandler를 사용하여 HTTP Error 를 제어한다.

 

Resttemplate는 우선 Http Status Code로 1차적으로 API 이상 유무를 검사하게 된다. 2xxx 이 외의 코드가 넘어오게 되면 Resttemplate 예외를 발생시킨다.

 

그런데 문제는 2xx http status code를 응답받고 위 JSON 같이 success에 false를 주는 API들이다.

ResponseErrorHandler을 상속하면 If ~ False로 다 처리해주어야 하는데 defaultResponseErrorHandler보다 효율적이지 않다.

 

ResponseErrorHandler 예제 사진 아직 미작성...

 

ResponseErrorHandler를 상속해도 되지만, DefaultResponseErrorHandler를 사용하면 쉽게 할 수 있다.

@Slf4j
public class RestTemplateResponseErrorHandler extends DefaultResponseErrorHandler {
    // hasError에서 true를 return하면 해당 메서드 실행.
    @Override
    public void handleError(ClientHttpResponse response) throws IOException {
        log.error("Has error response: {}", response);
        super.handleError(response);
    }

    // response.getBody() 넘겨 받은 body 값으로 적절한 예외 상태 확인 이후 boolean retur
    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException {
        log.error("Has error response: {}", response);
        return super.hasError(response);
    }
}

Response 객체에 "success" : false 를 hasError() 메서드에서 확인하고, false가 되면 handleError()에서 추가적인 에러 핸들링 작업을 이어나갈 수 있다. 이렇게 ResponseErrorHandler 등록을 하면 위처럼 반복 적인 if else 문을 작성하지 않아도 된다.

 

..수정 예정

728x90
반응형
Comments