Spring

Spring Boot: HTTP 요청을 더 우아하게 처리하기 (RestTemplate 자세한 설명)

Written by 개발자서동우 · 37 sec read >
스프링 부트 RestTemplate의 자세한 설명

안녕하세요 Devloo 입니다 🙂 . 현대 웹 애플리케이션 개발에서는 클라이언트와 서버 간의 HTTP 통신이 필수적입니다. Spring Framework는 이러한 통신을 쉽게 처리할 수 있도록 다양한 도구를 제공합니다. 그 중 하나가 바로 RestTemplate입니다.

RestTemplate은 RESTful 웹 서비스와의 상호작용을 단순화하고, HTTP 요청과 응답을 손쉽게 처리할 수 있도록 도와줍니다. 이번 글에서는 RestTemplate을 활용하여 GET 및 POST 요청을 수행하는 방법과 다양한 HTTP 메서드를 지정하는 방법에 대해 알아보겠습니다.

RestTemplate에 대한 자세한 설명
사진: UnsplashLiana S

예전의 저는 Apache의 HttpClient를 사용하여 HTTP 요청을 개발했습니다. 그러나 이 방식은 코드가 복잡하고, 리소스 정리 등을 직접 관리해야 해서 중복된 코드가 많았습니다.

아래는 코드 예시입니다:

public String getJsonByParam(String url, List<BasicNameValuePair> formParams) throws IOException {
    // CloseableHttpClient와 CloseableHttpResponse는 try-with-resources로 관리
    try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
        HttpPost httpPost = new HttpPost(url);

        // HTTP 헤더 설정
        httpPost.setHeader("Accept", "application/json");
        httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded");
        httpPost.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0");

        // 요청 엔터티 설정
        UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(formParams, StandardCharsets.UTF_8);
        httpPost.setEntity(formEntity);

        // 요청 실행 및 응답 처리
        try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                String resultPre = EntityUtils.toString(entity, StandardCharsets.UTF_8);
                Document doc = Jsoup.parse(resultPre);
                return doc.text();
            }
        }
    }
    return null;
}

이 글에서는 Spring 생태계에서 GET 및 POST 요청을 처리하기 위해 RestTemplate을 구현하는 방법과 exchange 메서드를 사용하여 요청 유형을 지정하는 방법을 다룹니다. 또한 RestTemplate의 주요 메서드에 대한 분석도 포함되어 있습니다.

이 글을 다 읽어보시면 HTTP 요청을 보다 우아하고 효율적으로 처리할 수 있게 될 것입니다. 🙂

1. RestTemplate 간단 소개

RestTemplate은 Spring에서 동기식 클라이언트 측 통신을 위한 핵심 클래스입니다. 이 클래스는 HTTP 서비스와의 통신을 단순화하고 RESTful 원칙을 준수합니다. URL을 제공하고 결과를 추출하는 작업을 손쉽게 처리할 수 있습니다.

기본적으로 RestTemplate은 JDK의 HTTP 연결 도구를 사용하지만, setRequestFactory 속성을 통해 Apache HttpComponents, Netty, OkHttp 등의 다른 HTTP 클라이언트 라이브러리로 전환할 수 있습니다.

RestTemplate은 양식 데이터 제출을 크게 단순화하고 JSON 데이터를 자동으로 변환해줍니다.

그러나 이를 완전히 이해하고 활용하려면 HttpEntity(헤더와 본문)의 구조와 uriVariables의 차이를 이해해야 합니다. 이는 특히 POST 요청에서 두드러지며, 이 부분은 나중에 다룰 것입니다.

이 클래스의 주요 진입점은 여섯 가지 HTTP 메서드를 기반으로 합니다. 또한, exchangeexecute 메서드는 이 여섯 가지 HTTP 메서드와 상호 교환하여 사용할 수 있습니다.

RestTemplate의 설명 - 지원하는 HTTP 메서드
RestTemplate의 설명 – 지원하는 HTTP 메서드

내부적으로 RestTemplateHttpMessageConverter 인스턴스를 사용하여 HTTP 메시지를 POJO로 변환하거나 POJO를 HTTP 메시지로 변환합니다. 기본적으로 주요 MIME 유형에 대해 변환기가 등록되어 있지만, setMessageConverters 메서드를 통해 다른 변환기를 등록할 수도 있습니다.

