상세 컨텐츠

본문 제목

[스프링2] 12장. 서버 간 통신

23-24/Spring 2

by hyom1n 2023. 12. 29. 10:00

본문

728x90

 

최근 서비스들은 마이크로서비스 아키텍처(MSA)를 주로 채택합니다. MSA는 애플리케이션이 가지고 있는 서비스가 하나의 비즈니스 범위만 가지는 형태로, 애플리케이션은 자신이 가진 기능을 API로 외부에 노출하고, 다른 서버가 API를 호출하여 사용할 수 있게 구성합니다. 따라서 각 서버가 다른 서버의 클라이언트가 되는 경우가 많습니다.

이번 장에서 다른 서버로 웹 요청을 보내고 응답을 받을 수 있게 돕는 통신 모듈인 RestTemplate WebClient에 대하여 알아봅시다.


12.1 RestTemplate이란?

RestTemplate HTTP 통신 기능을 손쉽게 사용하도록 설계된 스프링 템플릿입니다. HTTP 서버와의 통신을 단순화한 템플릿을 이용하면 RESTful 원칙을 따르는 서비스를 편리하게 만들 수 있습니다. RestTemplate는 기본적으로 동기 방식으로 처리되며, 비동기 방식으로 사용하고 싶을 경우에는 AsyncRestTemplate를 사용합니다. 단, RestTemplate는 지원 중단된 상태입니다.

 

 

12.1.1 RestTemplate의 동작원리

RestTemplate의 동작을 도식화하면 다음과 같습니다.

[출처] 장정우,   『 스프링 부트 핵심 가이드』, 위키북스(2022), p.350

 

애플리케이션은 우리가 직접 작성하는 애플리케이션 코드 구현부를 의미합니다. 애플리케이션에 RestTemplate을 선언하고 URI와 HTTP 메서드, Body 등을 설정합니다.

그리고 외부 API로 요청을 보내면 RestTemplate에서 HttpMessageConverter를 통하여 ResquestEntity를 요청 메시지로 변환합니다. RestTemplate에서는 변환된 요청 메시지를 ClientHttpRequestFactory를 통하여 ClientHttpRequest로 가져온 후 외부 API로 요청을 보냅니다.

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

받은 응답 데이터가 정상적이라면 HttpMessageConverter를 거쳐 자바 객체로 변환하여 애플리케이션으로 반환합니다.

 

 

12.1.2 RestTemplate의 대표적인 메서드

RestTemplate는 편리하게 외부 API로 요청을 보낼 수 있도록 다양한 메서드를 제공합니다.

메서드 HTTP 형태 설명
getForObject GET GET 형식으로 요청한 결과를 객체로 반환
getForEntity GET GET 형식으로 요청한 결과를 ResponseEntity 형식으로 반환
postForLocation POST POST 형식으로 요청한 결과를 헤더에 저장된 URI로 반환
postForObject POST POST 형식으로 요청한 결과를 객체로 반환
postForEntity POST POST 형식으로 요청한 결과를 ResponseEntity 형식으로 반환
delete DELETE DELETE 형식으로 요청
put PUT PUT 형식으로 요청
patchForObject PATCH PATCH 형식으로 요청한 결과를 객체로 반환
optionsForAllow OPTIONS 해당 URI에서 지원하는 HTTP 메서드를 조회
exchange any HTTP 헤더를 임의로 추가할 수 있고, 어떤 메서드 형식에서도 사용할 수 있음
execute any 요청과 응답에 대한 콜백을 수정

 

 

 

 

12.2 RestTemplate 사용하기

RestTemplate을 사용해봅시다. 요청을 보낼 서버 용도의 프로젝트를 하나 생성하고, 별도의 프로젝트에서 RestTemplate을 통해 요청을 보내는 방식으로 실습을 진행합니다.

 

 

12.2.1 서버 프로젝트 생성하기

RestTemplate의 동작을 확인하기 위하여 서버 용도의 프로젝트를 생성합니다. 실습 환경에서 하나의 컴퓨터 안에서 두 개의 프로젝트를 가동시켜야 하기 때문에 톰캣의 포트를 변경해야 합니다.

