상세 컨텐츠

본문 제목

[스프링 2팀] 서버 간 통신

24-25/Spring 2

by dalpaeng4 2025. 1. 24. 10:00

본문

728x90

 

 

 RestTemplate 

스프링에서 HTTP 통신 기능을 손쉽게 사용하도록 설계된 템플릿이다.

HTTP 서버와의 통신을 단순화한 템플릿이며, RESTful 원칙을 따르는 서비스를 편리하게 만들 수 있다.

 

동기 방식으로 처리디며, 비동기 방식으로 사용하고 싶을 경우 AsyncRestTemplate을 사용하면 된다.

그러나 지원 중단된 상태라 WebClient 방식도 함께 알아두어야 한다.

 

  • HTTP 프로토콜의 메서드에 맞는 여러 메서드를 지원한다.
  • RESTful 형식을 갖춘 템플릿이다.
  • HTTP 요청 후 JSON, XML, 문자열 등의 다양한 형식으로 응답을 받을 수 있다.
  • 블로킹 I/O 기반의 동기 방식을 사용한다.
  • 다른 API를 호출할 때 헤더에 다양한 값을 설정할 수 있다.

 

RestTemplate의 동작 원리

 

 

 

애플리케이션

애플리케이션은 우리가 직접 작성하는 애플리케이션 코드 부분을 의미한다.

애플리케이션에서는 RestTemplate을 선언하고 URI와 HTTP 메서드, Body 등읈 설정한다.

 

외부 API로 요청

외부 API로 요청을 보내게 되면 RestTemplate에서 HttpMessageConverter를 통해 RequestEntity를 요청 메세지로 변환한다.

RestTemplaet에서는 변환된 요청 메세지를 ClientHttpRequestFactory를 통해 ClientRequest로 가져온 후 외부 API로 요청을 보낸다.

 

요청에 대한 응답

외부에서 요청에 대한 응답을 받으면 RestTemplate은 ResponseErrorHandler로 오류를 확인하고, 오류가 있다면 ClientHttpResponse에서 응답 데이터를 처리한다.

받은 응답 데이터가 정상이라면 다시 한번 HttpMessageConverter를 거져 차바 객체러 변환하여 애플리케이션으로 반환한다.

 

 

RestTemplate 사용하기

@RestController
@RequestMapping("/api/v1/crud-api")
public class CrudController {

	// GET 요청이 오는 경우 
    
    // 파라미터가 없는 경우 
    @GetMapping
    public String getName(){
        return "Flature";
    }

	// variable을 사용하는 경우 
    @GetMapping(value = "/{variable}")
    public String getVariable(@PathVariable String variable){
        return variable;
    }

	// RequestParameter을 사용하는 경우 
    @GetMapping("/param")
    public String getNameWithParam(@RequestParam String name){
        return "Hello" + name + "!";
    }
    
    // POST 요청이 오는 경우 
    @PostMapping
    public ResponseEntity<MemberDto> getMember(
            @RequestBody MemberDto request,
            @RequestParam String name,
            @RequestParam String email,
            @RequestParam String organization
    ) {
        System.out.println(request.getName());
        System.out.println(request.getEmail());
        System.out.println(request.getOrganization());

        MemberDto memberDto = new MemberDto();
        memberDto.setName(name);
        memberDto.setEmail(email);
        memberDto.setOrganization(organization);

        return ResponseEntity.status(HttpStatus.OK).body(memberDto);
    }

	// 임의의 HTTP 헤더를 받는 경우 
    @PostMapping(value = "/add-header")
    public ResponseEntity<MemberDto> addHeader(@RequestHeader("my-header") String header, @RequestBody MemberDto memberDto){
        System.out.println(header);

        return ResponseEntity.status(HttpStatus.OK).body(memberDto);
    }
}
public class MemberDto {
    private String name;
    private String email;
    private String organization;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getOrganization() {
        return organization;
    }

    public void setOrganization(String organization) {
        this.organization = organization;
    }

    @Override
    public String toString() {
        return "MemberDto{" +
                "name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", orgaization='" + organization + '\'' +
                '}';
    }
}

 

 

 

RestTemplate 구현하기

