2/16(월) RestTemplate

dev_joo·2026년 2월 16일

오늘의 갈 길이 멀다!
일단 강의를 듣는다😭

Spring 숙련주차

API 사용

회원가입을 진행할 때 사용자가 자신의 주소를 검색해서 전달하는 등 라이브러리 사용만으로 구현하기 힘든 부분을 미리 만들어진 주소 검색 API를 활용하면 해당 기능을 간편하게 구현할 수 있다.
그러니까 이해하기 쉽게 비유하자면 브라우저에게 요청을 받는 서버도 응답을 위해 필요한 일부 정보를 짬 때릴 수 있다!
이러한 다른 API서버로의 요청을 간편하게 요청 할 수 있도록 Spring은 RestTemplate 기능을 제공하고 있다.

RestTemplate

프로젝트 생성

API를 제공하는 쪽(Server)과, API를 제공받는 쪽(Client) 양 쪽의 서버 프로젝트를 생성한다.
Dependency :
SpringBootWeb, Lombok

  • spring-resttemplate-server
  • spring-resttemplate-client
    서버 간 통신을 위해 양쪽의 포트를 다르게 설정한다.
# application.properties
server.port=7070

Controller (Client)

service에서 Item을 받아 dto로 반환한다.

package com.sparta.springresttemplateclient.controller;

import com.sparta.springresttemplateclient.dto.ItemDto;
import com.sparta.springresttemplateclient.service.RestTemplateService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api/client")
public class RestTemplateController {

    private final RestTemplateService restTemplateService;

    public RestTemplateController(RestTemplateService restTemplateService) {
        this.restTemplateService = restTemplateService;
    }

    @GetMapping("/get-call-obj")
    public ItemDto getCallObject(String query) {
        return restTemplateService.getCallObject(query);
    }

    @GetMapping("/get-call-list")
    public List<ItemDto> getCallList() {
        return restTemplateService.getCallList();
    }

    @GetMapping("/post-call")
    public ItemDto postCall(String query) {
        return restTemplateService.postCall(query);
    }

    @GetMapping("/exchange-call")
    public List<ItemDto> exchangeCall(@RequestHeader("Authorization") String token) {
        return restTemplateService.exchangeCall(token);
    }
}

Contoller (server)

Client 측 restTemplateService에서 요청하는 대로 itemService에서 Item을 전달한다.

package com.sparta.springresttemplateserver.controller;

import com.sparta.springresttemplateserver.dto.ItemResponseDto;
import com.sparta.springresttemplateserver.dto.UserRequestDto;
import com.sparta.springresttemplateserver.entity.Item;
import com.sparta.springresttemplateserver.service.ItemService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/server")
public class ItemController {

    private final ItemService itemService;

    public ItemController(ItemService itemService) {
        this.itemService = itemService;
    }

    @GetMapping("/get-call-obj")
    public Item getCallObject(@RequestParam String query) {
        return itemService.getCallObject(query);
    }

    @GetMapping("/get-call-list")
    public ItemResponseDto getCallList() {
        return itemService.getCallList();
    }

    @PostMapping("/post-call/{query}")
    public Item postCall(@PathVariable String query, @RequestBody UserRequestDto requestDto) {
        return itemService.postCall(query, requestDto);
    }

    @PostMapping("/exchange-call")
    public ItemResponseDto exchangeCall(@RequestHeader("X-Authorization") String token, @RequestBody UserRequestDto requestDto) {
        return itemService.exchangeCall(token, requestDto);
    }
}

Client의 Get요청 (getForEntity)

강의에서는 RestTemplateBuilder를 통해서 RestTemplate 객체를 생성해준다.

@Service
class RestTemplateService{
	private final RestTemplate restTemplate;
    
    public RestTemplateSerive(RestTemplateBuilder builder) {
    	this.restTemplate = builder.build();
    }
}

WebClient

WebFlux 기반 신규 개발에는 비동기를 지원하는RestTemplate 대신WebClient 사용을 권장한다.

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient(WebClient.Builder builder) {
        return builder.build();
    }
    @Service
public class ApiService {

    private final WebClient webClient;

    public ApiService(WebClient webClient) {
        this.webClient = webClient;
    }

    public Mono<String> getSomething() {
        return webClient
                .get()
                .uri("https://api.example.com/data")
                .retrieve()
                .bodyToMono(String.class);
                //.block();   // 동기 호출, String 반환
    }
}

}

RestTemplate - 단일 객체 요청

public ItemDto getCallObject(String query) {
    // 요청 URL 만들기
    URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:7070")
            .path("/api/server/get-call-obj")
            .queryParam("query", query)
            .encode()
            .build()
            .toUri();
    log.info("uri = " + uri);

    ResponseEntity<ItemDto> responseEntity = restTemplate.getForEntity(uri, ItemDto.class);

    log.info("statusCode = " + responseEntity.getStatusCode());

    return responseEntity.getBody();
}
    public RestTemplateService() {
        this.restTemplate = new RestTemplate();
    }

