[Spring Boot] REST API 호출 (WebClient)

개요

여기에서는 Spring Boot + WebClient + Thymeleaf를 사용하여, 이전 글 [Spring Boot] REST API 개발 (MyBatis,HSQLDB) 에서 예제로 만든 CRUD REST API를 호출하는 방법을 보여준다.


개발환경은 다음과 같다:

● Spring Tool Suite 3 (Version: 3.9.11)

● Spring Boot Version : 2.7.13

● Java : JDK 8


※ 관련글 목록: http://yellow.kr/lifeView.jsp?s=spring



예제 애플리케이션 설명

만들려는 예제 Books 관리 시스템은 REST API를 호출하여 다음과 같은 기능을 구현한다:

● 책 정보 리스트

● 책 정보 등록

● 책 정보 조회

● 책 정보 수정

● 책 정보 삭제


예제 Application의 Flow는 다음과 같다. 여기에서는 Client 부분에 해당한다(8080 port 사용).


‘Spring Boot REST 애플리케이션(8081 port 사용)’은 [Spring Boot] REST API 개발 (MyBatis,HSQLDB) 글을 참조

그리고 CORS(cross-origin 리소스 공유) 오류가 나타나는 것을 해결하기 위해 ‘Spring Boot REST 애플리케이션’의 Book Controller에 @CrossOrigin 어노테이션을 사용하였다.



WebClient

WebClient는 Spring WebFlux에서 제공하는 비동기(non-blocking) HTTP 클라이언트이다. WebClient는 리액티브 스트림을 활용하여 비동기 작업을 처리하며, 네트워크 호출을 위한 요청과 응답을 비동기적으로 처리하는 데에 적합하다.

WebClient의 주요 특징과 장점은 다음과 같다:

  1. 비동기(non-blocking) 처리: WebClient는 비동기 방식으로 동작하며, 요청을 보내고 응답을 기다리는 동안 다른 작업을 수행할 수 있다. 이는 더 많은 동시 요청을 처리하고 응답 대기 시간을 최소화하는 데 도움이 된다.
  2. 리액티브 스트림 지원: WebClient는 리액티브 스트림(Flux와 Mono)을 지원하여 데이터의 스트리밍 처리를 쉽게 할 수 있다. 이는 대량의 데이터나 스트리밍 API와 통신할 때 유용하다.
  3. 작은 메모리 풋프린트: WebClient는 메모리 사용량을 최소화하는 경량화된 디자인을 가지고 있다. 이는 서버의 확장성과 성능에 도움을 줄 수 있다.
  4. 다양한 기능 지원: WebClient는 다양한 기능을 제공한다. 헤더 설정, 쿠키 관리, 요청 본문 설정, 인증 등 다양한 요구 사항에 대응할 수 있다.


그리고 RestTemplate과 WebClient의 비교는 다음과 같다:

◎ RestTemplate은 Spring MVC 기반의 동기식(synchronous) HTTP 클라이언트이며, WebClient는 Spring WebFlux 기반의 비동기식(asynchronous) HTTP 클라이언트이다.

◎ RestTemplate은 기본적으로 Java.net 패키지를 사용하여 HTTP 요청을 처리하지만, WebClient는 Java 8에서 소개된 리액티브 스트림을 활용하여 비동기 작업을 처리한다.

◎ WebClient는 비동기 방식으로 동작하므로 더 많은 동시 요청을 처리하고 응답 대기 시간을 최소화할 수 있다. 따라서 높은 성능과 확장성을 요구하는 시스템에서 유리하다.

◎ WebClient는 리액티브 스트림을 지원하여 데이터의 스트리밍 처리에 용이하다. RestTemplate은 스트리밍 처리를 지원하지 않는다.

◎ RestTemplate은 더 많은 편의 기능을 제공하며, 레거시 시스템과의 통합에 적합하다. WebClient는 더 작은 메모리 풋프린트와 더 높은 확장성을 제공하며, 리액티브 스트림과의 통합에 적합하다.

따라서 성능, 확장성, 비동기 작업 처리, 리액티브 스트림과의 통합 등의 요구 사항에 따라 RestTemplate과 WebClient 중에서 선택할 수 있습니다. Spring 5부터는 WebClient를 권장하며, 특히 리액티브 스트림과 비동기 작업 처리에 더 적합한 WebClient를 사용하는 것이 좋다.



Spring Boot project 생성

● eclips에서 Spring Boot project를 생성한다.

File > New > Project… > Spring Boot > Spring Starter Project

Name, Group, Package에 적당한 내용을 입력하고 [Next] 클릭