일반적으로 별도의 유틸리티 클래스로 생성하거나, 서비스 또는 비즈니스 계층에 구현된다.

RestTemplate은 spring-boot-starter-web 모듈에 포함되어있는 기능이기 때문에 의존성을 추가할 필요는 없다.

 

GET 형식의 RestTemplate 작성

@Service
public class RestTemplateService {

    public String getName(){
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/api/v1/crud-api")
                .encode() 
                .build() 
                .toUri(); 
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class); 


        return responseEntity.getBody();
    }

    public String getNameWithPathVariable(){
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/api/v1/crud-api/{name}") 
                .encode()
                .build()
                .expand("Flature") 
                .toUri();

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

        return responseEntity.getBody();
    }

    public String getNameWithParameter(){
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/api/v1/crud-api/param")
                .queryParam("name", "Flature") 
                .encode()
                .build()
                .toUri();

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

        return responseEntity.getBody();
    }
}

 

 

UriComponentsBuilder

스프링 프레임워크에서 제공하는 클래스로, 여러 파라미터를 연결해서 URI 형식으로 만드는 기능을 수행한다. 

 

    public String getName(){
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/api/v1/crud-api")
                .encode() 
                .build() 
                .toUri(); 
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class); 


        return responseEntity.getBody();
    }

 

PathVariable이나 파라미터를 사용하지 않는 호출 방식이다.

UriComponentsBuilder는 빌더 형식으로 객체를 생성한다.

fromUriString에서는 호출부의 URL을 입력하고, 이어서 path() 메서드에 세부 경로를 입력한다.

encode()에 아무런 인자를 전달하지 않으면 UTF-9로 코드가 실행된다.

마지막으로 build() 메서드를 통해 빌더 생성을 종려하면 UriComments 타입이 리턴되는데, toUri() 메서드를 통해 URI 타입으로 리턴받는다.

 

-> 이렇게 생성된 uri는 외부 API를 요청하는데 사용되며, getForEntity()에 파라미터로 전달된다.

 

 

    public String getNameWithPathVariable(){
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/api/v1/crud-api/{name}") 
                .encode()
                .build()
                .expand("Flature") 
                .toUri();

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

        return responseEntity.getBody();
    }

 

path 메서드에 중괄호를 사용해 개발 단계에서 쉽게 이해할 수 있는 변수명을 입력하고, extand() 메서드에는 순서대로 값을 입력한다.

               .fromUriString("http://localhost:9090")
                .path("/api/v1/crud-api/param")
                .queryParam("name", "Flature")

 

값을 여러개 나열해야 하는 경우네는 ,로 구분하여 나열한다.

 

                .path("/api/v1/crud-api")
                .queryParam("name", "Flature")
                .queryParam("email", "flature@wikibooks.co.kr")
                .queryParam("organization", "Wikibooks")

 

queryParam 메서드를 통해 (키, 값) 형식으로 파라미터를 추가할 수도 있다.

 

 

POST 형식의 RestTemplate 작성

    public ResponseEntity<MemberDto> postWithParamAndBody(){
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/api/v1/crud-api")
                .queryParam("name", "Flature")
                .queryParam("email", "flature@wikibooks.co.kr")
                .queryParam("organization", "Wikibooks")
                .encode()
                .build()
                .toUri();

        MemberDto memberDto = new MemberDto();
        memberDto.setName("flature!!");
        memberDto.setEmail("flature@gmail.com");
        memberDto.setOrganization("Around Hub Studio");

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<MemberDto> responseEntity = restTemplate.postForEntity(uri, memberDto, MemberDto.class); // 파라미터로 데이터 객처 넣기

        return responseEntity;
    }

    public ResponseEntity<MemberDto> postWithHeader(){
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:9090")
                .path("/api/v1/crud-api/add-header")
                .encode()
                .build()
                .toUri();

        MemberDto memberDto = new MemberDto();
        memberDto.setName("flature!!");
        memberDto.setEmail("flature@gmail.com");
        memberDto.setOrganization("Around Hub Studio");

        RequestEntity<MemberDto> requestEntity = RequestEntity
                .post(uri) 
                .header("my-header", "Wikibooks API") 
                .body(memberDto);

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<MemberDto> responseEntity = restTemplate.exchange(requestEntity, MemberDto.class); 

        return responseEntity;
    }

 

 

        MemberDto memberDto = new MemberDto();
        memberDto.setName("flature!!");
        memberDto.setEmail("flature@gmail.com");
        memberDto.setOrganization("Around Hub Studio");

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<MemberDto> responseEntity = restTemplate.postForEntity(uri, memberDto, MemberDto.class); // 파라미터로 데이터 객처 넣기

 

