Spring Reactive 프로그래밍 – Spring WebFlux

Spring WebFlux는 Spring 5에서 도입된 새로운 모듈입니다. Spring WebFlux는 Spring 프레임워크에서 반응형 프로그래밍 모델로의 첫 번째 단계입니다.

Spring 반응형 프로그래밍

만약 반응형 프로그래밍 모델에 처음 접하신다면, 반응형 프로그래밍에 대해 배우기 위해 다음 기사들을 읽어보시기를 강력히 추천합니다.

만약 Spring 5에 처음 접하신다면, Spring 5 기능을 읽어보시기 바랍니다.

Spring WebFlux

Spring WebFlux는 Spring MVC 모듈의 대안입니다. Spring WebFlux는 이벤트 루프 실행 모델에 기반한 완전히 비동기 및 논블로킹 애플리케이션을 생성하는 데 사용됩니다. 아래 다이어그램은 Spring 공식 문서에서 Spring WebFlux와 Spring Web MVC를 비교한 것을 잘 보여줍니다. 비동기 반응형 모델에서 웹 애플리케이션 또는 REST 웹 서비스를 개발하려면 Spring WebFlux를 고려해볼 수 있습니다. Spring WebFlux는 Tomcat, Jetty, Servlet 3.1+ 컨테이너뿐만 아니라 Netty와 Undertow와 같은 비-Servlet 런타임에서도 지원됩니다. Spring WebFlux는 Project Reactor에 기반을 두고 있습니다. Project Reactor는 Reactive Streams 사양의 구현체입니다. Reactor는 두 가지 유형을 제공합니다:

  1. Mono: Publisher를 구현하고 0 또는 1개의 요소를 반환합니다.
  2. Flux: Publisher를 구현하고 N개의 요소를 반환합니다.

Spring WebFlux Hello World 예제

간단한 Spring WebFlux Hello World 응용 프로그램을 만들어 보겠습니다. 간단한 REST 웹 서비스를 만들고 Spring Boot를 사용하여 기본 Netty 서버에서 실행합니다. 최종 프로젝트 구조는 아래 이미지와 같습니다. 애플리케이션의 각 구성 요소를 하나씩 살펴보겠습니다.

Spring WebFlux Maven 종속성

<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.journaldev.spring</groupId>
  <artifactId>SpringWebflux</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>Spring WebFlux</name>
  <description>Spring WebFlux Example</description>
  
      <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <jdk.version>1.9</jdk.version>
    </properties>
    
  <parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.1.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webflux</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>io.projectreactor</groupId>
			<artifactId>reactor-test</artifactId>
			<scope>test</scope>
		</dependency>
    </dependencies>
	<repositories>
		<repository>
			<id>spring-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</repository>
		<repository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</repository>
	</repositories>
	<pluginRepositories>
		<pluginRepository>
			<id>spring-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</pluginRepository>
		<pluginRepository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</pluginRepository>
	</pluginRepositories>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.7.0</version>
                    <configuration>
                        <source>${jdk.version}</source>
                        <target>${jdk.version}</target>
                    </configuration>
                </plugin>
            </plugins>
    </pluginManagement>
    </build>
    
</project>

가장 중요한 종속성은 spring-boot-starter-webfluxspring-boot-starter-parent입니다. 일부 다른 종속성은 JUnit 테스트 케이스를 작성하기 위한 것입니다.

Spring WebFlux 핸들러

Spring WebFlux 핸들러 메서드는 요청을 처리하고 Mono 또는 Flux를 응답으로 반환합니다.

package com.journaldev.spring.component;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;

import reactor.core.publisher.Mono;

@Component
public class HelloWorldHandler {

	public Mono<ServerResponse> helloWorld(ServerRequest request) {
		return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN)
			.body(BodyInserters.fromObject("Hello World!"));
	}
}

반응형 컴포넌트인 MonoServerResponse 본문을 보유하고 있음에 주목하세요. 또한 반환 콘텐츠 유형, 응답 코드 및 본문을 설정하는 함수 체인을 살펴보세요.

Spring WebFlux 라우터

라우터 메서드는 애플리케이션의 경로를 정의하는 데 사용됩니다. 이러한 메서드는 또한 RouterFunction 객체를 반환하며 ServerResponse 본문을 유지합니다.

package com.journaldev.spring.component;

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 HelloWorldRouter {

	@Bean
	public RouterFunction<ServerResponse> routeHelloWorld(HelloWorldHandler helloWorldHandler) {

		return RouterFunctions.route(RequestPredicates.GET("/helloWorld")
                .and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), helloWorldHandler::helloWorld);
	}
}

따라서 /helloWorld에 대한 GET 메서드를 노출하고 클라이언트 호출은 일반 텍스트 응답을 수락해야 합니다.

Spring Boot 애플리케이션

간단한 WebFlux 애플리케이션을 Spring Boot로 구성해 보겠습니다.

package com.journaldev.spring;

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