restTemplate.getForEntity(URI, CLASS) 메서드를 통해 해당 URI로 GET요청을 받아온다.
RestTemplate을 사용하면 요청의 결과값에 대해서 직접 JSON 직렬화/역직렬화를 자동으로 처리 해준다. (엄밀히 말하면 JSON 변환은 RestTemplate 내부의 MessageConverter가 수행하고,
Spring Boot가 그 Converter와 ObjectMapper를 자동으로 등록해주는 구조이다.)
getForEntity()는 요청으로 받아온 직렬화된 객체 정보를 두 번째 인수에 지정한 class(DTO)형태로 역직렬화 해 ResponseEntity<T>형태로 저장한다.

ResponseEntity<ItemDto> responseEntity = restTemplate.getForEntity(uri, ItemDto.class);

❓Service 단에서 ResponseEntity를 사용하는것이 아키텍처 설계를 깨는것이 아닌가 의문이 들었다. 이 과정에서 HTTP응답과 관련없이 객체만 받아오는 getForObject()의 존재도 알게 되었다.
하지만 이는 외부 서버와 통신하기 위해 내부에서 잠깐 사용하는 도구일 뿐,
메서드가 최종적으로 순수 객체인 DTO만 반환하게 해서
HTTP 개념이 밖으로 드러나지 않으므로 레이어드 아키텍처를 위반하지 않는다.

Spring의 UriComponentsBuilder를 사용하면 URI를 손쉽게 만들 수 있다.
UriConponentsBuilder.queryParam() 메서드로 query-string 방식 요청경로를 만들 수 있다.

// 요청 URL 만들기
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:7070")
   .path("/api/server/get-call-obj")
   .queryParam("query", query)
   .encode()
   .build()
   .toUri();

RestTemplate - 객체 리스트 요청

강의에서는 엔티티 목록 정보를 ResponseEntity<String>으로 받고, 나중에 JSON을 DTO로 변환한다.

❓“그냥 ResponseEntity<List<ItemDto>>로 못 쓰나?"
RestTemplate.getForEntity(..., List<ItemDto>.class)는 런타임에 타입을 무시하는 Java 타입 소거(Type Erasure) 때문에 Jackson이 정확한 내부 타입(ItemDto)을 몰라서 List<LinkedHashMap>으로만 읽음
→ String으로 받아서 수동 변환하거나 Jackson에서 제네릭 타입 정보를 런타임에 전달해주는 TypeReference, 또는 exchange + ParameterizedTypeReference를 써야 한다.

JSON 처리를 도와주는 라이브러리를 추가한다.
JSONObjectJSONArray 클래스를 사용할 수 있다.

// json Dependency
implementation 'org.json:json:20230227'

JSONObjectget~()메서드를 통해 JSON의 key-value 쌍에서 값을 가져올 수 있다.

@Getter
@NoArgsConstructor
public class ItemDto {
   private String title;
   private int price;

   public ItemDto(JSONObject item) {
       this.title = item.getString("title");
       this.price = item.getInt("price");
   }
}

API서버에서 여러 개의 객체를 포함한 하나의 배열이 루트 객체에 담긴 형태의 JSON데이터가 String으로 전달된다.

"
   {
  "items":[
          {"title":"Mac","price":3888000},
          {"title":"iPad","price":1230000},
          {"title":"iPhone","price":1550000},
          {"title":"Watch","price":450000},
          {"title":"AirPods","price":350000}
      ]
  }
"

API클라이언트에서 이를 받아 ItemDto 리스트 형태로 변환한다.

public List<ItemDto> getCallList() {
   URI uri = UriComponentsBuilder
           .fromUriString("http://localhost:7070")
           .path("/api/server/get-call-list")
           .encode()
           .build()
           .toUri();
   log.info("uri = " + uri);

   ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);

   log.info("statusCode = " + responseEntity.getStatusCode());
   log.info("Body = " + responseEntity.getBody());

   return fromJSONtoItems(responseEntity.getBody());
}

JSON배열 String 처리

public List<ItemDto> fromJSONtoItems(String responseBody) {
    JSONObject jsonObject = new JSONObject(responseBody);
    JSONArray items  = jsonObject.getJSONArray("items");
    List<ItemDto> itemDtoList = new ArrayList<>();

    for (Object item : items) {
        ItemDto itemDto = new ItemDto((JSONObject) item);
        itemDtoList.add(itemDto);
    }

    return itemDtoList;
}

Client의 POST요청 (PostForEntity)

