WebClient
- 일반적으로 실제 운영환경에 적용되는 애플리케이션은 정식 버전으로 출시된 스프링 부트의 버전보다 낮은 경우가 많기 때문에 RestTemplate을 많이 사용하고있다. 하지만 최신 버전에서는 RestTemplate이 지원 중단되어 WebClient를 사용할 것을 권고하고 있다.
- Spring WebFlux는 HTTP 요청을 수행하는 클라이언트로 WebClient를 제공한다.
- WebClient는 리액터(Reactor) 기반으로 동작하는 API이다. 리액터 기반이므로 스레드와 동시성 문제를 벗어나 비동기 형식으로 사용할 수 있다.
WebClient 특징
- 논블로킹(Non-Blocking) I/O를 지원
- 리액티브 스트림(Reactive Streams)의 백 프레셔(Back Pressure)를 지원
- 적은 하드웨어 리소스로 동시성을 지원
- 함수형 API를 지원
- 동기, 비동기 상호작용을 지원
- 스트리밍을 지원
WebClient를 사용하려면 WebFlux 모듈에 대한 의존성을 추가해야 한다.
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Gradle
dependencies {
// ... other dependencies
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
WebFlux는 클라이언트와 서버 간 리액티브 애플리케이션 개발을 지원하기 위해 스프링 프레임워크 5에서 새롭게 추가된 모듈이다.
최근 프로그래밍 추세에 맞춰 스프링에도 리액티브 프로그래밍(Reactive Programming) 이 도입되면서 여러 동시적 기능이 제공되고 있다.
리액티브 프로그래밍(Reactive Programming)
* Google Gemini의 답변입니다.
리액티브 프로그래밍(Reactive Programming)은 이벤트 스트림과 논블로킹 데이터 처리를 기반으로 하는 프로그래밍 패러다임입니다. 기존의 명령형 프로그래밍과는 다른 사고 방식을 요구하며, 다음과 같은 특징을 지니고 있습니다.
- 데이터 스트림(Data Streams): 데이터를 연속적인 흐름(스트림)으로 취급합니다. 이벤트가 발생할 때마다 데이터를 처리하는 방식으로, 전체 데이터를 한꺼번에 로딩하지 않고 필요한 부분만 처리할 수 있습니다.
- 논블로킹(Non-Blocking): 시스템은 사용자 입력이나 이벤트를 기다리는 동안 계속 실행됩니다. 특정 작업이 완료될 때까지 프로그램 전체가 멈추는 일이 없으며, 효율적인 리소스 활용이 가능합니다.
- 백프레셔(Backpressure): 데이터 처리 속도가 데이터 생성 속도를 따라갈 수 없는 상황 (백프레셔) 에서 시스템이 데이터 손실이나 버퍼 오버플로우를 방지하는 메커니즘을 제공합니다.
리액티브 프로그래밍의 장점
- 높은 응답성(Responsiveness): 사용자 입력이나 이벤트에 빠르게 반응할 수 있습니다.
- 확장성(Scalability): 시스템 부하가 증가하더라도 효율적으로 처리할 수 있습니다.
- 복잡성 관리(Complexity Management): 비동 비속적인 특성으로 인해 복잡한 시스템을 관리하기 용이합니다.
WebClient 사용하기
WebClient를 생성하는 방법은 다음과 같이 크게 두 가지가 있다.
create()
메서드를 이용한 생성builder()
를 이용한 생성
WebClient를 활용한 GET 요청 예제
builder()를 활용한 WebClient 생성 방법
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();
}
위 코드는 http://localhost:9090/api/v1/crud-api URL로 get 요청을 보내는 예제 코드이다.builder()
를 통해 baseUrl()
메서드에서 기본 URL을 설정하고, defaultHeader()
메서드로 헤더의 값을 설정한다.
일반적으로 WebClient 객체를 이용할 때는 이처럼 WebClient 객체를 생성한 후 재사용하는 방식으로 구현한다.
builder()
를 사용할 경우 확장할 수 있는 메서드
- defaultHeader(): WebClient의 기본 헤더 설정
- defaultCookie(): WebClient의 기본 쿠키 설정
- defaultUriVariable(): WebClient의 기본 URI 확장값 설정
- filter(): WebClient에서 발생하는 요청에 대한 필터 설정
WebClient는 get()
, post()
, put()
, delete()
등의 HTTP 메서드로 설정할 수 있다.
그리고 URI를 확장하는 방법으로 uri()
메서드를 사용할 수 있습니다.
retrieve()
메서드는 요청에 대한 응답을 받았을 때 그 값을 추출한다.
이 메서드는 bodyToMono()
메서드를 통해 리턴 타입을 설정해서 문자열 객체를 받아오게 돼 있다.
참고로 Mono는 리액티브 스트림에 대한 선행 학습이 필요한 개념이며, Flux와 비교되는 개념이다.
Flux와 Mono는 리액티브 스트림에서 데이터를 제공하는 발행자 역할을 수행하는 Publisher의 구현체이다.
WebClient는 기본적으로 논블로킹(Non-Blocking)방식으로 동작하기 때문에 기존에 사용하던 코드의 구조를 블로킹 구조로 바꿔줄 필요가 있는 경우, block()
이라는 메서드를 추가해서 블로킹 형식으로 동작하게끔 설정할 수 있다.
한 번 빌드된 WebClient는 변경할 수 없으며, 다음과 같이 복사해서 사용할 수는 있다.
WebClient webClient = WebClient.create("http://localhost:9090");
WebClient clone = webClient.mutate().build();
create를 활용한 WebClient 생성 방법: PathVariable
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();
}
- 위 예제는 PathVariable 값을 추가해 요청을 보내는 예제이다.
- uri() 내부에 uriBuilder를 사용해 path를 설정하고 build() 메서드에 추가할 값을 넣는 것으로 pathVariable을 추가할 수 있다.
- 또한 bodyToMono()가 아닌 toEntity()를 사용하고 있는데, 이를 통해 ResponseEntity 타입으로 응답을 전달받을 수 있다.
create를 활용한 WebClient 생성 방법: Parameter
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();
}
- 위 코드는 GET 요청 시 쿼리 파라미터를 함께 전달하는 방법이다.
- uriBuilder를 사용하며, queryParam() 메서드를 사용해 전달하려는 값을 설정한다.
- 또한 exchangeToMono() 를 사용하는데 이는 exchange() 메서드가 지원 중단됐기 때문이며, exchangeToFlux()를 사용할 수도 있다.
- 그리고 clientResponse 결괏값으로 상태값에 따라 if문 분기를 만들어 상황에 따라 결괏값을 다르게 전달할 수 있다.
WebClient를 활용한 POST 요청
파라미터와 Header를 포함한 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();
}
- GET 요청 방식과 크게 다르지 않지만, HTTP Body값을 담는 방법과 커스텀 헤더를 추가하는 방법이 추가될 수 있다.
- post() 메서드를 통해 POST 메서드 통신을 정의하고, uri()는 uriBuilder로 path와 parameter를 설정한다.
그 후 bodyValue() 메서드를 통해 HTTP body값을 설정한다. HTTP body에는 일반적으로 데이터 객체(DTO, VO 등)를 파라미터로 전달한다.
커스텀 Header를 포함한 POST 요청
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();
위 코드는 header()
메서드를 사용해 헤더에 값을 추가한다. 일반적으로 임의로 추가한 헤더에는 외부 API를 사용하기 위해 인증된 토큰값을 담아 전달한다.
참고
- 스프링 부트 핵심 가이드 "스프링 부트를 활용한 애플리케이션 개발 실무" , 장정우, 2022
- Google Gemini
- https://github.com/wikibook/springboot`
'Development > Spring' 카테고리의 다른 글
[Spring] JWT와 Spring Security (1) | 2024.03.27 |
---|---|
[Spring] 인증과 권한 부여, Spring Security (0) | 2024.03.22 |
[Spring] 서버 간 통신하기: RestTemplate (0) | 2024.03.19 |
[Spring] 스프링 액추에이터(Actuator) (0) | 2024.03.10 |
[Spring] 스프링 부트의 예외 처리 방식 (0) | 2024.03.08 |