프로젝트에는 spring-boot-starter-web 모듈만 의존성으로 추가하며, serverBox라는 이름으로 프로젝트를 생성했습니다. 프로젝트의 구조는 다음과 같습니다.

 

그리고 애플리케이션이 가동되는 톰캣의 포트를 변경하기 위하여 application.properties 파일에 server.port 속성을 추가합니다.

server.port = 9090

 

컨트롤러에는 GET과 POST 메서드 형식의 요청을 받기 위한 코드를 구성합니다. PUT, DELETE 메서드는 형식이 비슷하기에 생략합니다. 컨트롤러 클래스를 다음과 같이 작성합니다.

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

    @GetMapping
    public String getName() {
        return "Flature";
    }

    @GetMapping(value = "/{variable}")
    public String getVariable(@PathVariable String variable) {
        return variable;
    }

    @GetMapping("/param")
    public String getNameWithParam(@RequestParam String name) {
        return "Hello. " + name + "!";
    }

    @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);
    }

    @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);
    }
}

5~18번 줄의 코드는 GET 형식의 요청이 들어오는 상황의 케이스를 구현합니다. 첫 번째 메서드는 아무 파라미터가 없는 경우, 두 번째는 PathVariable을 사용하는 경우, 세 번째는 RequestParmeter를 사용하는 경우입니다.

20~46번 줄의 코드는 POST 형식의 요청을 받기 위한 두 개의 메서드를 구현합니다. 첫 번째 메서드는 요청 파라미터와 요청 바디를 함께 받도록 구현한 경우, 두 번째 메서드는 임의의 HTTP 헤더를 받도록 구현한 경우입니다.

 

여기서 사용된 MemberDto 객체는 다음과 같습니다. 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 + '\'' +
            ", organization='" + organization + '\'' +
            '}';
    }

}

 

 

12.2.2 RestTemplate 구현하기

RestTemplate 별도의 유틸리티 클래스로 생성하거나, 서비스 또는 비즈니스 계층에 구현됩니다. 앞서 생성한 서버 프로젝트에 요청을 날리기 위해 서버의 역할을 수행하면서 다른 서버로 요청을 보내는 클라이언트의 역할도 수행하는 RestTemplate을 포함하는 새로운 프로젝트를 다음과 같이 설정하여 생성합시다. 

  • groupId: com.springboot
  • artifactId: rest
  • name: rest
  • Developer Tools: Lombok, Spring Configuration Processor
  • Web: Spring Web

그리고 클라이언트에서 요청한 것처럼 실습하기 위하여 SwaggterConfiguration 클래스 파일 의존성을 추가해야 합니다. RestTemplate은 spring-boot-starter-web 모듈에 포함되어 있는 기능이므로 pom.xml에 별도로 의존성을 추가할 필요가 없습니다. 프로젝트의 구조는 클라이언트로부터 요청받는 컨트롤러 RestTemplate을 활용해 다른 서버에 통신 요청을 하는 서비스 계층입니다.

 

 

GET 형식의 RestTemplate 작성하기

먼저 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();
    }

RestTemplate을 생성하고 사용하는 방법은 다양하며, 가장 보편적인 방법은 UriComponentsBuilder를 사용하는 방법입니다. UriComponentsBuilder는 스프링 프레임워크에서 제공하는 클래스로, 여러 파라미터를 연결하여 URI 형식으로 만드는 기능을 수행합니다.

 

각 메서드는 컨트롤러 메서드와 비교하여 확인할 수 있습니다. 4~16번 줄의 메서드 PathVariable이나 파라미터를 사용하지 않는 호출 방법입니다. UriComponentsBuilder는 빌더 형식으로 객체를 생성합니다. fromUriString() 메서드에서는 호출부의 URL을 입력하고 이어서 path() 메서드에 세부 경로를 입력합니다. encode() 메서드는 인코딩 문자셋을 설정할 수 있는데, 인자를 전달하지 않으면 기본적으로 UTF-8로 설정됩니다.

