[Spring Boot Webflux] 활용하기
1. 주요 구성요소 확인
Spring Webflux는 Controller와 Router라는 두 가지 주요 컴포넌트를 제공한다.
1) Controller vs Router
Controller 방식은 Spring MVC와 유사한 방식으로 HTTP 요청을 처리하는 데 사용되며, 이 컴포넌트는 주로 RESTful API 엔드포인트를 구현하는 데 사용된다. Router 방식은 HTTP 요청을 처리하는 데 사용되는 라우터 및 핸들러 함수를 정의하는 데 사용되며 비동기적으로 실행되기 때문에 스레드가 블로킹 되지 않아 더 높은 처리량을 달성할 수 있다. 결론적으로는 Controller 방식은 동기식 요청처리에 적합하며, Router는 비동기식 요청처리에 적합하다.
분류 | Router | Controller |
설정 방식 | Java DSL을 사용하여 라우팅 규칙을 정의한다. | Annotation을 사용하여 URL 경로를 정의한다. |
요청 매핑 | 요청을 처리하는 메서드를 선택하기 위해 RequestPredicate를 사용한다. | URL 경로에 @RequestMapping 어노테이션을 사용한다. |
요청 처리 | HandlerFunction 또는 WebFlux.fn.ServerResponse 타입의 객체를 반환한다. | @RestController 어노테이션을 사용하여 해당 클래스의 모든 메서드가 HTTP 응답을 반환한다는 것을 나타낸다. |
예외 처리 | RouterFunction에서 예외 처리를 할 수 있다. | @ControllerAdvice 어노테이션을 사용하여 예외 처리 핸들러를 등록할 수 있다. |
2) Router Function
Router Function은 Handler Function을 호출하는 역할을 한다. 이를 통해 HTTP 요청을 받고 적절한 Handler Function으로 라우팅할 수 있다. Router Function은 RouterFunctions 클래스를 사용하여 생성할 수 있으며 다양한 메소드를 사용하여 HTTP 요청에 따라 적잘한 Handler Function으로 라우팅할 수 있다. Router Function을 사용하면 라우팅 규칙을 보다 직관적이고 유연하게 제어할 수 있다.
@Bean
public RouterFunction<ServerResponse> routerFunction(ServiceHandler handler) {
return RouterFunctions.route(RequestPredicates.GET("/service/{id}"), handler::getService)
.andRoute(RequestPredicates.GET("/services"), handler::getServices)
.andRoute(RequestPredicates.POST("/service"), handler::createService)
.andRoute(RequestPredicates.PUT("/service/{id}"), handler::updateService)
.andRoute(RequestPredicates.DELETE("/service/{id}"), handler::deleteService);
3) Handler Function
WebFlux에서 HTTP 요청을 처리하기 위해 호출하는 메서드이며 단일 입력과 단일 출력을 가진다.
Mono<ServerResponse> handle(ServerRequest request);
2. 환경 구성
1) 개발환경
일반적으로 WebFlux는 RDBMS를 사용하는 것이 아닌 비동기적이고 논 블로킹 방식으로 동작하는 MongoDB와 함께 사용한다. Mybatis와 ORM은 동기적인 블로킹 방식으로 동작하기 WebFlux와 함께 사용하면 전체적인 성능에 악영향을 미칠 수 있다.
2) gradle.build 라이브러리 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux' // Spring Boot Webflux
}
3) Handler Function 구성
- Hello 메시지를 반환하는 단일 반환(Mono) 값의 서비스를 구성
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
@Configuration
public class CodeRouter {
@Bean
public RouterFunction<ServerResponse> routerFunction(CodeHandler handler) {
return RouterFunctions.route(RequestPredicates.GET("/hello"), handler::hello)
.andRoute(RequestPredicates.POST("/api/v2/hello"), handler::hello);
}
}
4) Router Function 구성
- 구성한 Handler Function 함수의 서비스에 대해 Router를 통해 엔드포인트를 연결
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
@Configuration
public class CodeRouter {
@Bean
public RouterFunction<ServerResponse> route(CodeHandler codeHandler) {
return RouterFunctions.route(
RequestPredicates.GET("/api/v2/hello").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), codeHandler::hello);
}
}
3. WebClient 이용 방법
1) WebClient 인스턴스 생성
분류 | 메서드 | 설명 |
인스턴스 생성 | create() | 지정된 URL을 기반으로 WebClient 인스턴스를 만듭니다. |
WebClient client = WebClient.create("<http://localhost:8000>");
@Bean
public WebClient webClient() {
return WebClient.create("<http://localhost:8080>");
}
2). WebClient HTTP 메서드, URI, 전송 데이터를 지정
분류 | 메서드 | 설명 |
HTTP 메소드 | get() | GET 요청을 정의합니다. |
HTTP 메소드 | post() | POST 요청을 정의합니다. |
HTTP 메소드 | put() | PUT 요청을 정의합니다. |
HTTP 메소드 | delete() | DELETE 요청을 정의합니다. |
HTTP 메소드 | head() | HEAD 요청을 정의합니다. |
HTTP 메소드 | options() | OPTIONS 요청을 정의합니다. |
HTTP 메소드 | patch() | PATCH 요청을 정의합니다. |
HTTP 메소드 | method() | 지정된 HTTP 메소드로 요청을 정의합니다. |
URI | uri() | 요청할 엔드포인트 URL을 지정합니다. |
요청 바디 | body() | 요청 바디에 담을 데이터를 설정합니다. |
요청 바디 | bodyValue() | 요청 바디에 담을 데이터를 설정합니다. |
응답 결과 | bodyToMono() | 응답 결과를 Mono로 래핑합니다. |
응답 결과 | bodyToFlux() | 응답 결과를 Flux로 래핑합니다. |
응답 결과 | retrieve() | 요청 결과를 가져오는 ResponseSpec 인스턴스를 반환합니다. |
응답 결과 | exchange() | 요청 결과를 ClientResponse 인스턴스로 가져옵니다. |
응답 결과 | exchangeToMono() | 요청 결과를 Mono로 래핑합니다. |
응답 결과 | exchangeToFlux() | 요청 결과를 Flux로 래핑합니다. |
응답 결과 | exchangeToBodilessEntity() | 응답 결과를 ResponseEntity<Void>로 래핑합니다. |
응답 결과 | toEntity() | 응답 결과를 ResponseEntity로 래핑합니다. |
webClient()
.post()
.uri("/api/v1/code/code")
.bodyValue(codeDto)
.retrieve();
3) WebClient의 응답 결과를 지정
분류 | 메서드 | 설명 |
응답 결과 | bodyToMono() | 응답 결과를 Mono로 래핑합니다. |
응답 결과 | bodyToFlux() | 응답 결과를 Flux로 래핑합니다. |
응답 결과 | retrieve() | 요청 결과를 가져오는 ResponseSpec 인스턴스를 반환합니다. |
응답 결과 | exchange() | 요청 결과를 ClientResponse 인스턴스로 가져옵니다. |
응답 결과 | exchangeToMono() | 요청 결과를 Mono로 래핑합니다. |
응답 결과 | exchangeToFlux() | 요청 결과를 Flux로 래핑합니다. |
응답 결과 | exchangeToBodilessEntity() | 응답 결과를 ResponseEntity<Void>로 래핑합니다. |
응답 결과 | toEntity() | 응답 결과를 ResponseEntity로 래핑합니다. |
동기 메소드 | block() | Publisher가 갖는 값을 동기적으로 반환하는 메소드입니다. 해당 Publisher가 값을 방출할 때까지 대기한 후 값을 반환합니다. |
4) 종합 결과
- 인스턴스 최초 생성
- 인스턴스를 기반으로 HTTP 메서드의 POST 방식 사용
- 최종 전달하려는 http://localhost:8000/api/v1/code/code로 POST의 Body 데이터를 담아 전송
- 전송 후 반환값으로 CodeDto.class 객체의 값으로 반환받기
WebClient client = WebClient.create("<http://localhost:8000>");
ResponseEntity<ApiResponseWrapper> result = client
.post()
.uri("/api/v1/code/code")
.bodyValue(codeDto)
.retrieve()
.toEntity(CodeDto.class)
.block();
Map<String, Object> resultMap = (Map<String, Object>) result.getBody().getResult();
log.info("resultJsonObj :: " + resultMap);
5) Builder 메서드
Builder 메서드를 사용하면 WebClient 인스턴스를 더욱 유연하게 초기화할 수 있다.
분류 | 메서드 | 설명 |
일반적인 설정 | baseUrl(String baseUrl) | WebClient 인스턴스의 기본 URL을 설정합니다. |
일반적인 설정 | codecs(Consumer<CodecConfigurer> configurer) | 요청 및 응답 바디에 사용할 코덱을 구성합니다. |
일반적인 설정 | defaultCookie(String name, String value) | 요청에 대한 기본 쿠키 값을 설정합니다. |
일반적인 설정 | defaultHeader(String headerName, String... headerValues) | 요청에 대한 기본 헤더 값을 설정합니다. |
일반적인 설정 | defaultUriVariables(Map<String, ?> defaultUriVariables) | 확장된 URI 템플릿에서 사용할 기본 URI 변수를 설정합니다. |
일반적인 설정 | defaultUriVariables(Consumer<UriBuilderFactory> builderConsumer) | UriBuilderFactory를 사용하여 확장된 URI 템플릿에서 사용할 기본 URI 변수를 설정합니다. |
일반적인 설정 | exchangeStrategies(ExchangeStrategies strategies) | 요청 및 응답 바디에 사용할 교환 전략을 구성합니다. |
일반적인 설정 | filter(ExchangeFilterFunction filterFunction) | ExchangeFilterFunction을 WebClient 인스턴스에 추가합니다. |
일반적인 설정 | filters(Consumer<List<ExchangeFilterFunction>> filters) | 여러 ExchangeFilterFunction을 WebClient 인스턴스에 추가합니다. |
요청 설정 | accept(MediaType... acceptableMediaTypes) | 응답에 대한 허용 가능한 미디어 유형을 설정합니다. |
요청 설정 | attribute(String name, Object value) | 요청에 속성을 추가합니다. |
요청 설정 | body(BodyInserter<?, ? super ClientHttpRequest> inserter) | 요청 바디를 설정합니다. |
요청 설정 | body(BodyInserter<?, ? super ClientHttpRequest> inserter, Class<?> elementClass) | 요청 바디를 설정하고 요소의 클래스를 지정합니다. |
요청 설정 | cookies(Consumer<MultiValueMap<String, HttpCookie>> cookiesConsumer) | 요청에 쿠키를 추가합니다. |
요청 설정 | header(String headerName, String... headerValues) | 요청에 헤더를 추가합니다. |
요청 설정 | headers(Consumer<HttpHeaders> headersConsumer) | 요청에 여러 헤더를 추가합니다. |
요청 설정 | ifModifiedSince(ZonedDateTime ifModifiedSince) | 요청의 "If-Modified-Since" 헤더를 설정합니다. |
요청 설정 | ifNoneMatch(String ifNoneMatch) | 요청의 "If-None-Match" 헤더를 설정합니다. |
요청 설정 | uri(URI uri) | 요청 URI를 설정합니다. |
요청 설정 | uri(String uri, Object... uriVariables) | 포맷된 문자열과 URI 변수를 사용하여 요청 URI를 설정합니다. |
HTTP 메소드 | get() | GET 요청을 설정합니다. |
HTTP 메소드 | post() | POST 요청을 설정합니다. |
HTTP 메소드 | put() | PUT 요청을 설정합니다. |
HTTP 메소드 | delete() | DELETE 요청을 설정합니다. |
HTTP 메소드 | head() | HEAD 요청을 설정합니다. |
HTTP 메소드 | options() | OPTIONS 요청을 설정합니다. |
HTTP 메소드 | patch() | PATCH 요청을 설정합니다. |
HTTP 메소드 | method() | 지정된 HTTP 메소드로 요청을 설정합니다. |
WebClient client = WebClient.builder()
.baseUrl("<http://localhost:8000>")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer my-token")
.build();
ResponseEntity<ApiResponseWrapper> result = client
.post()
.uri("/api/v1/code/code")
.bodyValue(codeDto)
.retrieve()
.toEntity(CodeDto.class)
.block();
Map<String, Object> resultMap = (Map<String, Object>) result.getBody().getResult();
log.info("resultJsonObj :: " + resultMap);
- 참고자료