public ItemDto postCall(String query) {
   // 요청 URL 만들기
   URI uri = UriComponentsBuilder
           .fromUriString("http://localhost:7070")
           .path("/api/server/post-call/{query}")
           .encode()
           .build()
           .expand(query)
           .toUri();
   log.info("uri = " + uri);

   User user = new User("Robbie", "1234");

   ResponseEntity<ItemDto> responseEntity = restTemplate.postForEntity(uri, user, ItemDto.class);

   log.info("statusCode = " + responseEntity.getStatusCode());

   return responseEntity.getBody();
}

UriComponentsBuilderpath()메서드와 expand()메서드로 PathVariable방식의 요청 경로를 만들 수 있다.

// 요청 URL 만들기
URI uri = UriComponentsBuilder
	.fromUriString("http://localhost:7070")
    .path("/api/server/post-call/{query}")
    .encode()
    .build()
    .expand(query)
    .toUri();

path()에 PathVariable 위치를 {}로 지정한다.

expand(query){query}에 값을 치환한다.
이 방식은 순서 기반 매핑이다.

이름 기반으로 변수를 매핑할 땐, 다음처럼 Map을 사용한다.

.path("/api/server/post-call/{query}")
.buildAndExpand(Map.of("query", query)) 
// Map의 key와 {query} 이름이 반드시 일치해야 한다.
// buildAndExpand()는 build() + expand()를 한 번에 수행하는 메서드

RestTemplate - post 요청

ResponseEntity<ItemDto> responseEntity = restTemplate.postForEntity(uri, user, ItemDto.class);

restTemplate.postForEntity(URI, BODY, CLASS) 메서드를 통해 해당 URI로 POST요청을 하고 결과를 받아온다. 두 번째 파라미터로 요청의 body로 보내줄 객체를 받는다.

getForEntity와 마찬가지로 객체와 JSON관련 처리를 알아서 해 준다.

요청 흐름

  1. API Client가 다음과 같이 요청을 받는다면,
    GET {{base_url}}/api/client/post-call?query=Mac
@GetMapping("/post-call")
    public ItemDto postCall(String query) {
        return restTemplateService.postCall(query);
}
  1. API Client는postForEntity()
    쿼리 스트링으로 받은 정보를 토대로 PathVariable 방식으로 API Sever에 POST요청을 보낸다.
    POST {{base_url}}/api/server/post-call/Mac
    Body:
{
    "username":"Robbie",
    "password":"1234"
}
  1. 요청을 받은 API Server는
    @RequestBody설정에 따라 Body를 UserRequestDto 객체로 변환한다.
@PostMapping("/post-call/{query}")
public Item postCall(@PathVariable String query, @RequestBody UserRequestDto requestDto) {
	return itemService.postCall(query, requestDto);
}

요청 헤더에 정보 입력하기

JWT와 같이 요청때마다 데이터를 전달해야하는 경우 헤더에 정보를 포함한 요청을 보낼 수 있다.

public List<ItemDto> exchangeCall(String token) {
        // 요청 URL 만들기
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:7070")
                .path("/api/server/exchange-call")
                .encode()
                .build()
                .toUri();
        log.info("uri = " + uri);

        User user = new User("Robbie", "1234");

        RequestEntity<User> requestEntity = RequestEntity
                .post(uri)
                .header("X-Authorization", token)
                // 비표준 확장 헤더에 X- 접두사를 붙이는 관례가 있었다.
                .body(user);

        ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);

        return fromJSONtoItems(responseEntity.getBody());
    }

RestTemplate - exchange

RestTemplate.exchange(URI,METHOD,ENTITY,CLASS)메서드를 통해 해당 URI로 해당HTTP요청을 하고 결과를 받아온다. 세 번째 파라미터로 POST요청의 body로 보낼 객체를 받는다. GET요청의 경우 null로 값을 비워서 보내면 된다.

exchange(URI url,
         HttpMethod method,
         HttpEntity<?> requestEntity,
         Class<T> responseType)
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer token");
// HttpEntity: body + header
HttpEntity<User> entity = new HttpEntity<>(user, headers);

ResponseEntity<String> response =
    restTemplate.exchange(uri, HttpMethod.POST, entity, String.class);

오버로딩된 exchange 메서드의 첫 번째 파라미터에 RequestEntity 객체를 만들어 전달해주면 uri, HTTPmethod, header, body의 정보를 한번에 전달할 수 있다.

// RequestEntity: uri+ method + header + body
RequestEntity<User> requestEntity = RequestEntity
                .post(uri)
                .header("X-Authorization", token)
                .body(user);

ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);
profile
풀스택 연습생. 끈기있는 삽질로 무대에서 화려하게 데뷔할 예정 ❤️🔥

0개의 댓글