이후 build() 메서드를 통하여 빌더 생성을 종료하고 UriComponents 타입이 리턴됩니다. toUri() 메서드를 통하여 URI를 리턴 받았는데, 만약 String 타입의 URI를 사용한다면 toUriString() 메서드로 대체하여 사용합니다.

 

이렇게 생성된 uri는 restTemplate이 외부 API를 요청하는 데 사용되며, 13번 줄 getForEntity()에 파라미터로 전달됩니다. getForEntity()는 URI와 응답받는 타입을 매개변수로 사용합니다.

18~31번 줄의 코드에서 path() 메서드는 입력한 세부 URI 중괄호 부분을 사용하여 개발 단계에서 쉽게 이해할 수 있는 변수명을 입력하고 expand() 메서드에서는 콤마로 구분하여 순서대로 값을 입력합니다.

33~46번 줄은 파라미터로 전달하는 예제입니다. queryParam() 메서드를 사용하여 키, 값 형식으로 파라미터를 추가할 수 있습니다.

 

 

 

POST 형식의 RestTemplate 작성하기

먼저 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@wikibooks.co.kr");
        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;
    }

예제에서 1~22번 줄은 POST 형식으로, 외부 API에 요청할 때 Body 값과 파라미터 값을 담는 방법 두 가지 모두 보여줍니다. 2~10번 줄에서 파라미터에 값을 추가하는 작업이 수행되며, 12~19번 줄에서 RequestBody에 값을 담는 작업이 수행됩니다. RequestBody에 값을 담기 위해서 12~15번 줄과 같이 데이터 객체를 생성합니다. postForEntity() 메서드를 사용할 경우에 파라미터로 데이터 객체를 넣으면 됩니다.

postForEntity() 메서드로 서버 프로젝트의 API를 호출하면 서버 프로젝트의 콘솔 로그에는 RequestBody 값이 출력되고 파라미터 값을 결괏값으로 리턴됩니다.

 

24~47번 줄의 메서드는 헤더를 추가하는 예제입니다. 대부분의 외부 API는 토큰키를 받아 서비스 접근을 인증하는 방식으로 작동합니다. 이때 토큰값을 헤더에 담아 전달하는 방식이 가장 많이 사용됩니다. 헤더를 설정하기 위해서는 RequestEntity를 정의해서 사용하는 방법이 가장 편한 방법입니다. 37~40번 줄 RequestEntity를 생성하고 post() 메서드로 URI를 설정한 후 header() 메서드에서 헤더의 키 이름과 값을 설정하는 코드입니다.

47번 줄 exchange() 메서드는 모든 형식의 HTTP 요청을 생성합니다. RequestEntity의 설정에서 post() 메서드 대신 다른 형식의 메서드로 정의만 하면 exchange() 메서드로 사용할 수 있기에 대부분 exchange() 메서드를 사용합니다.

 

프로젝트에서 쉽게 API를 호출할 수 있게 Swagger를 설정하겠습니다. pom.xml에 Swagger 의존성을 추가한 뒤 Swagger 설정 코드를 작성합니다.

@Configuration
@EnableSwagger2
public class SwaggerConfiguration {

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            .select()
            //.apis(RequestHandlerSelectors.any())
            .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();
    }

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

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

}

 

2개의 애플리케이션을 실행하고 postDto() 메서드에 해당하는 POST API를 호출하면 아래의 결과가 출력됩니다.

http://localhost:8080/swagger-ui.html#/rest-template-controller/postDtoUsingPOST

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

 

 

 

12.2.3 RestTemplate 커스텀 설정

RestTemplate HTTPClient를 추상화합니다. HTTPClient의 종류에 따라 기능에 차이가 있으며, 가장 큰 차이는 커넥션 풀(Connection Pool)입니다. 커넥션 풀이란 클라이언트 요청이 오면 connection을 빌려주고, 처리가 끝나면 다시 connection을 반납받아 pool에 저장하는 방식입니다.