(많은 메서드가 responseType 매개변수를 가지며, 이는 응답 본문과 매핑되는 객체를 전달할 수 있게 해주고, 내부적으로 HttpMessageConverter가 매핑을 수행합니다.)

HttpMessageConverterExtractor<T> responseExtractor =
                new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);

HttpMessageConverter.java 코드:

public interface HttpMessageConverter<T> {
    // 이 변환기가 주어진 클래스를 읽을 수 있는지 여부를 나타냅니다.
    boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);

    // 이 변환기가 주어진 클래스를 쓸 수 있는지 여부를 나타냅니다.
    boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

    // 지원되는 미디어 유형 목록을 반환합니다.
    List<MediaType> getSupportedMediaTypes();

    // 입력 메시지를 읽습니다.
    T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException;

    // 객체를 출력 메시지에 씁니다.
    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException;
}

내부적으로 RestTemplate은 HTTP 요청 생성을 위해 SimpleClientHttpRequestFactory를, 오류 처리를 위해 DefaultResponseErrorHandler를 기본적으로 사용합니다.

하지만 setRequestFactorysetErrorHandler 메서드를 사용하여 이를 재정의할 수 있습니다.

2. GET 요청 실습

2.1. getForObject() 메서드
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables){}
public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables)
public <T> T getForObject(URI url, Class<T> responseType)

getForObject()메서드는 HTTP 응답을 POJO로 변환해줍니다. getForEntity()와 달리, 응답을 직접 처리하지 않고 POJO만 반환하기 때문에 많은 응답 정보를 생략합니다.

2.1.1 POJO:
public class Notice {
    private int status;
    private Object msg;
    private List<DataBean> data;
}

public class DataBean {
  private int noticeId;
  private String noticeTitle;
  private Object noticeImg;
  private long noticeCreateTime;
  private long noticeUpdateTime;
  private String noticeContent;
}
2.1.2 파라미터가 없는 GET 요청
/**
 * 파라미터가 없는 GET 요청
 */
@Test
public void restTemplateGetTest(){
    RestTemplate restTemplate = new RestTemplate();
    Notice notice = restTemplate.getForObject("http://devloo.io/notice/list/1/5", Notice.class);
    System.out.println(notice);
}

콘솔 출력:

INFO 19076 --- [           main] c.w.s.c.w.c.HelloControllerTest          
: Started HelloControllerTest in 5.532 seconds (JVM running for 7.233)

Notice{status=200, msg=null, data=[DataBean{noticeId=21, noticeTitle='aaa', noticeImg=null, 
noticeCreateTime=1525292723000, noticeUpdateTime=1525292723000, noticeContent='<p>aaa</p>'}, 
DataBean{noticeId=20, noticeTitle='ahaha', noticeImg=null, noticeCreateTime=1525291492000, 
noticeUpdateTime=1525291492000, noticeContent='<p>ah.......'
2.1.3 파라미터가 있는 GET 요청 1
Notice notice = restTemplate.getForObject("http://devloo.io/notice/list/{1}/{2}", Notice.class, 1, 5);

플레이스홀더 {1}과 {2}가 사용된 것이 한눈에 들어옵니다.

2.1.4 파라미터가 있는 GET 요청 2
Map<String, String> map = new HashMap<>();
map.put("start", "1");
map.put("page", "5");
Notice notice = restTemplate.getForObject("http://fantj.top/notice/list/", Notice.class, map);

이 경우, 파라미터를 로드하기 위해 Map을 사용합니다. 기본적으로 URL을 PathVariable 형식으로 해석합니다.

2.2 getForEntity() 메서드
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables){}
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables){}
public <T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType){}

getForObject()와 달리, 이 메서드는 ResponseEntity 객체를 반환합니다.

POJO로 변환하려면 JSON 도구 클래스를 사용해야 하며, 이는 개인의 선호도에 따라 선택할 수 있습니다.

JSON 파싱에 익숙하지 않은 사람은 FastJson이나 Jackson 같은 도구를 사용할 수 있습니다. 이제 ResponseEntity에서 사용할 수 있는 메서드들을 살펴보겠습니다.

ResponseEntity, HttpStatus 및 BodyBuilder의 구조

ResponseEntity.java