RequestBody에 값을 담기 위해서 데이터 객체를 생성한 후 postForEntity() 메서드를 사용하여 데이터 객체를 넣는다.

postForEntity() 메서드로 서버 프로젝트의 API를 호출하면 서버 프로젝트의 콘솔 로그에는 RequestBody 값이 출력되며, 파라미터 값은 결과값으로 리턴된다.

 

        RequestEntity<MemberDto> requestEntity = RequestEntity
                .post(uri) 
                .header("my-header", "Wikibooks API") 
                .body(memberDto);

 

RequestEntity를 생성하고, header() 메서드에서 헤더의 키 이름과 값을 설정하는 방법이다.

 

 

Swagger, Controller 작성

@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.springboot.rest"))
                . paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("Spring Boot Open API Test with Swagger")
                .description("설명 부분")
                .version("1.0.0")
                .build();
    }
}

 

@RestController
@RequestMapping("/rest-template")
public class RestTemplateController {

    private final RestTemplateService restTemplateService;

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

    @GetMapping
    public String getName(){
        return restTemplateService.getName();
    }

    @GetMapping("/path-variable")
    public String getNameWithPathVariable(){
        return restTemplateService.getNameWithPathVariable();
    }

    @GetMapping("/parameter")
    public String getNameWithParameter(){
        return restTemplateService.getNameWithParameter();
    }

    @GetMapping
    public ResponseEntity<MemberDto> postDto(){
        return restTemplateService.postWithParamAndBody();
    }

    @GetMapping("/header")
    public ResponseEntity<MemberDto> postWithHeader(){
        return restTemplateService.postWithHeader();
    }
}

 

 

flature!!
flature@gamil.com
Around Hub Studio

 

위 출력 결과는 서버 프로젝트가 파라미터의 값과 Body 값을 정상적으로 전달받았다는 것을 의미한다.

 

 

 

RestTemplate 커스텀 설정

RestTemplate은 HTTPClient를 추상화한다. 

그러나 RestTemplate은 커넥션 풀을 지원하지 않아 매번 호출을 할 때마다 포트를 열어 커넥션을 생성하게 되는데, TIME_WAIT 상태가 된 소켓을 다시 사용하려고 접근한다며 재사용하지 못하게 된다.

따라서 커넥션 풀 기능을 활성화하여 재사용할 수 있게 하는 것이 좋다.

 

HttpClient를 사용하기 위하여 의존성을 추가한다.

<dependency>
	<groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>

 

 

커스텀 RestTemplate 객체 생성 메서드

public RestTemplate restTemplate(){
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();

        HttpClient client = HttpClientBuilder.create()
                .setMaxConnTotal(500)
                .setMaxConnPerRoute(500)
                .build();

        CloseableHttpClient httpClient = HttpClients.custom()
                .setMaxConnTotal(500)
                .setMaxConnPerRoute(500)
                .build();

        factory.setHttpClient(httpClient);
        factory.setConnectTimeout(2000);
        factory.setReadTimeout(5000);

        RestTemplate restTemplate = new RestTemplate(factory);

        return restTemplate;
    }

 

RestTemplate의 생성자에 ClientHttpRequestFactory를 매개변수로 받는 생성자가 존재한다.

ClientHttpRequestFactory는 함수형 인터페이스로, 대표적인 구현체로서 SimpleClientHttpRequestFactory와 HttpComponentsClientHttpRequestFactory가 있다.

 

별도의 구현체를 설정해서 전달하지 않으면 HttpAccessor에 구현돼 있는 내용에 의해 SimpleClientHttpRequestFactory를 사용하게 된다. 

 

별도의 HttpCompoentsClientHttpRequestsFactory 객체를 생성해서 CllientHttpRequestFactory를 사용하면 RestTemplaqte의 Timeout을 설정할 수 있다.

