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 객체를 생성하고 기본 URL 및 헤더를 설정
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:9090") // 기본 URL 설정
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) // 기본 헤더 설정
.build();
// GET 요청을 보내고, 응답을 문자열로 반환
return webClient.get()
.uri("/api/v1/crud-api") // URI 확장 설정
.retrieve() // 요청에 대한 응답 추출
.bodyToMono(String.class) // 응답 본문을 문자열로 변환
.block(); // 논블로킹을 블로킹 방식으로 전환
}
위 코드는 http://localhost:9090/api/v1/crud-api URL로 get 요청을 보내는 예제 코드이다.
WebClient 생성 및 설정
- builder() 메서드 사용: WebClient 객체를 생성할 때 builder() 메서드를 사용하여 확장 가능한 설정을 적용할 수 있다.
- baseUrl(): 기본으로 사용할 URL을 설정한다.
- defaultHeader(): 기본 HTTP 헤더를 설정한다.
- build(): 설정을 완료하고 WebClient 객체를 생성한다.
확장 가능한 WebClient 설정 메서드
- defaultHeader(): 기본 HTTP 헤더를 설정한다.
- defaultCookie(): 기본 쿠키를 설정한다.
- defaultUriVariables(): 기본 URI 변수 확장값을 설정한다.
- filter(): 요청에 대해 필터를 설정한다.
WebClient의 HTTP 메서드와 URI 설정
- WebClient의 HTTP 메서드: WebClient는 HTTP 요청을 보낼 때 get(), post(), put(), delete() 등 다양한 HTTP 메서드를 지원한다.
- 예: webClient.get(), webClient.post() 등을 통해 HTTP GET 또는 POST 요청을 보낼 수 있다.
- URI 확장: uri() 메서드를 사용하여 기본 URL에 추가적인 경로와 파라미터를 지정할 수 있다.
- 예: uri("/api/v1/resource")는 기본 URL에 /api/v1/resource를 추가하여 완전한 URI를 만든다.
응답 처리와 리액티브 스트림의 Mono
- 응답 처리: retrieve() 메서드는 요청에 대한 응답을 처리하는데 사용된다. 이 메서드는 서버의 응답을 추출하고, 후속 처리를 위한 다양한 메서드를 연결할 수 있다.
- bodyToMono() 메서드를 사용하여 응답 본문을 특정 타입으로 변환한다. 이때 변환된 데이터는 Mono로 감싸지며, Mono는 리액티브 스트림에서 단일 결과를 나타낸다.
Mono와 Flux의 개념
Mono:Mono는 리액티브 스트림에서 단일 값을 비동기적으로 제공하는 발행자(Publisher)이다. 즉, Mono는 0 또는 1개의 데이터 요소를 비동기적으로 반환한다.예를 들어, 서버에서 하나의 데이터 항목을 가져오거나, 단일 작업의 결과를 반환할 때 사용된다.
Flux:Flux는 여러 값을 비동기적으로 제공하는 발행자이다. Flux는 0부터 N개의 데이터 요소를 스트리밍할 수 있다.예를 들어, 여러 개의 데이터를 연속적으로 전송해야 하는 경우(예: 데이터 스트리밍, 다수의 결과 반환) Flux를 사용한다.
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 |