public HttpStatus getStatusCode(){}
public int getStatusCodeValue(){}
public boolean equals(@Nullable Object other) {}
public String toString() {}
public static BodyBuilder status(HttpStatus status) {}
public static BodyBuilder ok() {}
public static <T> ResponseEntity<T> ok(T body) {}
public static BodyBuilder created(URI location) {}
...

HttpStatus.java

public enum HttpStatus {
public boolean is1xxInformational() {}
public boolean is2xxSuccessful() {}
public boolean is3xxRedirection() {}
public boolean is4xxClientError() {}
public boolean is5xxServerError() {}
public boolean isError() {}
}

BodyBuilder.java

public interface BodyBuilder extends HeadersBuilder<BodyBuilder> {
    // 본문의 길이를 Content-Length 헤더를 통해 설정합니다.
    BodyBuilder contentLength(long contentLength);
    // 본문의 MediaType을 설정합니다.
    BodyBuilder contentType(MediaType contentType);
    // 본문을 설정하고 ResponseEntity를 반환합니다.
    <T> ResponseEntity<T> body(@Nullable T body);
}

보시다시피, ResponseEntityHttpStatusBodyBuilder의 정보를 포함하여 원시 응답을 쉽게 처리할 수 있게 해줍니다.

예제:

@Test
public void rtGetEntity() {
    RestTemplate restTemplate = new RestTemplate();
    ResponseEntity<Notice> responseEntity = restTemplate.getForEntity("http://devloo.io/notice/list/1/5", Notice.class);

    // 상태 코드 확인 및 출력
    HttpStatus statusCode = responseEntity.getStatusCode();
    System.out.println("statusCode.is2xxSuccessful() : " + statusCode.is2xxSuccessful());

    // 응답 본문 확인 및 출력
    Notice notice = responseEntity.getBody();
    System.out.println("entity.getBody() : " + notice);

    // ResponseEntity.BodyBuilder를 사용하여 새로운 응답 빌드
    ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.status(statusCode)
                                                              .contentLength(100)
                                                              .body("Adding a statement here");

    ResponseEntity<Class<Notice>> builtResponse = responseBuilder.body(Notice.class);
    Class<Notice> builtResponseBody = builtResponse.getBody();
    System.out.println("builtResponse.toString() : " + builtResponse.toString());
}

출력:

statusCode.is2xxSuccessful() true
entity.getBody() Notice{status=200, msg=null, data=[DataBean{noticeId=21, noticeTitle='aaa', ...
body1.toString() <200 OK, class com.devloo.spring.cloud.weather.pojo.Notice, {Content-Length=[100]}>

물론, getHeaders() 같은 메서드도 사용할 수 있지만, 여기서는 예시로 다루지 않았습니다.

3. POST 요청 실습

POST 요청에는 postForObjectpostForEntity 메서드가 있습니다.

public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables)
         throws RestClientException {}
public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Map<String, ?> uriVariables)
         throws RestClientException {}
public <T> T postForObject(URI url, @Nullable Object request, Class<T> responseType) throws RestClientException {}
예제

이메일 인증 인터페이스를 테스트에 사용하겠습니다.

@Test
public void testPostRequest() {
    RestTemplate restTemplate = new RestTemplate();
    String url = "http://devloo.io/register/checkEmail";

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

    MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("email", "dannyseo@growmighty.co.kr");

    HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
    ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);

    System.out.println(response.getBody());
}

실행 결과:

{"status":500,"msg":"해당 이메일은 이미 등록되었습니다.","data":null}
MultiValueMap을 사용하는 이유는?

MultiValueMap은 각 키에 여러 값을 저장할 수 있는 Map의 하위 클래스입니다. 인터페이스는 다음과 같습니다:

public interface MultiValueMap<K, V> extends Map<K, List<V>> {...}

MultiValueMap을 사용하는 이유는 HttpEntityMultiValueMap 유형의 요청을 허용하기 때문입니다:

public HttpEntity(@Nullable T body, @Nullable MultiValueMap<String, String> headers){}

이 생성자에서 우리가 전달하는 맵은 요청 본문이고, headers는 요청 헤더입니다.

HttpEntity를 사용하는 이유는 restTemplate.postForEntity 메서드가 @Nullable Object 요청 유형을 허용하는 것처럼 보이지만, 실제로는 이 요청이 HttpEntity를 사용하여 구문 분석되기 때문입니다. 핵심 코드는 다음과 같습니다:

if (requestBody instanceof HttpEntity) {
    this.requestEntity = (HttpEntity<?>) requestBody;
} else if (requestBody != null) {
    this.requestEntity = new HttpEntity<>(requestBody);
} else {
    this.requestEntity = HttpEntity.EMPTY;
}

한 번은 Map을 사용해 파라미터를 전달해 보았지만, 컴파일 오류는 없었음에도 잘못된 URL 요청(400 오류)이 발생했습니다.

이 방법은 POST 요청의 요구 사항을 이미 충족하며, 필요에 따라 헤더에 쿠키를 설정할 수 있습니다.

다른 방법들도 비슷합니다.

4. HTTP 메서드를 지정하기 위해 exchange 사용

exchange() 메서드는 getForObject(), getForEntity(), postForObject(), postForEntity()와 달리 HTTP 메서드를 지정할 수 있습니다.

RestTemplate :: exchange() 메서드
RestTemplate :: exchange() 메서드

exchange 메서드는 모두 @Nullable HttpEntity requestEntity 매개변수를 가지고 있으며, 이는 요청 본문을 전달하기 위해 HttpEntity를 사용해야 함을 의미합니다. 앞서 언급했듯이, 더 나은 성능을 위해 HttpEntity를 사용하는 것이 좋습니다.

예제
@Test
public void testExchange() throws JSONException {
    RestTemplate restTemplate = new RestTemplate();
    String url = "http://xxx.top/notice/list";
    
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
    
    JSONObject requestJson = new JSONObject();
    requestJson.put("start", 1);
    requestJson.put("page", 5);
    
    HttpEntity<String> requestEntity = new HttpEntity<>(requestJson.toString(), headers);
    
    ResponseEntity<JSONObject> response = restTemplate.exchange(url, HttpMethod.GET, requestEntity, JSONObject.class);
    
    System.out.println(response.getBody());
}

이번에는 JSONObject를 사용해 데이터를 전달하고 반환했습니다. 유사한 방식으로 사용할 수 있는 다른 HttpMethod 메서드도 많습니다.

5. HTTP 메서드를 지정하기 위해 execute 사용

execute() 메서드는 exchange()와 마찬가지로 다양한 HttpMethod 유형을 지정할 수 있습니다. 그러나 차이점은 execute() 메서드가 응답 본문을 객체로 반환하며, ResponseEntity로 반환하지 않는다는 점입니다.

중요한 점은 execute() 메서드가 앞서 언급한 모든 메서드의 기본 메서드라는 것입니다. 아래는 예시입니다. :

@Override
@Nullable
public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Map<String, ?> uriVariables)
        throws RestClientException {

    RequestCallback requestCallback = httpEntityCallback(request, responseType);
    HttpMessageConverterExtractor<T> responseExtractor =
            new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
    return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables);
}

마무리

이번 시간에는 RestTemplate을 사용하여 GET 및 POST 요청을 수행하는 방법과 다양한 HTTP 메서드를 지정하는 방법을 익혔습니다. 또한, HttpEntityMultiValueMap의 사용 이유와 효과적인 요청을 위한 핵심 개념들을 이해하게 되었습니다.

RestTemplate을 활용하면 복잡한 HTTP 요청 처리를 단순화하고 코드의 가독성과 유지보수성을 크게 향상시킬 수 있습니다. 이제 RestTemplate을 사용하여 우아하고 효율적으로 HTTP 요청을 처리할 수 있는 능력을 갖추게 되었습니다. 실습을 통해 얻은 지식을 바탕으로 다양한 응용 프로그램에서 RestTemplate을 활용해 보시기 바랍니다.

끝까지 읽어주셔서 감사합니다 🙂 궁금하신 점은 댓글로 남겨주세요.

Written by 개발자서동우
안녕하세요! 저는 기술 분야에서 활동 중인 개발자 서동우입니다. 명품 플랫폼 (주)트렌비의 창업 멤버이자 CTO로 활동했으며, AI 기술회사 (주)헤드리스의 공동 창업자이자 CTO로서 역할을 수행했습니다. 다양한 스타트업에서 일하며 회사의 성장과 더불어 비즈니스 상황에 맞는 기술 선택, 개발팀 구성 및 문화 정착에 깊은 경험을 쌓았습니다. 개발 관련 고민은 언제든지 편하게 연락주세요 :) https://linktr.ee/dannyseo Profile

Leave a Reply

Your email address will not be published. Required fields are marked *