-> 이후 HttpCompoentsClientHttpRequestsFactory는 커넥션 풀을 설정하기 위해 HttpClient를 HttpCompoentsClientHttpRequestsFactory 에 설정할 수 있다.

 

HttpClient를 생성하는 2가지 방법

  • HttpClientBuilder.create() 메서드 사용
  • HttpClient.custom() 메서드 사용 

생성한 HttpClient는 factory의 setHttpClient() 메서드를 통해 인자로 전달하여 설정할 수 있다.

이렇게 서렂ㅇ된  factory 객체를 RestTemplate을 초기화하는 과정에 인자로 전달하면 된다.

 

 

 

 WebClient 

Spring Webflux는 HTTP 요청을 수행하는 클라이언트로 WebCllient를 제공한다.

리액터 (Reactor)를 기반으로 동작하는 API이며, 리액터 기반이므로 스레드와 동시성 문제를 벗어나 비동기형식으로 사용할 수 있다.

  • 논블로킹 IO를 지원한다.
  • 리액티브 스트림의 백 프레셔를 지원한다.
  • 적은 하드웨어 리소스로 동시성을 지원한다.
  • 함수형 API를 지원한다.
  • 동기, 비동기 상호작용을 지원한다.
  • 스트리밍을 지원한다.

 

WebFlux에 대한 의존성을 추가한다.

 <dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

 

 

WebClient 구현

webClient를 생성하는 2가지 방법 

    • builder을 이요한 생성
        WebClient webClient = WebClient.builder()
            .baseUrl("http://localhost:9090") 
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 
            .build();
  • reate() 메서드를 이용한 생성
        WebClient webClient = WebClient.create("http://localhost:9090");

 

 

GET 요청

@Service
public class WebClientService {

    public String getName() {
        WebClient webClient = WebClient.builder()
            .baseUrl("http://localhost:9090") 
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 
            .build();

        return webClient.get() 
            .uri("/api/v1/crud-api") 
            .retrieve() 
            .bodyToMono(String.class) 
            .block(); 
    }

    public String getNameWithPathVariable() {
        WebClient webClient = WebClient.create("http://localhost:9090");

        ResponseEntity<String> responseEntity = webClient.get()
            .uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api/{name}")
                .build("Flature"))  
            .retrieve().toEntity(String.class).block(); 

        ResponseEntity<String> responseEntity1 = webClient.get()
            .uri("/api/v1/crud-api/{name}", "Flature")
            .retrieve()
            .toEntity(String.class) 
            .block();

        return responseEntity.getBody();
    }

    public String getNameWithParameter() {
        WebClient webClient = WebClient.create("http://localhost:9090");

        return webClient.get().uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api")
                .queryParam("name", "Flature") 
                .build())
            .exchangeToMono(clientResponse -> {
                if (clientResponse.statusCode().equals(HttpStatus.OK)) {
                    return clientResponse.bodyToMono(String.class);
                } else {
                    return clientResponse.createException().flatMap(Mono::error);
                }
            })
            .block();
    }
}

 

WebClient는 객체를 생성한 후 요청을 전달하는 방식으로 동작한다.