RestTemplate은 기본적으로 커넥션 풀을 지원하지 않습니다. 매번 호출할 때마다 포트를 열어 커넥션을 생성하는데, TIME_WAIT 상태가 된 소켓을 다시 사용하려고 접근한다면 재사용하지 못합니다. 이를 방지하기 위해 커넥션 풀 기능을 활성화하여 재사용할 수 있게 하는 것이 좋습니다. 이를 위하여 아파치에서 제공하는 HttpClient로 대체하여 사용합니다.

 

아파치의 HttpClient를 사용하기 위하여 pom.xml에 다음과 같이 의존성을 추가합니다.

    <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를 사용합니다.

별도의 HttpComponetsClientHttpRequestFactory 객체를 생성하여 ClientHttpRequestFactory를 사용하면 RestTemplate의 Timeout 설정을 할 수 있습니다.

커넥션 풀을 설정하기 위해 HttpClient를 HttpComponetsClientHttpRequestFactory에 설정할 수 있습니다. HttpClient를 생성하는 방법은 create() 메서드를 사용하거나 custom() 메서드를 사용하는 것입니다.

생성한 HttpClient는 14번 줄과 같이 setHttpClient() 메서드를 통하여 인자로 전달하여 설정할 수 있습니다. 이렇게 설정된 factory 객체를 RestTemplate을 초기화하는 과정에서 인자로 전달하면 됩니다.

 

 

 

12.3 WebClient란?

실제 운영환경에 적용되는 애플리케이션은 정식 버전의 스프링 부트보다 낮은 경우가 많아,  RestTemplate를 사용하는 경우가 많습니다. 최신 버전에서는 RestTemplate의 지원이 중단되었기 때문에 WebClient를 사용하는 것을 권고합니다.

 

Spring WebFlux는 HTTP 요청을 수행하는 클라이언트 WebClient를 제공합니다. WebClient 리액터 기반으로 동작하는 API입니다. 리액터 기반이므로 스레드와 동시성 문제를 벗어나 비동기 형식으로 사용할 수 있습니다. RestTemplate와 WebClient의 특징을 비교하여 살펴봅시다.

RestTemplate의 특징 WebClient의 특징
HTTP 프로토콜의 메서드에 맞는 여러 메서드 제공 논블로킹 I/O 지원
RESTful 형식을 갖춘 템플릿 리액티브 스트림의 백 프레셔 지원
HTTP 요청 후 다양한 형식의 응답 설정 가능 적은 하드웨어 리소스로 동시성 지원
블로킹 I/O기반의 동기 방식 사용 함수형 API 지원
다른 API 호출 시 HTTP 헤더에 다양한 값 설정 가능 동기, 비동기 상호작용 지원
  스트리밍 지원

 

 

12.3.1 WebCllient 구성

WebClient를 사용하려면 pom.xml 파일에 WebFlux 모듈에 대한 의존성을 추가해야 합니다.  WebFlux는 클라이언트와 서버 간 리액티브 애플리케이션 개발을 지원하기 위해 새롭게 추가된 모듈입니다.

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

 

 

12.3 WebClient 사용하기

WebClient를 활용한 코드를 작성해봅시다. 지금까지의 실습은 리액티브 프로그래밍을 기반으로 작성된 애플리케이션이 아니기 때문에 WebClient를 사용하는데에 제약사항이 있습니다.

 

 

12.3.1 WebCllient 구현

WebClient를 생성하는 방법은 create()를 이용하거나 builder()를 이용하는 방법이 있습니다. 먼저 서버 프로젝트의 GET 메서드 컨트롤러에 접근할 수 있는 WebClient를 생성합니다.

@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();
    }

4~15번 줄의 getName() 메서드는 builder()를 활용하고, 나머지 메서드는 create()를 활용하여 WebClient를 생성하였습니다.

WebClient 객체를 생성한 후 요청을 전달하는 방식으로 동작합니다. 5~8번 줄에서, builder()를 통해 기본 URL을 설정하고, defaultHeader() 메서드로 헤더의 값을 설정했습니다.