● 필요한 Dependency들을 선택한다.

Spring Web, Spring Reactive Web, Thymeleaf를 선택한다.

[Finish] 클릭


● Project 생성 됨

yellow-webclient 라는 이름으로 Project가 생성되었고, YellowWebclientApplication.java 가 생성되었다.

package com.yellow.webclient;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class YellowWebclientApplication {

	public static void main(String[] args) {
		SpringApplication.run(YellowWebclientApplication.class, args);
	}

}


예제 애플리케이션이 완성되면 Project Structure은 다음과 같다.



Maven Dependencies

pom.xml을 보면 다음과 같은 dependency를 확인할 수 있다.

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



Model

● Book.java

package com.yellow.resttemplate;

public class Book {
	
	private String bookId;
	private String title;
	private String author;
	private String publisher;
	private String releaseDate;
	private String isbn;
	
	public Book() {}
	
	public Book(String bookId, String title, String author, String publisher, String releaseDate, String isbn) {
		this.setBookId(bookId);
		this.setTitle(title);
		this.setAuthor(author);
		this.setPublisher(publisher);
		this.setReleaseDate(releaseDate);
		this.setIsbn(isbn);
	}
	
	public String getBookId() {
		return bookId;
	}
	public void setBookId(String bookId) {
		this.bookId = bookId;
	}
	
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	
	public String getAuthor() {
		return author;
	}
	public void setAuthor(String author) {
		this.author = author;
	}
	
	public String getPublisher() {
		return publisher;
	}
	public void setPublisher(String publisher) {
		this.publisher = publisher;
	}
	
	public String getReleaseDate() {
		return releaseDate;
	}
	public void setReleaseDate(String releaseDate) {
		this.releaseDate = releaseDate;
	}

	public String getIsbn() {
		return isbn;
	}
	public void setIsbn(String isbn) {
		this.isbn = isbn;
	}
	
	@Override
	public String toString() {
		return "bookId:" + bookId + ",title:" + title + ",author:" + author 
				+ ",publisher:" + publisher + ",releaseDate:" + releaseDate + ",isbn:" + isbn;
	}
}



Contoller, View(Thymeleaf)

● BookController.java

package com.yellow.webclient;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Controller
public class BookController {

    private final WebClient webClient;
    private final String BASE_URL = "http://localhost:8081/api/books";

    public BookController(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder.baseUrl(BASE_URL).build();
    }

    @GetMapping("/")
    public String index() {
        return "index";
    }

    @GetMapping("/listBook")
    public Mono<String> viewBookList(Model model) {
        return webClient.get()
                .retrieve()
                .bodyToFlux(Book.class)
                .collectList()
                .doOnSuccess(books -> model.addAttribute("allBooks", books))
                .thenReturn("listBook");
    }

    @GetMapping("/viewBook/{id}")
    public Mono<String> viewBook(@PathVariable("id") String id, Model model) {
        return webClient.get()
                .uri("/{id}", id)
                .retrieve()
                .bodyToMono(Book.class)
                .doOnSuccess(book -> model.addAttribute("book", book))
                .thenReturn("viewBook");
    }

    @GetMapping("/addViewBook")
    public String addViewBook() {
        return "addViewBook";
    }

    @PostMapping("/addBook")
    public Mono<String> addBook(@ModelAttribute Book book) {
        return webClient.post()
                .body(Mono.just(book), Book.class)
                .retrieve()
                .bodyToMono(Book.class)
                .thenReturn("redirect:/listBook");
    }

    @GetMapping("/updateViewBook/{id}")
    public Mono<String> updateViewBook(@PathVariable("id") String id, Model model) {
        return webClient.get()
                .uri("/{id}", id)
                .retrieve()
                .bodyToMono(Book.class)
                .doOnSuccess(book -> model.addAttribute("book", book))
                .thenReturn("updateViewBook");
    }

    @PostMapping("/updateBook/{id}")
    public Mono<String> updateBook(@PathVariable("id") String id, @ModelAttribute Book book) {
        return webClient.put()
                .uri("/{id}", id)
                .body(Mono.just(book), Book.class)
                .retrieve()
                .toBodilessEntity()
                .thenReturn("redirect:/listBook");
    }

    @GetMapping("/deleteBook/{id}")
    public Mono<String> deleteBook(@PathVariable("id") String id) {
        return webClient.delete()
                .uri("/{id}", id)
                .retrieve()
                .toBodilessEntity()
                .thenReturn("redirect:/listBook");
    }
}


● index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Main Page</title>
</head>
<body>
	<h1>Main Page</h1>      
    <p><a href="/listBook">List Book</a></p>      
    <p><a href="/addViewBook">Register Book</a></p>