@SpringBootApplication
public class Application {

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

위의 코드를 보면 Spring WebFlux와 관련된 내용이 없습니다. 하지만 우리가 spring-boot-starter-webflux 모듈의 종속성을 추가했기 때문에 Spring Boot가 애플리케이션을 Spring WebFlux로 구성할 것입니다.

Java 9 모듈 지원

우리의 애플리케이션은 Java 8에서 실행할 준비가 되었지만, Java 9를 사용하는 경우에는 module-info.java 클래스를 추가해야 합니다.

module com.journaldev.spring {
    requires reactor.core;
    requires spring.web;
    requires spring.beans;
    requires spring.context;
    requires spring.webflux;
    requires spring.boot;
    requires spring.boot.autoconfigure;
    exports com.journaldev.spring;
}

Spring WebFlux Spring Boot 앱 실행

이클립스에서 Spring 지원이 있다면 위의 클래스를 Spring Boot 앱으로 실행할 수 있습니다.
명령 줄을 사용하려면 터미널을 열고 프로젝트 소스 디렉토리에서 명령어 mvn spring-boot:run을 실행하십시오. 앱이 실행되면 다음 로그 메시지를 확인하여 앱이 정상인지 확인할 수 있습니다. 추가 경로 및 기능을 추가하여이 간단한 앱을 확장 할 때도 도움이 됩니다.

2018-05-07 15:01:47.893  INFO 25158 --- [           main] o.s.w.r.f.s.s.RouterFunctionMapping      : Mapped ((GET && /helloWorld) && Accept: [text/plain]) -> com.journaldev.spring.component.HelloWorldRouter$$Lambda$501/704766954@6eeb5d56
2018-05-07 15:01:48.495  INFO 25158 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext     : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2018-05-07 15:01:48.495  INFO 25158 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080
2018-05-07 15:01:48.501  INFO 25158 --- [           main] com.journaldev.spring.Application        : Started Application in 1.86 seconds (JVM running for 5.542)

로그에서 앱이 8080 포트의 Netty 서버에서 실행 중임을 알 수 있습니다. 이제 애플리케이션을 테스트 해 보겠습니다.

Spring WebFlux App 테스트

다양한 방법으로 앱을 테스트 할 수 있습니다.

  1. CURL 명령 사용

    $ curl https://localhost:8080/helloWorld
    Hello World!
    $ 
    
  2. 브라우저에서 URL 시작

  3. Spring 5에서 WebTestClient 사용하기 여기에는 Spring 5의 반응형 웹에서 WebTestClient를 사용하여 Rest 웹 서비스를 테스트하기 위한 JUnit 테스트 프로그램이 있습니다.

    package com.journaldev.spring;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.http.MediaType;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.test.web.reactive.server.WebTestClient;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    public class SpringWebFluxTest {
    
    	@Autowired
    	private WebTestClient webTestClient;
    
    	
    	@Test
    	public void testHelloWorld() {
    		webTestClient
    		.get().uri("/helloWorld") // GET 메서드와 URI
    		.accept(MediaType.TEXT_PLAIN) // ACCEPT-Content 설정
    		.exchange() // 응답에 액세스
    		.expectStatus().isOk() // 응답이 OK인지 확인
    		.expectBody(String.class).isEqualTo("Hello World!"); // 응답 유형과 메시지 확인
    	}
    
    }
    

    JUnit 테스트 케이스로 실행하면 성공적으로 통과해야합니다.

  4. Spring Web Reactive에서 WebClient를 사용하면, REST 웹 서비스를 호출하는 데에도 WebClient를 사용할 수 있습니다.

    package com.journaldev.spring.client;
    
    import org.springframework.http.MediaType;
    import org.springframework.web.reactive.function.client.ClientResponse;
    import org.springframework.web.reactive.function.client.WebClient;
    
    import reactor.core.publisher.Mono;
    
    public class HelloWorldWebClient {
    
    	public static void main(String args[]) {
    		WebClient client = WebClient.create("https://localhost:8080");
    
    		Mono<ClientResponse> result = client.get()
    				.uri("/helloWorld")
    				.accept(MediaType.TEXT_PLAIN)
    				.exchange();
    
    			System.out.println("Result = " + result.flatMap(res -> res.bodyToMono(String.class)).block());
    	}
    	
    }
    

    간단한 자바 애플리케이션으로 실행하면 디버그 메시지와 함께 올바른 출력을 볼 수 있습니다.

요약

이 글에서 우리는 Spring WebFlux에 대해 알아보고 hello world 반응형 Restful 웹 서비스를 구축하는 방법을 배웠습니다. Spring과 같은 인기있는 프레임워크가 반응형 프로그래밍 모델을 지지하고 있다는 것을 알 수 있어 좋습니다. 그러나 모든 종속성이 반응형이 아니고 논블로킹이 아니라면 애플리케이션도 실제로 반응형이 아닙니다. 예를 들어, 관계형 데이터베이스 공급업체는 반응형 드라이버를 갖고 있지 않습니다. 왜냐하면 JDBC에 의존하기 때문입니다. 따라서 하이버네이트 API도 비반응형입니다. 따라서 관계형 데이터베이스를 사용하고 있다면 아직 완전히 반응형 애플리케이션을 구축할 수 없습니다. 그러나 빨리 바뀔 것이라고 희망합니다.

프로젝트 코드는 제 GitHub 저장소에서 다운로드할 수 있습니다.

참고: 공식 문서

Source:
https://www.digitalocean.com/community/tutorials/spring-webflux-reactive-programming