제육's 휘발성 코딩
반응형
반응형

RestTemplate 이란?

RestTemplate은 스프링 3.0 버전부터 지원하는 라이브러리로, REST 방식의 API를 요청하고 json, xml, String 등 응답받을 수 있다.

Spring 5부턴 WebFlux와 함께 WebClient를 도입하여 동기식 방식 및 비동기 접근을 지원하고 있다.

 

 

RestTemplate 동작 과정

image

RestTemplate restTemplate = new RestTemplate();
  • 애플리케이션에서 Http Rest API 요청을 위한 RestTemplate을 생성한다.

 

private final List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
public RestTemplate() {
  // 자동등록 리스트
  this.messageConverters.add(new ByteArrayHttpMessageConverter());
  this.messageConverters.add(new StringHttpMessageConverter());
  this.messageConverters.add(new ResourceHttpMessageConverter(false));
  if (jackson2Present) {
            this.messageConverters.add(new MappingJackson2HttpMessageConverter());
    }
  //...
}
  • MessageConverter를 이용해서 객체를 메시지 형태에 맞춰 request body에 변환하여 담아준고, body의 타입을 Content-type으로 명시해준다.
  • 메시지 컨버터를 통해 jackson 라이브러리에 포함된 ObjectMapper를 통해 JSON을 읽고 쓸 수 있게 해준다.

 

/**
     * Create a new instance of the {@link RestTemplate} based on the given {@link ClientHttpRequestFactory}.
     * @param requestFactory the HTTP request factory to use
     * @see org.springframework.http.client.SimpleClientHttpRequestFactory
     * @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory
     */
public RestTemplate(ClientHttpRequestFactory requestFactory) {
  this();
  setRequestFactory(requestFactory);
}
  • ClientHttpRequestFactory 에서 ClientHttpRequest를 받아와 요청을 전달한다. 실제로 요청을 수행하는 객체는 ClientHttpRequest 가 수행한다.

 

public void setErrorHandler(ResponseErrorHandler errorHandler) {
  Assert.notNull(errorHandler, "ResponseErrorHandler must not be null");
  this.errorHandler = errorHandler;
}

public ResponseErrorHandler getErrorHandler() {
  return this.errorHandler;
}
  • ClientHttpResponse 에 오류가 있으면 에러 핸들링 처리를 한다. 문제가 없으면 MessageConverter를 통해 결과를 애플리케이션에 돌려준다.

 

요청 메서드

  • getForObject(String url, ...), postForObject(String url, ...) : GET, POST 방식으로 요청하며, 객체로 결과를 반환한다.
  • getForEntity(String url, ...), postForEntity(String url, ...) : GET, POST 방식으로 요청하며, ResponseEntity로 결과를 반환한다.
  • postForLocation(String url, ...) : POST 방식으로 요청하며, 헤더에 저장된 URL을 결과로 반환한다.
  • exchange(String url, HttpMethod method, ...) : 모든 HTTP 메서드를 사용할 수 있으며, 그에 맞는 결과 값을 반환한다.
  • execute(String url, HttpMethod method, ...) : 모든 요청메서드들이 마지막에 실행하는 메서드로, Request나 Response 콜백을 수정할 수 있다.

 

RestTemplate 사용 예제

@GetMapping("/getForAny/{id}")
public ResponseEntity<User> getForAny(@PathVariable Long id) {
  User user = User
    .builder()
    .id(id)
    .name("sasca")
    .build();
  return new ResponseEntity<>(user, HttpStatus.OK);
}
  • getForEntity와 getForobject 테스트를 위해, GET 요청을 받았을 때 OK 응답을 만들어주는 컨트롤러 메서드를 만들어주자.

 

private RestTemplate restTemplate;
private static final String DEFAULT_URL = "http://localhost:8080";
private Map<String, Object> params;

@BeforeEach
public void before() {
  restTemplate = new RestTemplate();
  params = new HashMap<>();
  params.put("id", 1);
}

@Test
@DisplayName("getForObject 테스트")
public void getForEntity() throws IOException {
  UriComponents uriComponents = UriComponentsBuilder
    .fromHttpUrl(DEFAULT_URL + "/getForAny")
    .path("/{id}")
    .buildAndExpand(params);
  User user = restTemplate.getForObject(uriComponents.toUriString(), User.class);
  Assertions.assertThat(user.getName()).isEqualTo("sasca");
}

@Test
@DisplayName("getForEntity 테스트")
public void getForObject() {
  String url = DEFAULT_URL + "/getForAny/{id}";
  ResponseEntity<User> user = restTemplate.getForEntity(url, User.class, params);
  Assertions.assertThat(user.getBody().getName()).isEqualTo("sasca");
}

@Test
@DisplayName("exchange 테스트")
public void exchangeToGetForObject() {
  String url = DEFAULT_URL + "/getForAny/{id}";
  ResponseEntity<User> user = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(User.class), User.class, params);
  Assertions.assertThat(user.getBody().getName()).isEqualTo("sasca");
}
  • getForEntity, getForObject 모두 테스트를 통과한다. RestTemplate은 uri를 String 타입과 URI 타입으로 받을 수 있다. 따라서, UriComponent 를 사용하여, url과 파라미터를 조합하여 사용할 수 있다.

 

RestTemplate JSON 요청 사용 예제

// POST 요청부
{
  "info" : {
    "country" : "korea",
    "name" : "sasca",
    "secretIdentity" : "unknown",
  },
  "reqData" : {
    "id" : "sasca37",
    "password" : "sasca37"
  }
}