일반적으로 WebClient 객체를 이용할 때는 객체를 생성한 후, 재사용하는 방식으로 구현하는 것이 좋습니다. 빌드된 WebClient는 변경될 수 없으나 복사하여 사용할 수 있습니다.

 

10~14번 줄은 실제 요청 코드입니다. WebClient HTTP 메서드를 네이밍 명확한 메서드로 설정할 수 있으며, URI를 확장하는 방법으로 uri() 메서드를 사용할 수 있습니다.

retrieve() 메서드는 요청에 대한 응답을 받았을 때 값을 추출하는 방법 중 하나입니다. bodyToMono() 메서드를 통하여 리턴 타입을 설정해서 문자열 객체를 받아오게 됩니다. Mono 리액티브 스트림에서 데이터를 제공하는 발행자 역할을 수행하는 Publisher의 구현체이며, Flux와 비교됩니다.

 

WebClient는 논블로킹 방식으로 동작하기에 14번 줄 block() 메서드를 추가하여 블로킹 형식으로 동작하도록 하였습니다. 17~26번 줄 getNameWithPathVariable() 메서드는 PathVariable 값을 추가하여 요청 보내는 예제입니다. 21번 줄처럼 uri() 메서드 내부에서 uriBuilder를 사용하여 path를 설정하고 build() 메서드에 추가할 값을 넣는 것으로 PathVariable을 추가할 수 있습니다. 또한, bodyToMono()가 아닌 toEntity()를 사용하였는데, 이를 통하여 ResponseEntity 타입으로 응답을 받을 수 있습니다.

 

28~42번 줄 getNameWithParameter() 메서드는 쿼리 파라미터를 함께 전달받는 방법을 제시합니다. 쿼리 파라미터를 요청에 담기 위하여 uriBuilder를 사용하였으며, queryParam() 메서드를 사용하여 전달하려는 값을 설정합니다. retrieve() 대신 exchange() 메서드를 사용하였는데, 이는 응답 결과 코드에 따라 응답을 다르게 설정할 수 있습니다. exchange()  메서드는 지원 중단되었기 때문에 exchangeToMono() 또는 exchangeFlux()를 사용해야 합니다.

 

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();
    }

 

15~23번 줄 WebClient에서 post() 메서드를 통하여 POST 메서드 통신을 정의했고, uri()는 uriBuilder로 path와 parameter를 설정했습니다. bodyValue() 메서드를 통하여 HTTP 바디 값을 설정합니다. HTTP 바디에는 일반적으로 데이터 객체를 파라미터로 전달합니다.

26~46번 줄의 postWithHeader() 메서드는 POST 요청을 보낼 때 헤더를 추가하여 보내는 예제입니다. 42번 줄에서 header() 메서드를 사용하여 헤더에 값을 추가했습니다. 일반적으로 임의로 추가한 헤더에는 외부 API를 사용하기 위해 인증된 토큰값을 담아 전달합니다.

 


0.1 환경설정

아래 코드를 통하여 MySQL DB 및 사용자 생성을 진행합니다.

create user 'cos'@'%' identified by 'cos1234';
GRANT ALL PRIVILEGES ON *.* TO 'cos'@'%';
create database security;
use security;

 

아래의 설정으로 프로젝트를 생성합니다.

  • groupId: com.cos
  • artifactId: security1
  • name: security1
  • 의존성: Spring Boot DevTools, Lombok, Spring Data JPA, MySQL Driver, Spring Security, Mustache, Spring Web

application.yml를 다음과 같이 수정합니다.

server:
  port: 8080
  servlet:
    context-path: /
    encoding:
      charset: UTF-8
      enabled: true
      force: true
      
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Seoul
    username: cos
    password: cos1234

  jpa:
    hibernate:
      ddl-auto: create #create update none
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    show-sql: true

 

controller 패키지 생성 후 indexContoroller를 아래와 같이 작성합니다.

@Controller
public class IndexController {
    @GetMapping({"", "/"})
    pubilc String index() {
    	return "index"; //src/main/resoures/templates/index.mustache
    }
}

 

