12.2 서버 간 통신 - WebClient

김찬미·2024년 7월 1일
0

WebClient란?

일반적으로 실제 운영 환경에 적용되는 애플리케이션은 낮은 버전의 스프링 부트를 사용할 때가 많다. 때문에 RestTemplate을 많이 사용하고 있지만, 최신 버전에서는 RestTemplate이 지원 중단되어 WebClient를 사용할 것을 권고하고 있다. 이러한 흐름에 맞춰 현재 빈번히 가용되고 있는 RestTemplate과 앞으로 많이 사용될 WebClient를 모두 알고 있는 것이 좋다.

WebClient의 특징

Spring WebFlux는 HTTP 요청을 수행하는 클라이언트로 WebClient를 제공한다. WebClient는 리액터(Reactor) 기반으로 동작하는 API이다. 리액터 기반이므로 스레드와 동시성 문제를 벗어나 비동기 형식으로 사용할 수 있다. WebClient의 주요 특징은 다음과 같다.

  • 논블로킹(Non-Blocking) I/O를 지원한다.
  • 리액티브 스트림(Reactive Streams)의 백프레셔(Back Pressure)를 지원한다.
  • 적은 하드웨어 리소스로 동시성을 지원한다.
  • 함수형 API를 지원한다.
  • 동기, 비동기 상호작용을 지원한다.
  • 스트리밍을 지원한다.

최근 프로그래밍 추세에 맞춰 스프링에도 리액티브 프로그래밍(Reactive Programming)이 도입되면서 여러 동시적 기능이 제공되고 있다. 다만 이 문서에서는 리액티브 프로그래밍을 자세히 다루지 않으며, WebClient를 사용할 수 있는 환경을 구성하고 사용하는 방법에 대해서만 다루도록 하겠다.

WebClient 구성

WebClient를 사용하려면 WebFlux 모듈에 대한 의존성을 추가해야 한다. 아래와 같이 pom.xml 파일에 의존성을 추가한다.

<dependencies>
    <!-- 생략 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <!-- 생략 -->
</dependencies>

WebFlux는 클라이언트와 서버 간 리액티브 애플리케이션 개발을 지원하기 위해 스프링 프레임워크 5에서 새롭게 추가된 모듈이다. pom.xml에 위와 같이 WebFlux를 추가하면 WebClient를 사용할 수 있는 환경이 만들어진다.


WebClient 사용하기

이제 환경이 구성됐으므로 WebClient를 활용한 코드를 작성해보자. 다만 지금까지의 실습은 리액티브 프로그래밍을 기반으로 작성된 애플리케이션이 아니기 때문에 WebClient를 온전히 사용하기에는 제약사항이 있다.

WebClient 구현

WebClient를 생성하는 방법은 다음과 같이 크게 두 가지가 있다.

  1. create() 메서드를 이용한 생성
  2. builder()를 이용한 생성

WebClient 생성 예제

먼저 앞에서 생성했던 서버 프로젝트의 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();

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

위 코드에서 3개의 메서드가 정의돼 있다. getName() 메서드는 builder()를 활용해 WebClient를 만들고 다른 두 개의 메서드에서는 create()를 활용해 WebClient를 생성한다.

WebClient 설정 및 요청

WebClient는 우선 객체를 생성한 후 요청을 전달하는 방식으로 동작한다. 이를 위해 builder()를 통해 baseUrl() 메서드에서 기본 URL을 설정하고 defaultHeader() 메서드로 헤더의 값을 설정했다. 일반적으로 WebClient 객체를 이용할 때는 이처럼 WebClient 객체를 생성한 후 재사용하는 방식으로 구현하는 것이 좋다. 예제에서 소개된 메서드 외에 builder()를 사용할 경우 확장할 수 있는 메서드는 다음과 같다.

  • defaultHeader(): WebClient의 기본 헤더 설정
  • defaultCookie(): WebClient의 기본 쿠키 설정
  • defaultUriVariable(): WebClient의 기본 URI 확장값 설정
  • filter(): WebClient에서 발생하는 요청에 대한 필터 설정

일단 빌드된 WebClient는 변경할 수 없다. 다만 다음과 같이 복사해서 사용할 수는 있다.

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

실제 요청 예제