// POST 응답부 
{
  "code" : {
    "result" : "200",
    "message" : "success"
  },
  "resData" : {
    "token" : "tokenURL!!!",
    "expirationTime" : "20230101"
  }
}
  • 다음과 같이 JSON 포맷의 사용자 정보가 POST 요청으로 오면 응답으로 token을 발급해주는 코드를 만들어보자.

 

RestTemplate 응답부

package sasca.springjsonapi.entity.entity;

import lombok.*;

@AllArgsConstructor
@Builder
@Getter
@NoArgsConstructor
public class RespEntity {
    private Code code;
    private ResData resData;

    @Builder
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Code {
        private String result;
        private String message;
    }

    @Builder
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class ResData {
        private String token;
        private String expirationTime;
    }
}
  • 응답 정보를 만들기 위한 오브젝트 생성

 

@RestController
public class JsonRespController {
  @PostMapping("/getToken")
  public ResponseEntity<Object> requestTest(HttpServletResponse response) throws JsonProcessingException {
    response.setContentType("application/json");
    response.setCharacterEncoding("utf-8");
    ObjectMapper objectMapper = new ObjectMapper();
    RespEntity resp = RespEntity.builder()
      .code(RespEntity.Code.builder().result("200").message("success").build())
      .resData(RespEntity.ResData.builder().token(TokenUtil.TOKEN_INFO).expirationTime("20230130000000").build())
      .build();
    String messageBody = objectMapper.writeValueAsString(resp);
    return ResponseEntity.status(HttpStatus.OK).body(messageBody);
  }
}
  • POST 요청을 받아 응답해주기 위한 컨트롤러 생성하자. 요청 메시지 바디에 대한 검증은 생략하였다.

 

image

  • 포스트맨으로 요청 시 정상 응답을 반환하는 것을 확인할 수 있다.

 

curl --connect-timeout 15 \
 -i \
 -H 'Content-Type: application/json' \
 -d '{ "info" : { "country" : "korea", "name" : "sasca", "secretIdentity" : "nknown"}, "reqData" : { "id" : "sasca37","password" : "sasca37"}}' \
 -X POST http://localhost:8080/getToken
  • 터미널에서 CURL 명령어를 통해서도 확인해볼 수 있다.

 

image

  • 정상적으로 요청이 된다면, 다음과 같은 결과를 받을 수 있다.

 

RestTemplate 요청부

package sasca.springjsonapi.util;

import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import sasca.springjsonapi.entity.entity.*;
import java.io.IOException;

public class TokenUtil {
  public static final String TARGET_URL = "http://localhost:8080/getToken";
  public static final String TOKEN_INFO = "tokenURL!!!";
  private final int connTimeout = 10 * 1000;
  private final int connReqTimeout = 10 * 1000;
  private final int readTimeout = 10 * 1000;
  private int maxConnTotal = Runtime.getRuntime().availableProcessors();
  private int maxConnPerRoute = Runtime.getRuntime().availableProcessors();

  public String getToken(ReqEntity req) throws IOException {

    // org.apache.http.client.HttpClient 생성 후 Connection Pool 설정 
    HttpClient client = HttpClientBuilder
      .create()
      .setMaxConnTotal(maxConnTotal) // 최대 오픈 커넥션 수 제한
      .setMaxConnPerRoute(maxConnPerRoute) // 호스트(IP, PORT 조합)에 대한 커넥션 수 제한
      .build();

    // ClientHttpRequestFactory의 구현체인 HttpComponentsClientHttpRequestFactory 생성
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    factory.setHttpClient(client); // HttpClient를 직접 설정하지 않는 경우 생략해도 된다. 
    factory.setConnectTimeout(connTimeout); // 커넥션 타임아웃 설정
    factory.setConnectionRequestTimeout(connReqTimeout); // 요청 커넥션 타임아웃 설정
    factory.setReadTimeout(readTimeout);

    RestTemplate restTemplate = new RestTemplate(factory);

    HttpHeaders header = new HttpHeaders();
        header.setContentType(new MediaType("application", "json", StandardCharsets.UTF_8));

    RespEntity token = restTemplate.postForObject(TARGET_URL, req, RespEntity.class);
    return token.getResData().getToken();
  }
}
  • Token 발급을 위한 클래스를 만들었다. RestTemplate은 커넥션 풀링을 지원하지 않는다. 따라서 아파치에서 제공하는 HttpClient에 커넥션 설정을 지정하여, ClientHttpRequsetFactory 를 만든 후, RestTemplate을 만들어주자.

 

package sasca;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import sasca.springjsonapi.entity.entity.User;
import sasca.springjsonapi.util.TokenUtil;

import java.io.IOException;

public class RestTemplateTest {

    @Test
    public void postForObject() throws IOException {
        Info info = Info
                .builder()
                .country("korea")
                .name("sasca")
                .secretIdentity("unknown")
                .build();
        ReqData reqData = ReqData
                .builder()
                .id("sasca37")
                .password("sasca37")
                .build();
        ReqEntity req = new ReqEntity(info, reqData);
        String token = new TokenUtil().getToken(req);
        Assertions.assertThat(token).isEqualTo(TokenUtil.TOKEN_INFO);
    }
}
  • 테스트 시 통과하는 것을 확인할 수 있다.

 

정리

  • RestTemplate은 요청 url로 String 또는 URI 형태를 받는다.
  • JSON 데이터 요청을 처리하기 위해 LinkedMultiValueMap 사용도 가능하다.
  • RestTemplateHttpClient를 추상화하고 있으며, Connection Pooling을 지원하지 않는다. 따라서 사용한 소켓은 TIME_WAIT 상태가 되며, 재사용하지 못하게 된다. apache에서 제공하는 HttpClient를 구현하여 커넥션 풀 설정을 하자.

 

REFERENCES


반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^