</body>
</html>


● listBook.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Book List</title>
</head>
<body>
<h1>Book List</h1>
<br/>
<div>
   <table border="1">
      <tr>
         <th>ID</th>
         <th>제목</th>
         <th>저자</th>
         <th>Action</th>
      </tr>
      <tr th:each ="book : ${allBooks}">
         <td><a th:href="@{/viewBook/{id}(id=${book.bookId})}"  th:text="${book.bookId}"></a></td>
         <td th:text="${book.title}"></td>
         <td th:text="${book.author}"></td>
         <td>
         	<a th:href="@{/updateViewBook/{id}(id=${book.bookId})}" >수정</a>, 
  	        <a th:href="@{/deleteBook/{id}(id=${book.bookId})}" >삭제</a>
         </td>
      </tr>
   </table>
</div>
<br/><br/>
<a href="/">Main Page</a>
</body>
</html>


● viewBook.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>View Book</title>
</head>
<body>
<h1>Book Detail View</h1>
<div>
	<span>Book ID : </span>
	<span th:text="${book.bookId}"></span>
</div>
<div>
	<span>제 목 : </span>
   	<span th:text="${book.title}"></span>
</div>
<div>
   	<span>저 자 : </span>
   	<span th:text="${book.author}"></span>
</div>
<div>
   	<span>출판사 : </span>
   	<span th:text="${book.publisher}"></span>
</div>
<div>
   	<span>출판일 : </span>
	<span th:text="${book.releaseDate}"></span>
</div>
<div>
   	<span>ISBN : </span>
   	<span th:text="${book.isbn}"></span>
</div>
<br>
<br>
<div>
	<a th:href="@{/updateViewBook/{id}(id=${book.bookId})}" >수정</a> /  
  	<a th:href="@{/deleteBook/{id}(id=${book.bookId})}" >삭제</a>
</div>
<p><br></p>
<p><a href="/listBook">List Book</a></p>
</body>
</html>


● addViewBook.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Add a New Book</title>
</head>
<body>
<h1>Add a New Book</h1>
<form method="POST" action="/addBook">
<div>
	<label>Book ID</label>
	<input type="text" id="bookId" name="bookId">
</div>
<div>
	<label>제 목  </label>
	<input type="text" id="title" name="title">
</div>
<div>
	<label>저 자  </label>
	<input type="text" id="author" name="author">
</div>
<div>
	<label>출판사</label>
	<input type="text" id="publisher" name="publisher">
</div>
<div>
	<label>출판일</label>
	<input type="text" id="releaseDate" name="releaseDate">
</div>
<div>
	<label>ISBN </label>
	<input type="text" id="isbn" name="isbn">	
</div>
<div>
	<p><br></p>
	<input type="submit" id="addNew" value="Submit">
</div>
</form>
<p><br></p>
<p><a href="/">Main</a></p>
</body>
</html>


● updateViewBook.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Update a Book</title>
</head>
<body>
<h1>Update a Book</h1>
<form th:action="@{/updateBook/{id}(id=${book.bookId})}" method="POST" >
<div>
	<label>Book ID :</label>
	<span th:text="${book.bookId}"></span>
	<input type="hidden" th:field="*{book.bookId}" >
</div>
<div>
	<label>제 목  </label>
	<input type="text" th:field="*{book.title}" >
</div>
<div>
	<label>저 자  </label>
	<input type="text" th:field="*{book.author}" >
</div>
<div>
	<label>출판사</label>
	<input type="text" th:field="*{book.publisher}" >
</div>
<div>
	<label>출판일</label>
	<input type="text" th:field="*{book.releaseDate}" >
</div>
<div>
	<label>ISBN </label>
	<input type="text" th:field="*{book.isbn}" >	
</div>
<div>
	<p><br></p>
	<input type="submit" id="addNew" value="Submit">
</div>
</form>
<p><br></p>
<p><a href="/">Main</a></p>
</body>
</html>



Application 실행

Project를 선택한 후, Run AS > Spring Boot App 를 하면 내장 Tomcat 서버에 배포된다. 브라우저에서 다음의 URL을 실행한다.​​

● http://localhost:8080/


● http://localhost:8080/listBook


● http://localhost:8080/addViewBook


● http://localhost:8080/listBook


● http://localhost:8080/viewBook/1004


● http://localhost:8080/updateViewBook/1004


● http://localhost:8080/listBook


​등등…


[Spring Boot] REST API 호출 (WebClient)

2 thoughts on “[Spring Boot] REST API 호출 (WebClient)

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.