builder)(를 통해 baseUrl() 메서드에서 기본 URL을 설정하고, defaultHeader() 메서드로 헤더의 값을 설정한다.

일반적으로 WebClient 객체를 이용할 때는 WebClient 객체를 생성한 후 재사용하는 방식으로 구현하는 것이 좋다.

WebClient webClient = WebClilent.create("http://localhost:9090");
WebClient clone = webClient.mutrate().build();

 

일단 빌드된 WebClient는 변경할 수 없고, 위와 같이 복사해서 사용한다.

 

        return webClient.get() 
            .uri("/api/v1/crud-api") 
            .retrieve() 
            .bodyToMono(String.class) 
            .block();

 

HTTP 메서드를 get(), post(), delete(), 등의 네이밍이 정확한 메서드로 설정할 수 있으며, URI를 확장하는 방법으로 uri() 메서드를 사용할 수 있다.

retrieve() 메서드는 요청에 대한 응답을 받았을 때, 그 값을 추출하는 방법 중 하나이다.

 

Webclient는 기본적으로 논블로킹 방식으로 동작하기 때문에 기존에 사용하던 코드의 구조를 블로킹 구조로 바꿔주어야 학 때문에 block() 메서드를 추가해 블로킹 형식으로 동작하게 설정한다.

 

 

POST 요청

    public ResponseEntity<MemberDto> postWithParamAndBody() {
        WebClient webClient = WebClient.builder()
            .baseUrl("http://localhost:9090")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();

        MemberDto memberDTO = new MemberDto();
        memberDTO.setName("flature!!");
        memberDTO.setEmail("flature@gmail.com");
        memberDTO.setOrganization("Around Hub Studio");

        return webClient.post().uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api")
                .queryParam("name", "Flature")
                .queryParam("email", "flature@wikibooks.co.kr")
                .queryParam("organization", "Wikibooks")
                .build())
            .bodyValue(memberDTO) 
            .retrieve()
            .toEntity(MemberDto.class)
            .block();
    }

    public ResponseEntity<MemberDto> postWithHeader() {
        WebClient webClient = WebClient.builder()
            .baseUrl("http://localhost:9090")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();

        MemberDto memberDTO = new MemberDto();
        memberDTO.setName("flature!!");
        memberDTO.setEmail("flature@gmail.com");
        memberDTO.setOrganization("Around Hub Studio");

        return webClient
            .post()
            .uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api/add-header")
                .build())
            .bodyValue(memberDTO)
            .header("my-header", "Wikibooks API") 
            .retrieve()
            .toEntity(MemberDto.class)
            .block();
    }

    public void cloneWebClient() {
        WebClient webClient = WebClient.create("http://localhost:9090");

        WebClient clone = webClient.mutate().build();
    }

 

WebClient를 생성하고 사용하는 방법은 GET 요청을 만드는 방법과 다르지 않지만, HTTP 바디 값과 헤더를 추가하는 방법이 다르다.

 

 

QUIZ

1. ( RestTemplate )는 스프링에서 HTTP 통신 기능을 손쉽게 사용하도록 설계된 템플릿이고, HTTP 서버와의 통신을 단순화한 템플릿이다.

2. ( UriComponentsBuilder )는 스프링 프레임워크에서 제공하는 클래스로, 여러 파라미터를 연결해서 URI 형식으로 만드는 기능을 수행한다. 

3. UriComponentsBuilder는 ( 빌더 형식 )으로 객체를 생성한다. 이때 build() 메서드를 통해 빌드 메서드를 종료하면 UriComments 타입이 리턴되는데,( toUri() ) 메서드를 통해 URI 타입으로 리턴받는다.

4. RestTemplate은 ( 커넥션 풀 )을 지원하지 않아 매번 호출을 할 때마다 포트를 열어 커넥션을 생성한다. -> 이 기능을 활성화하여 재사용할 수 있게 하는 것이 좋다.

5. HttpClient를 생성하는 2가지 방법으로 HttpClientBuilder.create() 메서드를 사용하는 방법과 HttpClient.( custom() )메서드를 사용하는 방법이 있다.

6. ( Spring Webflux )는 HTTP 요청을 수행하는 클라이언트로 WebCllient를 제공한다.

7. WebClient는 기존에 사용하던 코드의 구조를 블로킹 구조로 바꿔주어야 하기 때문에 ( block() ) 메서드를 추가해 블로킹 형식으로 동작하게 설정한다.

 

코드 QUIZ

    public String getName(){
        URI uri = UriComponentsBuilder
                .// 코드 작성 
                .// 코드 작성
                .encode() 
                .build() 
                .toUri(); 
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);

 

http://localhost:9090/api/v1/crud-api에 get 요청을 보내는 메서드를 완성하시오.

답) fromUriString("http://localhost:9090"), path("/api/v1/crud-api")

 

WebClient webClient = WebClilent.create("http://localhost:9090");
WebClient clone = // 위에서 생성한 webClient 복제하기

 

답) WebClient clone = webClient.mutrate().build();

 

 

출처: 스프링부트 핵심 가이드 12장

SPRING #2

Editor :  노을

728x90

관련글 더보기