@Controller는 View를 리턴하겠다는 어노테이션이며, 머스테치의 기본폴더는 src/main/resoures/ 이기 때문에 application.yml 파일의 뷰리졸버 설정 부인 아래 코드는 생략 가능합니다.

  mvc:
    view:
      prefix: /templates/
      suffix: .mustache

 

templates 패키지 안에 index.html 파일을 생성합니다.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>인덱스 페이지</title>
</head>
<body>
<h1>인덱스페이지입니다.</h1>
</body>
</html>

 

config 패키지 안에 WebMvcConfig.java 파일을 생성합니다.

@Configuration
public class WebMvcConfig implements WebMvcConfigurer{  

  @Override
  public void configureViewResolvers(ViewResolverRegistry registry) {
      MustacheViewResolver resolver = new MustacheViewResolver();

      resolver.setCharset("UTF-8");
      resolver.setContentType("text/html;charset=UTF-8");
      resolver.setPrefix("classpath:/templates/");
      resolver.setSuffix(".html");

      registry.viewResolver(resolver);
  }
}

오버라이딩을 통하여 머스테치를 재설정합니다. 뷰의 인코딩, 데이터 파일의 형식 등을 전달하여 머스테치가 이식할 수 있도록 합니다. 이렇게 설정한 resolver를 registry의 viewResolver로 등록합니다. 


Quiz

1. (  RestTemplate  )은 HTTP 통신 기능을 손쉽게 사용하도록 설계된 스프링 템플릿으로, 기본적으로 (  동기  ) 방식으로 처리된다.

2. 실습 환경에서 하나의 컴퓨터 안에서 두 개의 프로젝트를 가동시킬 때는  톰캣 포트  )를 변경해야 한다.

3. RestTemplate 별도의 (유틸리티 클래스)로 생성하거나, (서비스) 또는 (비즈니스) 계층에 구현된다.

4. 스프링 프레임워크에서 제공하는 클래스인 (UriComponentsBuilder)는 여러 파라미터를 연결하여 URI 형식으로 만드는 기능을 수행하며, RestTemplate를 생성할 때 사용한다.

5. HTTPClient의 종류에 따라 기능에 차이가 있으며, 가장 큰 차이는 (커넥션 풀)이다. RestTemplate는 기본적으로 이를 지원하지 않는다.

6. (create())를 사용하거나 (custom())를 사용하여 HttpClient를 생성할 수 있다.

7.  WebClient )은 리액터 기반으로 동작하는 API으로, 스레드와 동시성 문제를 벗어나 (  비동기  ) 방식으로 사용된다.

8. 다음 GET 형식의 RestTemplate 예제를 참고하여 WebClient를 활용한 코드를 작성하시오.

@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();
    }
}
// 8번 정답
@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();
    }
}

 

 

9. 컨트롤러에 id에 해당하는 Member를 업데이트하고 성공 메시지를 반환하는 PUT 메서드를 추가하여 작성한 뒤 RestTemplate으로  id와 memberDto를 이용하여 PUT 요청을 보내고 결과를 반환하는 코드를 작성하시오.

// 9번 정답

// 컨트롤러
@PutMapping("/{id}")
public ResponseEntity<String> updateMember(@PathVariable Long id, @RequestBody MemberDto request) {
    return ResponseEntity.status(HttpStatus.OK).body("Member with ID " + id + " updated successfully");
}

// PUT 형식의 RestTemplate
public String updateMember(Long id, MemberDto memberDto) {
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.put("http://localhost:9090/api/v1/crud-api/{id}", memberDto, id);
    
    return "Member with ID " + id + " updated successfully";
}

[출처] 장정우, 『스프링 부트 핵심 가이드』, 위키북스(2022), p.349-348.

[출처] 최주호, [스프링부트 시큐리티 & JWT 강의], (2023, 2월 22일), 섹션 0. 스프링 시큐리티 기본(1강 환경설정)

 

Corner Spring 2

Editor : 동동

728x90

관련글 더보기