be_better

[Springboot] 서버 간 통신 - WebClient 본문

Springboot/이론

[Springboot] 서버 간 통신 - WebClient

NiceTry 2023. 11. 29. 23:44

목차

    WebClient 란?

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

     

    WebClient 특징

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

     

    WebClient 구성

    WebClient를 사용하려면 WebFlux 모듈에 대한 의존성을 추가해야 한다.

     

    WebFlux 의존성 추가
    // gradle
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    
    // maven
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
      </dependency>
    </dependencies>

     

    WebClient 사용하기

    WebClient 구현

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

    • create() 메서드를 이용한 생성
    • builder()를 이용한 생성

    WebClient GET 예제

    WebClient를 활용한 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("Jnjeaaaat"))
                    .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", "Jnjeaaaat")
                            .build())
                    .exchangeToMono(clientResponse -> {
                        if (clientResponse.statusCode().equals(HttpStatus.OK)) {
                            return clientResponse.bodyToMono(String.class)
                        } else {
                            return clientResponse.createException().flatMap(Mono::error);
                        }
                    })
                    .block();
        }
    }

    getName() 메서드는 builder()를 활용해 WebClient를 만들고

    다른 두 개의 메서드에서는 create()를 활용해 WebClient를 생성한다.

     

    WebClient는 우선 객체를 생성한 후 요청을 전달하는 방식으로 동작한다. builder()를 통해 baseUrl() 메서드에서 기본 URL을 설정하고 defaultHeader() 메서드로 헤더의 값을 설정했다. 일반적으로 WebClient 객체를 이용할 때는 이처럼 WebCllient 객체를 생성한 후 재사용하는 방식으로 구현하는 것이 좋다.

     

    builder()를 사용할 경우 확장할 수 있는 메서드 리스트

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

    이미 빌드된 WebClient는 변경할 수 없지만 복사해서는 사용할 수 있다.

     

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

     

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

     

    retrieve() 메서드는 요청에 대한 응답을 받았을 때 그 값을 추출하는 방법. retrieve() 메서드는 bodyToMono() 메서드를 통해 리턴 타입을 설정해서 문자열 객체를 받아온다.

    이때 Mono는 Flux와 비교되는 개념으로 Flux와 Mono는 리액티브 스트림에서 데이터를 제공하는 발행자 역할을 수행하는 Publisher의 구현체이다.

     

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

     

    코드에서 getNameWithPathVariable() 메서드 내부에서 uriBuilder를 사용해 path를 설정하고 build() 메서드에 추가할 값을 넣는 것으로 pathVariable을 추가하는 방식으로 구현했지만 간략하게 작성하고 싶다면 다음 코드로 변경해도 좋다.

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

    이때 toEntity() 를 사용하면 ResponseEntity 타입으로 응답을 전달받을 수 있다.

     

    WebClient POST 예제

    WebClient를 활용한 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("Jnjeaaaat");
            memberDto.setEmail("ekswn120@gmail.com");
            memberDto.setOrganization("Around Hub Studio");
    
            return webClient.post().uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api")
                            .queryParam("name", "Jnjeaaaat")
                            .queryParam("email", "ekswn120@gmail.com")
                            .queryParam("organization", "Free")
                            .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("Jnjeaaaat");
            memberDto.setEmail("ekswn120@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 방식에서는 bodyValue() 메서드를 통해 HTTP 바디 값을 설정하고

    header() 메서드를 통해 헤더에 값을 추가한다.