ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • WebClient
    Spring Reactive Web Application/Spring WebFlux 2023. 9. 16. 10:00
    반응형

    WebClient

      WebClient는 Spring 5부터 지원하는 Non-Blocking HTTP request를 위한 리액티브 웹 클라이언트로서 함수형 기반의 향상된 API를 제공합니다. WebClient는 내부적으로 HTTP 클라이언트 라이브러리에게 HTTP request를 위임하며, 기본 HTTP 클라이언트 라이브러리는 Reactor Netty입니다. WebClient는 Non-Blocking과 Blocking HTTP request를 모두 지원합니다.

     

      다음은 WebClient를 사용한 예제 코드입니다.

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.ApplicationArguments;
    import org.springframework.boot.ApplicationRunner;
    import org.springframework.context.annotation.Bean;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.reactive.function.client.WebClient;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    
    @Slf4j
    //@Configuration
    public class WebClientExample01 {
        @Bean
        public ApplicationRunner examplesWebClient() {
    
            return (ApplicationArguments arguments) -> {
                exampleWebClient01();
                exampleWebClient02();
                exampleWebClient03();
                exampleWebClient04();
            };
        }
    
        private void exampleWebClient01() {
            BookDto.Post requestBody = new BookDto.Post("Java 중급",
                    "Intermediate Java",
                    "Java 중급 프로그래밍 마스터",
                    "Kevin1", "222-22-2222-222-2",
                    "2022-03-22");
    
            WebClient webClient = WebClient.create();
            Mono<ResponseEntity<Void>> response =
                    webClient
                            .post()
                            .uri("http://localhost:8080/v10/books")
                            .bodyValue(requestBody)
                            .retrieve()
                            .toEntity(Void.class);
    
            response.subscribe(res -> {
               log.info("response status: {}", res.getStatusCode());
               log.info("Header Location: {}", res.getHeaders().get("Location"));
            });
        }
    
        private void exampleWebClient02() {
            BookDto.Patch requestBody =
                    new BookDto.Patch.PatchBuilder().titleKorean("Java 고급")
                    .titleEnglish("Advanced Java")
                    .description("Java 고급 프로그래밍 마스터")
                    .author("Tom")
                    .build();
    
            WebClient webClient = WebClient.create("http://localhost:8080");
            Mono<BookDto.Response> response =
                    webClient
                            .patch()
                            .uri("http://localhost:8080/v10/books/{book-id}", 20)
                            .bodyValue(requestBody)
                            .retrieve()
                            .bodyToMono(BookDto.Response.class);
    
            response.subscribe(book -> {
                log.info("bookId: {}", book.getBookId());
                log.info("titleKorean: {}", book.getTitleKorean());
                log.info("titleEnglish: {}", book.getTitleEnglish());
                log.info("description: {}", book.getDescription());
                log.info("author: {}", book.getAuthor());
            });
        }
    
        private void exampleWebClient03() {
            Mono<BookDto.Response> response =
                    WebClient
                            .create("http://localhost:8080")
                            .get()
                            .uri(uriBuilder -> uriBuilder
                                    .path("/v10/books/{book-id}")
                                    .build(21))
                                    .retrieve()
                                    .bodyToMono(BookDto.Response.class);
    
            response.subscribe(book -> {
                log.info("bookId: {}", book.getBookId());
                log.info("titleKorean: {}", book.getTitleKorean());
                log.info("titleEnglish: {}", book.getTitleEnglish());
                log.info("description: {}", book.getDescription());
                log.info("author: {}", book.getAuthor());
            });
        }
    
        private void exampleWebClient04() {
            Flux<BookDto.Response> response =
                    WebClient
                            .create("http://localhost:8080")
                            .get()
                            .uri(uriBuilder -> uriBuilder
                                    .path("/v10/books")
                                    .queryParam("page", "1")
                                    .queryParam("size", "10")
                                    .build())
                            .retrieve()
                            .bodyToFlux(BookDto.Response.class);
    
            response
                    .map(book -> book.getTitleKorean())
                    .subscribe(bookName -> log.info("book name: {}", bookName));
        }
    }

    exampleWebClient01()

      BookHandler의 createBook() 핸들러 메서드를 호출합니다.

    • WebClient.create(): WebClient 인터페이스의 구현 객체를 생성합니다.
    • post(): HTTP Method 타입을 POST로 지정합니다.
    • uri(): request를 전송할 URI를 지정합니다.
    • bodyValue(): body(BodyInserter)의 단축 메서드로 request body를 지정합니다.
    • retrieve(): response를 어떤 형태로 얻을지에 대한 프로세스의 시작을 선언하는 역할을 합니다.
    • toEntity(Void.class): 파라미터로 주어진 클래스의 형태로 변환한 response body가 포함된 ResponseEntity 객체를 리턴합니다. createMember()가 리턴하는 ServerResponse에 body 데이터가 추가되지 않기에 Void.class가 됩니다.

     

    exampleWebClient02()

      BookHandler의 patchBook() 핸들러 메서드를 호출합니다.

    • patch(): HTTP Method 타입을 PATCH로 지정합니다.
    • uri(): path variable이 포함된 URI의 경우, Varargs의 형태로 path variable의 값을 전달할 수 있습니다.
    • bodyToMono(): response body를 파라미터로 전달된 타입의 객체로 디코딩합니다.

     

    exampleWebClient03()

      BookHandler의 getBook() 핸들러 메서드를 호출합니다.

    • get(): HTTP Method 타입을 GET으로 지정합니다.
    • uri(): uriBuilder를 이용해 path variable의 값을 포함한 URI를 생성합니다. build() 메서드의 파라미터로 전달한 값은 path variable의 값으로 설정됩니다.

     

    exampleWebClient04()

      BookHandler의 getBooks() 핸들러 메서드를 호출합니다.

    • uri(): uriBuilder의 queryParam() 메서드를 이용해 페이지네이션을 위한 query parameter인 page와 size의 값을 포함한 URI를 생성합니다.
    • bodyToFlux(): response body를 파라미터로 전달된 타입의 객체로 디코딩합니다. bodyToFlux()는 bodyToMono()와 달리 Java Collection 타입의 response body를 수신합니다.

     

    WebClient Connection Timeout 설정

      다음은 WebClient에 HTTP Connection Timeout을 설정한 예제 코드입니다. 코드에 대한 설명은 주석을 참고하시면 됩니다.

    import io.netty.channel.ChannelOption;
    import io.netty.handler.timeout.ReadTimeoutHandler;
    import io.netty.handler.timeout.WriteTimeoutHandler;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.ApplicationArguments;
    import org.springframework.boot.ApplicationRunner;
    import org.springframework.context.annotation.Bean;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.client.reactive.ReactorClientHttpConnector;
    import org.springframework.web.reactive.function.client.WebClient;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    import reactor.netty.http.client.HttpClient;
    
    import java.time.Duration;
    import java.util.concurrent.TimeUnit;
    
    @Slf4j
    @Configuration
    public class WebClientExample02 {
        @Bean
        public ApplicationRunner examplesWebClient02() {
    
            return (ApplicationArguments arguments) -> {
                exampleWebClient01();
            };
        }
    
        private void exampleWebClient01() {
            // HTTP Client 객체를 생성합니다.
            HttpClient httpClient =
                    HttpClient
                            .create()
                            // option(): Connection 설정을 위한 Timeout 시간을 설정합니다.
                            // (HTTP Connection이 연결되기까지의 시간을 의미합니다.)
                            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 500)
                            // responseTimeout(): 응답을 수신하기까지의 Timeout 시간을 설정합니다.
                            .responseTimeout(Duration.ofMillis(500))
                            // doOnConnected(): Connection이 연결된 이후 수행할 동작을 정의합니다.
                            .doOnConnected(connection ->
                                connection
                                        .addHandlerLast(
                                                // ReadTimeoutHandler(): 특정 시간 동안 읽을 수 있는 데이터가 없을 경우 ReadTimeoutException을 발생시킵니다.
                                                new ReadTimeoutHandler(500,
                                                                    TimeUnit.MILLISECONDS))
                                        .addHandlerLast(
                                                // WriteTimeoutHandler(): 특정 시간 동안 쓰기 작업을 종료할 수 없을 경우 WriteTimeoutException을 발생시킵니다.
                                                new WriteTimeoutHandler(500,
                                                                    TimeUnit.MILLISECONDS)));
    
            Flux<BookDto.Response> response =
                    WebClient
                            // Builder 패턴을 통해 WebClient 객체를 생성합니다.
                            .builder()
                            .baseUrl("http://localhost:8080")
                            // HTTP Client Connector를 설정합니다.
                            // Reactor Netty에서 제공하는 ReactorClientHttpConector를 설정합니다.
                            .clientConnector(new ReactorClientHttpConnector(httpClient))
                            .build()
                            .get()
                            .uri(uriBuilder -> uriBuilder
                                    .path("/v10/books")
                                    .queryParam("page", "1")
                                    .queryParam("size", "10")
                                    .build())
                            .retrieve()
                            .bodyToFlux(BookDto.Response.class);
    
            response
                    .map(book -> book.getTitleKorean())
                    .subscribe(bookName -> log.info("book name2: {}", bookName));
        }
    }

     

    exchangeToMono()를 사용한 응답 디코딩

      retrieve() 대신 exchangeToMono()exchangeToFlux() 메서드를 이용하면 response를 사용자의 요구 조건에 맞게 제어할 수 있습니다.  다음은 exchangeToMono()를 통해 response를 수신한 다음, HttpStatus가 CREATED이면 ResponseEntity를 리턴하고 그 외에는 Exception을 throw하도록 처리한 예제 코드입니다.

    import io.netty.channel.ChannelOption;
    import io.netty.handler.timeout.ReadTimeoutHandler;
    import io.netty.handler.timeout.WriteTimeoutHandler;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.ApplicationArguments;
    import org.springframework.boot.ApplicationRunner;
    import org.springframework.context.annotation.Bean;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.client.reactive.ReactorClientHttpConnector;
    import org.springframework.web.reactive.function.client.WebClient;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    import reactor.netty.http.client.HttpClient;
    
    import java.time.Duration;
    import java.util.concurrent.TimeUnit;
    
    @Slf4j
    @Configuration
    public class WebClientExample02 {
        @Bean
        public ApplicationRunner examplesWebClient02() {
    
            return (ApplicationArguments arguments) -> {
                exampleWebClient02();
            };
        }
    
        private void exampleWebClient02() {
            BookDto.Post post = new BookDto.Post("Java 중급",
                    "Intermediate Java",
                    "Java 중급 프로그래밍 마스터",
                    "Kevin1", "333-33-3333-333-3",
                    "2022-03-22");
            WebClient webClient = WebClient.create();
            webClient
                    .post()
                    .uri("http://localhost:8080/v10/books")
                    .bodyValue(post)
                    .exchangeToMono(response -> {
                        if(response.statusCode().equals(HttpStatus.CREATED))
                            return response.toEntity(Void.class);
                        else
                            return response
                                    .createException()
                                    .flatMap(throwable -> Mono.error(throwable));
                    })
                    .subscribe(res -> {
                        log.info("response status2: {}", res.getStatusCode());
                        log.info("Header Location2: {}", res.getHeaders().get("Location"));
                        },
                        error -> log.error("Error happened: ", error));
        }
    }
    • createException(): request/response 정보를 포함한 WebClientResponseException을 생성합니다.

     

     

     

    [참고 정보]

    반응형

    'Spring Reactive Web Application > Spring WebFlux' 카테고리의 다른 글

    Reactive Streaming  (0) 2023.09.21
    예외 처리  (0) 2023.09.16
    R2dbcEntityTemplate  (0) 2023.09.10
    Spring Data R2DBC  (0) 2023.09.09
    함수형 엔드포인트(Functional Endpoint)  (0) 2023.08.26

    댓글

Designed by Tistory.