10~14번 줄에는 실제 요청 코드가 작성돼 있다. WebClient는 HTTP 메서드를 get(), put(), delete() 등의 네이밍이 명확한 메서드로 설정할 수 있다. 그리고 URI를 확장하는 방법으로 uri() 메서드를 사용할 수 있다.

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

WebClient는 기본적으로 논블로킹(Non-Blocking) 방식으로 동작하기 때문에 기존에 사용하던 코드의 구조를 블로킹 구조로 바꿔줄 필요가 있다. 예제에서는 14번 줄에 block()이라는 메서드를 추가해서 블로킹 형식으로 동작하게끔 설정했다.

PathVariable 및 Parameter 사용 예제

getNameWithPathVariable() 메서드는 PathVariable 값을 추가해 요청을 보내는 예제이다. 21번 줄처럼 uri() 메서드 내부에서 uriBuilder를 사용해 path를 설정하고 build() 메서드를 추가할 값을 넣는 것으로 pathVariable을 추가할 수 있다. 좀 더 간략하게 작성하고 싶다면 다음과 같이 작성할 수 있다.

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

getNameWithPathVariable()bodyToMono() 메서드가 아닌 toEntity()를 사용하는 예제이다. toEntity()를 사용하면 ResponseEntity 타입으로 응답을 전달받을 수 있다.

getNameWithParameter() 메서드는 쿼리 파라미터를 함께 전달하는 방법을 제시한다. 쿼리 파라미터를 요청에 담기 위해서는 uriBuilder를 사용하며, queryParam() 메서드를 사용해 전달하려는 값을 설정한다. 그리고 예제에서는 retrieve() 대신 exchange() 메서드를 사용했다. exchange() 메서드는 지원 중단됐기 때문에 exchangeToMono() 또는 exchangeToFlux()를 사용해야 한다. exchange() 메서드는 응답 결과 코드에 따라 다르게 응답을 설정할 수 있다. 34~40번 줄을 보면 clientResponse 결괏값에 따라 if문 분기를 만들어 상황에 따라 결괏값을 다르게 전달할 수 있다.

POST 요청 예제

POST 요청을 아래와 같이 작성할 수 있다.

@Service
public class WebClientService {
    
    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();
    }
}

POST 요청 설명

WebClient를 생성하고 사용하는 방법은 앞서 본 GET 요청을 만드는 방법과 다르지 않다. 다만 POST 방식에서 눈여겨볼 내용은 HTTP 바디 값을 담는 방법과 커스텀 헤더를 추가하는 방법이다.

postWithParamAndBody() 메서드는 webClient에서 post() 메서드를 통해 POST 메서드 통신을 정의했고, uri()uriBuilder로 path와 parameter를 설정했다. 그 후 bodyValue() 메서드를 통해 HTTP 바디 값을 설정한다. HTTP 바디에는 일반적으로 데이터 객체(DTO, VO 등)를 파라미터로 전달한다.

postWithHeader() 메서드는 POST 요청을 보낼 때 헤더를 추가해서 보내는 예제이다. 전반적인 내용은 동일하며, header() 메서드를 사용해 헤더에 값을 추가했다. 일반적으로 임의로 추가한 헤더에는 외부 API를 사용하기 위해 인증된 토큰값을 담아 전달한다.

💡 Tip.
자세한 WebClient의 사용법은 공식 문서에서 확인할 수 있다.
Spring WebFlux 공식 문서


지금까지 웹 통신을 위해 RestTemplate 과 WebClient를 사용하는 방법을 알아봤다. 실무에서 다른 서버의 리소스에 접근하는 상황은 자주 발생한다. 이러한 경우 대체로 이번 장에서 소개한 통신 모듈을 사용해 기능을 구현하면 해결된다.

여기서 소개한 방법을 익혔다면 이후에는 통신하는 횟수나 접근하는 서버의 특성에 맞게 커넥션 풀이나 타임아웃 등의 설정을 최적화하는 작업으로 심화 학습을 진행하면 된다. 또한 서버와의 웹 통신을 최적화하는 방법으로 어떤 방법이 있는지 알아보고, 각 상황에 따른 설정 방법을 공부해 보는 것을 권장한다.

profile
백엔드 개발자

0개의 댓글

관련 채용 정보