Java/Spring Boot
[Java] 반응형 프로그래밍(Reactive Programming), Spring WebFlux를 활용하여 구현하기-1 : 전반적인 흐름
adjh54
2024. 12. 22. 14:34
반응형
해당 글에서는 반응형 프로그래밍과 이를 구현하기 위한 Spring WebFlux의 이해를 돕기 위해 작성한 글입니다.
💡 [참고] Spring WebFlux 관련 글에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
분류 | 링크 |
Spring Boot Webflux 이해하기 -1 : 흐름 및 주요 특징 이해 | https://adjh54.tistory.com/232 |
Spring Boot Webflux 이해하기 -2 : 활용하기 | https://adjh54.tistory.com/233 |
반응형 프로그래밍(Reactive Programming), Spring WebFlux를 활용하여 구현하기-1 : 전반적인 흐름 | https://adjh54.tistory.com/627 |
Spring Boot WebFlux 활용 Github | https://github.com/adjh54ir/blog-codes/tree/main/spring-boot-webflux |
1) 프로그래밍 패러다임(Programming Paradigm)
💡 프로그래밍 패러다임(Programming Paradigm)
- 프로그래머에게 프로그래밍의 관점을 갖게 해주는 역할을 하는 개발 방법론을 의미합니다.
- 이러한 개발 방법론은 크게 명령형 프로그래밍(Imperative Programming)과 선언형 프로그래밍(Declarative Programming)으로 나뉩니다.
- 이 중에서 대중적으로 많이 사용되는 명령형 프로그래밍 방식과 가장 현대적인 개발 방법인 ‘선언형’ 프로그래밍 중 ‘함수형 프로그래밍’에 대해 알아봅니다.
💡 아래의 이미지는 Google 검색 중 프로그래밍 패러다임에 대해 검색을 하면 가장 많이 나오는 이미지인데, ‘프로그래밍 패러다임’을 진화론에 빗대어 표현한 이미지입니다.
- 기계어(MACHINE) → 어셈블리(ASSEMBLY) → 절차적(PROCEDURAL) → 객체 지향(OBJECT ORIENTED) → 함수형(FUNTIONAL)
- 이러한 진화는 프로그래밍의 추상화 수준이 높아지고 코드의 재사용성과 유지보수성이 향상되는 방향으로 발전해 왔음을 보여줍니다.
2) 명령형 프로그래밍(Imperative Programming)
💡 명령형 프로그래밍(Imperative Programming)
- 프로그램의 상태를 변경하는 문장들을 순차적으로 작성하는 프로그래밍 패러다임을 의미합니다.
- 프로그램이 어떻게 실행되어야 하는지 단계별로 명시적으로 지시하고 상태 변경과 데이터 변경이 명시적으로 이루어지며, 순차적으로 실행 흐름을 가지며, 각 단계가 이전 단계에 의존적 방식입니다.
- 기존의 Spring MVC를 이용하여서 명령형 프로그래밍이 구현되어 왔습니다.
💡 아래와 같이 명령형 프로그래밍을 나타내는 대표적인 for문입니다.
- for문은 for(A;B;C;)에서 A는 조건화 식으로 반복문이 시작되기 전의 초기값을 지정하고, B는 true라는 조건식이 만족될 때까지 반복됩니다, C는 증감식으로 하나의 반복이 끝날 때마다 변화가 발생합니다.
- 이를 통해서 초기값에서 시작하여 순차적으로 실행 흐름을 가지고 최종결과를 얻어내는 명령형 프로그래밍의 대표적인 for문입니다.
1. 명령형 프로그래밍(Spring MVC)과 반응형 프로그래밍(Spring WebFlux) 특징 비교
💡명령형 프로그래밍(Spring MVC)과 반응형 프로그래밍(Spring WebFlux) 특징 비교
1. 명령형 프로그래밍(Spring MVC) 특징
- Imperative logic, simple to write and debug: 명령형 로직으로, 코드를 순차적으로 작성하고 디버깅하기 쉬운 특성을 가집니다.
- JDBC, JPA, blocking deps: 블로킹 방식의 데이터베이스 접근 방식을 사용합니다.
2. 반응형 프로그래밍(Spring WebFlux) 특징
- Functional endpoints: 함수형 프로그래밍 스타일의 엔드포인트를 제공합니다.
- Event loop concurrent model: 이벤트 루프를 사용한 동시성 모델을 채택하여 적은 수의 스레드로 많은 동시 연결을 처리할 수 있습니다.
- Netty: Spring WebFlux의 기본 웹 서버로, 비동기식 이벤트 기반 서버를 구성하는 데 사용됩니다.
3. 공통 특징
- @Controller: 두 프레임워크 모두 컨트롤러 애노테이션을 사용할 수 있습니다.
- Reactive clients: WebClient와 같은 반응형 클라이언트를 지원합니다.
- Tomcat, Jetty, Undertow: 다양한 서버 환경을 지원합니다.
특징 | Spring MVC | Spring WebFlux |
아키텍처 | Servlet API 기반 | Reactive Streams API 기반 |
처리 방식 | 동기식, 블로킹 | 비동기식, 논블로킹 |
스레드 모델 | Thread per Request | Event Loop 방식 |
서블릿 컨테이너 | Servlet Container (Tomcat 등) | Netty (기본), Servlet 3.1+ |
확장성 | 수직적 확장 중심 | 수평적 확장 용이 |
적합한 워크로드 | CPU 바운드 작업 | I/O 바운드 작업 |
학습 곡선 | 상대적으로 단순 | 반응형 프로그래밍 학습 필요 |
개발 방식 | 명령형 프로그래밍 | 선언형/함수형 프로그래밍 |
데이터 처리 | 단일 값 반환 | 스트림 형태의 데이터 처리 |
성능 특성 | 동기식 블로킹 처리에 최적화 | 비동기 논블로킹 처리에 최적화 |
사용 사례 | 전통적인 웹 애플리케이션 | 실시간 처리, 대용량 동시 처리 |
💡 동기 - 블로킹 방식, Synchronous Request - Blocking Request
- Spring MVC에서 수행되는 Blocking Request를 수행하면서 클라이언트에서 요청을 보내면 결과가 반환될 때까지 대기를 하는 것을 의미합니다.
💡 비동기 - 논블로킹 방식(Asynchrouse Request - Non-Blocking Request)
- Spring WebFlux에서 수행되는 Non-Blocking Request을 수행하면서 요청을 보내고 결과가 반환되지 않더라도 다른 작업을 수행할 수 있는 것을 의미합니다.
2. 반응형 프로그래밍(Reactive Programming)과 명령형 프로그래밍(Imperative Programming) 코드
💡 반응형 프로그래밍(Reactive Programming)과 명령형 프로그래밍(Imperative Programming) 코드
- 명령형 프로그래밍 코드와 반응형 프로그래밍 코드를 통해서 서로의 처리방식에 대해 이해합니다.
💡 명령형 프로그래밍(Imperative Programming) 예시
- 해당 코드는 위에서 아래로 순차적으로 실행되며, 각 단계는 이전 단계에 의존적으로 수행이 됩니다.
- 해당 과정은 변수를 초기화 → 반복문을 순차적 수행 → 각 단계에서 덧셈 연산을 수행하는 단계별로 명시적으로 지시하고 있습니다.
// 명령형 프로그래밍(Imperative Programming) 예시
int sum = 0;
for (int i = 0; i < array.length; i++) {
sum += array[i];
}
💡반응형 프로그래밍(Reactive Programming) 예시
- 해당 코드는 명령형 프로그래밍과 동일하게 수행되는 코드입니다.
- 일련의 스트림을 통한 작업으로 진행하기에 fromArray()를 통해서 배열을 반응형 스트림으로 변환하며, 스트림의 모든 요소를 reduce()를 통해서 0부터 순차적으로 값을 더합니다.
- 이러한, 작업의 결과를 성공, 에러, 완료 시에 대한 콜백함수로 피드백을 받습니다.
// 반응형 프로그래밍(Reactive Programming) 예시
Flux.fromArray(array)
.reduce(0, (sum, value) -> sum + value)
.subscribe(
result -> System.out.println("Sum: " + result),
error -> System.err.println("Error: " + error),
() -> System.out.println("Completed!")
);
3) 반응형 프로그래밍(Reactive Programming)
💡 반응형 프로그래밍(Reactive Programming)
- 일련의 발생하는 데이터 스트림을 처리하고 스트림이 변경될 때마다, 이에 따르는 반응을 하는 프로그래밍 방식을 의미합니다. 이를 기반으로 ‘이벤트 기반의 비동기식 애플리케이션’을 구축할 수 있습니다.
- 반응형 프로그래밍에서 발생하는 이벤트는 ‘비동기적’으로 처리가 되고, 여러 이벤트들을 하나의 스트림으로 생성이 됩니다.
- 이렇게 생성된 스트림에 대해 구독(Subscribe)하여 전달받은 스트림을 일괄 처리할 수 있습니다.
💡 반응형 프로그래밍 예시
- 아래의 예시에서는 데이터를 발행하는 Publisher 측과 데이터를 구독하는 Subscriber 측이 있습니다.
- 데이터를 발행하는 측에서는 배열을 Flux로 변환하고 map을 통해서 값을 2배로 변환합니다. 또한 Filter 연산자로 5보다 큰 값에 대해 필터링하여 전달(emit)합니다.
- 데이터를 수신하는 측에서는 onNext, onError, onComplete을 통해서 전달받은 값을 순차적으로 출력하고, 에러 발생 시 메시지 출력, 모든 처리 완료 시 "Completed!"를 출력합니다.
// Reactive Programming 예시 (Project Reactor 사용)
Integer[] array = {1, 2, 3, 4, 5, 6};
// Publisher (데이터 발행)
Flux<Integer> publisher = Flux.fromArray(array)
.map(value -> value * 2)
.filter(value -> value > 5);
// Subscriber (데이터 구독)
publisher.subscribe(
// onNext - 데이터 수신시 처리
value -> System.out.println("Received: " + value),
// onError - 에러 발생시 처리
error -> System.err.println("Error: " + error),
// onComplete - 완료시 처리
() -> System.out.println("Completed!")
);
// 실행 결과
// Received: 6
// Received: 8
// Received: 10
// Received: 12
// Completed!
[ 더 알아보기 ]
💡 스트림(Stream)
- 시간에 따라 연속적으로 발생하는 데이터의 흐름을 의미합니다. 이러한 스트림들은 여러 이벤트들을 시간 순서대로 묶은 하나의 데이터 흐름을 의미합니다.
- 예를 들어서, 네트워크 요청과 응답, 데이터베이스 변경사항과 같은 하나의 이벤트적인 것들을 묶은 것을 의미합니다.
- 반응형 프로그래밍에서는 데이터가 준비되면 즉시 처리되거나, 하나의 데이터가 아닌 시간에 따르는 연속된 데이터를 의미합니다.
💡 이벤트(Event)
- 시스템에서 발생하는 의미 있는 상태 변화나 사건을 의미합니다.
- 예를 들어서 사용자의 마우스 클릭이나 키보드 입력과 같은 단일한 상태 변화등을 의미합니다.
- 반응형 프로그래밍에서는 발생하는 이벤트들은 비동기적으로 처리되며 연속적으로 발생할 수 있습니다. 이를 묶어서 하나의 스트림으로 처리가 됩니다.
💡 스트림과 이벤트의 관계
- 스트림은 여러 이벤트들을 시간 순서대로 묶어서 하나의 데이터 흐름으로 처리하는 방식입니다. 즉, 이벤트들은 스트림을 구성하는 요소가 됩니다.
💡 반응형 프로그래밍에서 처리 이후 onComplete와 onError에 대한 피드백을 받는데 그러면 동기식으로 처리되는 거 아닌가?
- 비동기 처리에서도 작업의 완료나 에러 상태를 알려주는 콜백은 필수적으로 됩니다. 즉, 데이터 처리는 비동기적으로 진행되어서 메인 스레드를 차단하지 않습니다(Non-Block)
- 해당 작업은 별도의 스레드에서 실행되며, 콜백은 단순한 결과를 통지하는 역할을 합니다.
- onComplete/onError 콜백은 작업의 "결과"를 알려주는 것이지, 작업 자체를 동기식으로 만드는 것은 아닙니다.
1. 반응형 프로그래밍 지원 라이브러리 / 프레임워크
💡 반응형 프로그래밍 지원 라이브러리 / 프레임워크
- 반응형 프로그래밍을 구현하기 위한 라이브러리/프레임워크에 대해 알아봅니다.
라이브러리 / 프레임워크 | 설명 |
RxJava | - Java를 위한 반응형 확장 라이브러리 - Netflix에서 개발한 JVM 기반 - Android 개발에서 널리 사용 |
Project Reactor | - Spring Framework 채택 라이브러리 - Spring WebFlux의 기반 - Mono와 Flux 반응형 타입 제공 |
Akka Streams | - 액터 모델 기반 스트림 처리 - Scala와 Java 지원 - 분산 시스템 지원 |
RxJS | - JavaScript용 반응형 라이브러리 - Angular 프레임워크 핵심 - 브라우저 이벤트 처리에 적합 |
Reactive Streams | - JVM용 반응형 스트림 표준 사양 - Netflix, Pivotal, Lightbend 참여 - Publisher-Subscriber 패턴 정의 |
2. 반응형 프로그래밍 구조 : Project Reactor 라이브러리 - Spring WebFlux 간의 관계
💡 반응형 프로그래밍 구조 : Project Reactor 라이브러리 - Spring WebFlux 간의 관계
- 반응형 프로그래밍 전반적인 구조에 대해서 확인해 봅니다.
1. 반응형 프로그래밍(Reactive Programming)을 구축하기 위해 다양한 언어를 선택하여 구성할 수 있습니다. 그중 Spring framework를 기반으로 사용되는 Java/Kotlin 언어에서는 Project Reactor 라이브러리를 사용합니다.
2. Project Reactor 내의 Spring WebFlux를 기반으로 애플리케이션을 구축합니다.
3. 애플리케이션의 구축은 데이터의 송신 - 수신 관계에서는 발행자-구독자 간의 관계인 Publisher-Subscriber 패턴을 이용하고, 이를 수행하기 위해서 네트워크 애플리케이션 프레임워크로 웹 서버로 사용되는 Netty를 활용하며 외부 통신의 경우는 WebClient의 클래스를 활용하여 비동기적 통신을 수행합니다.
4) Project Reactor
💡 Project Reactor
- 반응형 프로그래밍(Reactive Programming)을 구현하기 위한 Reactor는 Reactive 라이브러리 중 하나입니다. 이는 리액티브 스트림 사양을 구현하며, 비동기 데이터 처리를 위한 강력한 도구를 제공합니다.
- Publisher-Subscriber 패턴을 중심으로 동작하며, 발행자(Publisher)는 스트림을 생성하고 방출(emit)을 하는 역할을 수행하며, 생성된 데이터는 Mono, Flux 타입으로 구성되어서 데이터를 구독자(Subscriber)가 처리할 수 있을양 만큼 전달되도록 백프레셔(backpressure)를 통해 제어됩니다
- 발행자는 데이터를 스트림으로 생성하고 가공하고 구독자에게 전달하는 역할을 합니다.
1. Reactor의 Publisher-Subscriber 패턴 수행 과정
💡 반응형 스트림(Reactive Stream) 수행과정
- 발행자(Publisher)는 데이터 스트림을 생성하고 방출(emit)하는 역할을 수행하며, Mono, Flux의 주요 타입을 제공합니다.
- 구독자(Subscriber)는 발행자(Publisher)가 방출한 데이터를 수신하고 처리하는 역할을 수행합니다.
- 해당 처리되는 과정을 통해 ‘데이터의 흐름이 제어’되고, 백프레셔(backpressure)를 통해 Subscriber가 처리할 수 있는 만큼의 데이터만 받을 수 있도록 보장됩니다.
1. Subscriber → Publisher : subscribe
- 데이터를 받고자 하는 Subscriber는 Publisher에게 구독(subscribe)을 신청하는 단계입니다.
2. Subscription → Subscriber : onSubscribe
- Publisher가 구독(subscribe) 요청을 수락하고, Subscription 객체를 통해 Subscriber에게 구독이 성공했음을 알립니다.
3. Subscriber → Subscription : request(n)/cancel
- Subscriber가 처리할 수 있는 데이터의 양(n)을 지정하여 요청하거나, 필요한 경우 구독을 취소할 수 있습니다.
4. Subscription → Subscriber : onNext(data)
- 요청받은 데이터를 Subscriber에게 하나씩 전달합니다.
5. Subscription → Subscriber : onComplete/onError
- 모든 데이터 처리가 완료되었거나 오류가 발생했을 때 Subscriber에게 알립니다.
구성 요소 | 설명 |
Publisher (발행자) | 데이터 스트림을 생성하고 발행하는 주체 |
Subscriber (구독자) | Publisher가 발행한 데이터를 받아 처리하는 주체 |
Subscription | Publisher와 Subscriber 간의 통신을 조정하는 중개자 |
3.1. 백프레셔(Backpressure)
💡 백프레셔(Backpressure)
- 데이터 스트림에서 데이터 발행자(Publisher)와 구독자(Subscriber) 사이의 데이터 처리 속도 차이를 조절하기 위한 메커니즘입니다.
- 아래의 예시와 같이 데이터 발행자(Publisher)가 데이터를 데이터 구독자(Subscriber)에게 전달(emit)을 하는 경우 발행자의 데이터 속도가 구독자 속도보다 빠를 때 발생하는 문제에 대해서 백프레셔가 이를 속도를 조절하여 처리합니다.
- 이는 구독자가 처리할 수 있는 만큼의 데이터만 요청하여 시스템 과부하를 방지하며 메모리의 사용량을 효율적으로 관리하고 시스템의 안정성을 보장합니다.
💡 아래의 예시에서는 데이터 구독자(Subsciber)는 데이터 발행자(Publisher)를 구독(Subscribe)하고 있습니다.
- 이때 데이터 전달(emit)을 통해서 데이터 1, 데이터 2, 데이터 3, 데이터 4, 데이터 N 형태로 데이터를 전달을 받는데, 데이터 구독자(Subsciber) 보다 많은 데이터가 전달이 되는 경우 시스템 과부하가 발생할 수 있습니다.
- 이에 따라서 백프레셔에서 이를 관리합니다.
3.2. Publisher 반응형 타입
Publisher 타입 | 설명 |
Mono | 0 또는 1개의 데이터 항목을 방출하는 Publisher입니다. |
Flux | 0개 이상의 데이터 항목을 방출하는 Publisher입니다. |
💡 Publisher 타입 예시
1. Mono 예시
- 단일 데이터("Hello")를 처리하는 예시입니다.
- subscribe() 메서드를 통해 데이터를 구독하고 처리합니다.
- 실행 결과로 "Hello" 데이터가 출력되고 완료됩니다
2. Flux 예시
- 여러 개의 데이터(1,2,3,4,5)를 연속적으로 처리하는 예시입니다
- 각각의 데이터가 순차적으로 처리되어 출력되고 완료됩니다
3. 동일한 콜백함수
- data: 데이터를 처리하는 콜백
- error: 오류 발생 시 처리하는 콜백
- completed: 모든 데이터 처리가 완료됐을 때 실행되는 콜백
// Mono 예시 - 단일 데이터 처리
Mono<String> mono = Mono.just("Hello");
mono.subscribe(
data -> System.out.println("Data: " + data),
error -> System.err.println("Error: " + error),
() -> System.out.println("Completed")
);
// 결과
// Data: Hello
// Completed
// Flux 예시 - 여러 데이터 처리
Flux<Integer> flux = Flux.just(1, 2, 3, 4, 5);
flux.subscribe(
data -> System.out.println("Data: " + data),
error -> System.err.println("Error: " + error),
() -> System.out.println("Completed")
);
// 결과
// Data: 1
// Data: 2
// Data: 3
// Data: 4
// Data: 5
// Completed
3.3. Subscriber 데이터 처리 타입
메서드 | 설명 |
onSubscribe | 구독이 시작될 때 호출됩니다. |
onNext | 새로운 데이터가 도착할 때마다 호출됩니다. |
onError | 오류가 발생했을 때 호출됩니다. |
onComplete | 모든 데이터 처리가 완료되었을 때 호출됩니다. |
💡 Subscriber 예시
- onSubscribe: 구독이 시작될 때 "Subscribed!" 메시지를 출력합니다.
- onNext: 1부터 5까지의 각 숫자가 도착할 때마다 "Received: [숫자]"를 출력합니다.
- onComplete: 모든 데이터 처리가 끝나면 "Completed!" 메시지를 출력합니다.
- onError: 에러가 발생했을 경우 에러 메시지를 출력합니다 (위 예시에서는 에러가 발생하지 않습니다).
// Subscriber 예시
Flux<Integer> numbers = Flux.just(1, 2, 3, 4, 5);
numbers.subscribe(
// onNext - 각 데이터 처리
data -> System.out.println("Received: " + data),
// onError - 에러 처리
error -> System.err.println("Error occurred: " + error),
// onComplete - 완료 처리
() -> System.out.println("Completed!"),
// onSubscribe - 구독 시작 시 처리
subscription -> {
System.out.println("Subscribed!");
subscription.request(Long.MAX_VALUE);
}
);
// 실행 결과:
// Subscribed!
// Received: 1
// Received: 2
// Received: 3
// Received: 4
// Received: 5
// Completed!
5) Netty
💡 Netty
- Spring 기반의 네트워크 애플리케이션 프레임워크로 ‘비동기식 이벤트 기반 서버’를 구성하는 데 사용이 되는 네트워크 애플리케이션 프레임워크입니다.
- Spring WebFlux를 이용하는 경우에는 기본 웹 서버로 사용이 됩니다. 이벤트 루프 기반으로 동작하여 단일 스레드로 여러 요청을 비동기적으로 처리할 수 있습니다.
- 이벤트가 발생하면 이벤트 큐에 저장되고, 이벤트 루프가 순차적으로 처리합니다. 높은 성능과 확장성을 갖추고 있으며, TCP, UDP 등 다양한 프로토콜을 지원하고 있어서 네트워크 애플리케이션 개발에 매우 유용합니다.
💡 전통적인 Spring Web 기반의 애플리케이션을 수행하였을 때 Tomcat이 수행됨을 확인할 수 있습니다.
💡 Spring WebFlux로 구성한 애플리케이션을 수행하였을때 Netty 서버가 수행됨을 확인할 수 있습니다.
1. Netty와 Tomcat 비교
특징 | Netty | Tomcat |
아키텍처 | 이벤트 루프 기반 비동기 | 스레드 풀 기반 동기 |
처리 방식 | 비동기-논블로킹 | 동기-블로킹 |
성능 특성 | 높은 동시성, 낮은 지연시간 | 스레드당 하나의 요청 처리 |
메모리 사용 | 효율적 (이벤트 루프 방식) | 상대적으로 높음 (스레드당 메모리) |
확장성 | 수평적 확장 용이 | 수직적 확장 중심 |
사용 사례 | 실시간 처리, 대규모 동시 연결 | 전통적인 웹 애플리케이션 |
프로토콜 지원 | TCP, UDP, HTTP 등 다양한 프로토콜 | 주로 HTTP/HTTPS |
구현 복잡도 | 상대적으로 복잡 | 단순하고 직관적 |
2. Netty의 이벤트 루프(Event Loop) 기반 비동기 처리 방식
💡 Netty의 이벤트 루프(Event Loop) 기반 비동기 처리 방식
- Netty가 웹 서버로 사용되는 경우 이벤트 루프 기반으로 동작하여 단일 스레드로 여러 요청을 비동기적으로 처리할 수 있습니다. 이벤트가 발생하면 이벤트 큐에 저장되고, 이벤트 루프가 순차적으로 처리합니다.
1. Client → Event Queue
- 클라이언트 요청으로 이벤트가 발생하면 해당 이벤트를 처리하기 위해 이벤트 큐(Event Queue)에 저장이 됩니다.
2. Event Queue → Process Events → Event Loop
- Event Loop를 통해 이벤트 큐(Event Queue)에서 요청에 따르는 이벤트를 이벤트 큐(Event Queue)내에서 하나씩 가져와 처리합니다.
3. Event Loop → Register Callback → Intensive Operations(Platform)
무거운 작업에 대해서 작업을 처리하기 위한 콜백 함수로 등록하여 Platform의 작업 스레드 풀로 위임됩니다.
4. Intensive Operations → Operation Completion → Event Loop
- 작업이 완료되면, 결과를 이벤트 루프(Event Loop)에게 알리고 새로운 스레드는 종료가 됩니다.
5. Event Loop → Client
- Event Loop가 콜백을 처리하여 클라이언트에 응답을 반환합니다.
6) Spring Webflux
💡 Spring Webflux
- 반응형(Reactive) 및 비동기적인 웹 애플리케이션 개발을 지원하는 웹 프레임워크를 의미합니다.
- 이는 비동기-논블로킹 웹 스택으로, 적은 수의 스레드로 동시성을 처리할 수 있습니다.
- 해당 Spring Webflux의 경우는 Spring Framework 5.0, Spring Boot 2.0 이상에서 지원을 합니다.
1. Spring WebFlux의 주요 특징
특징 | 설명 |
비동기-논블로킹 방식 | 이벤트 루프 모델을 사용하여 적은 수의 스레드로 많은 동시 연결을 처리할 수 있습니다. |
리액티브 스트림 지원 | Reactor 라이브러리를 기반으로 하여 백프레셔를 포함한 리액티브 스트림을 완벽하게 지원합니다. |
함수형 엔드포인트 | 전통적인 애노테이션 기반 모델 외에도 함수형 프로그래밍 스타일의 라우팅과 핸들링을 지원합니다. |
Netty 기반 | 기본적으로 Netty 서버를 사용하여 높은 성능과 확장성을 제공합니다. |
2. Spring WebFlux 사용이 적합한 경우
사용 사례 | 설명 |
마이크로서비스 아키텍처 | 서비스 간 비동기 통신이 필요한 경우 |
실시간 데이터 처리 | 스트리밍 데이터를 다루는 애플리케이션 |
높은 동시성 요구 | 다수의 동시 연결을 처리해야 하는 경우 |
이벤트 기반 시스템 | 실시간 알림이나 이벤트 처리가 필요한 경우 |
3. Spring WebFlux 계층 구조(Layer)
💡 Spring WebFlux 계층 구조(Layer)
- Spring WebFlux에서 각각 처리하는 계층 구조(Layer)에 대해 알아봅니다.
계층(Layer) | 주요 특징 |
표현 계층(Presentation Layer) | - 함수형 방식의 경우는 Router Functions와 Handler Functions이 위치합니다. - 주석 기반 방식(annotation-based)에서는 Controller가 해당 계층에 위치합니다. - HTTP 요청을 받아 적절한 비즈니스 로직으로 전달 - 클라이언트와의 직접적인 통신 처리를 수행합니다. |
비즈니스 계층(Service Layer) | - 실제 비즈니스 로직 구현 - 트랜잭션 관리 및 도메인 규칙 적용 - Reactive 스트림을 활용한 비동기 처리 |
영속성 계층(Persistence Layer) | - R2DBC나 Reactive MongoDB 등을 통한 데이터 접근 - Reactive 리포지토리 패턴 구현- 비동기 데이터 CRUD 연산 처리 |
도메인 계층(Domain Layer) | - 비즈니스 엔티티와 값 객체 정의 - 도메인 이벤트와 규칙 관리 - Reactive 스트림과 호환되는 도메인 모델 구현 |
[ 더 알아보기 ]
💡 Router Function과 Handler Function을 분리하는 이유는?
1. 단일 책임 원칙(SRP) 준수
- Router의 경우는 요청 경로와 HTTP 메서드 매핑에만 집중하며, Handler의 경우는 실제 비즈니스 로직 처리에만 집중할 수 있습니다.
2. 테스트 용이성
- Router와 Handler를 독립적으로 테스트 가능하며, 각 컴포넌트의 단위 테스트가 더 명확하고 간단해짐
3. 코드 재사용성 향상
- Handler 함수는 여러 Router에서 재사용 가능하며 동일한 로직을 다른 엔드포인트에서도 활용 가능합니다.
7) Spring WebFlux 사용예시 : 비동기 병렬 처리 API 서버 예시
💡Spring WebFlux 사용예시 : 비동기 병렬 처리 API 서버 예시
- 해당 사용예시에서는 애플리케이션이 신규 런칭이 되었다는 가정하에, 애플리케이션을 가입하는 사용자가 급증을 하게 됩니다. 이때에, 회원가입을 하면 축하 메일을 전송해 주는 비즈니스 로직이 포함되어 있습니다.
- 동기식으로 처리를 한다면 하나의 사용자 아이디 조회 → 사용자 등록 → 이메일 전송이라는 트랜잭션 내에 처리를 기다리고 있어야 합니다.
- 그러나 Spring WebFlux를 이용한다면, 이를 비동기 병렬적으로 처리하여서 트랜잭션의 순차적인 대기 없이 수행되도록 하는 예시입니다.
1. 구성 시나리오
💡 구성 시나리오
- 해당 시나리오는 비동기 병렬 처리를 위한 비즈니스 로직 처리 과정입니다.
- 시나리오에서는 임의의 사용자를 10명을 등록하여 비동기 API 통신을 확인합니다.
- 해당 등록을 위해서 사용자 아이디 조회 → 사용자 등록 → 이메일 전송 처리를 수행합니다.
1. UserServiceTest → RouterConfig
- Test 클래스 내에서는 WebClient를 통해 사용자를 등록하는 API 호출을 수행합니다. 해당 과정에서 총 10번의 사용자 등록을 하도록 호출하였습니다.
2. RouterConfig → UserHandler
- 호출된 엔드포인트를 기반으로 처리를 위한 UserHandler의 registerUser() 메서드를 호출합니다.
3. UserHandler → UserService
- UserHandler에서는 UserService를 호출하고 처리과정 중 성공을 하면 “1”을 반환하고 에러가 발생하면 “0”을 반환합니다.
4. UserSerivce → UserServiceImpl
- UserServiceImpl 구현체에서 인터페이스의 비즈니스 로직을 처리합니다. 비즈니스 로직은 사용자 아이디를 기반으로 사용자 조회 → 중복 체크를 통과하면 사용자 등록 → 사용자 등록이 완료되면 이메일을 전송하는 처리과정을 비동기로 처리를 수행합니다.
5. UserServiceImpl → UserRepository
- 비즈니스 로직 처리 중 데이터 처리를 위해서 UserRepository를 호출합니다.
6. UserRepository → ReactiveCrudRepository
- ReactiveCrudRepository로부터 상속을 받아서 구현합니다. 이를 통해 데이터베이스에 접근하여 비동기적 데이터를 조회합니다.
- 해당 처리가 완료되면 최종적으로 처리된 값을 반환받습니다.
2. 의존성 주입
💡 의존성 주입
- 주요한 라이브러리로 spring-boot-starter-webflux로 반응형 프로그래밍을 구현하였고, spring-boot-starter-data-r2dbc, org.postgresql:r2dbc-postgresql를 통해서 반응형 데이터베이스를 구현하였습니다
- 또한, 이를 테스트하기 위해 JUnit, reactor-test를 주입하였습니다.
dependencies {
// Spring Boot Starter
implementation "org.springframework.boot:spring-boot-starter-webflux:${bootVer}" // Webflux
implementation "org.springframework.boot:spring-boot-starter-data-r2dbc:${bootVer}" // R2DBC
implementation "org.springframework.boot:spring-boot-starter-mail" // Mail
// OpenSource
implementation 'org.postgresql:r2dbc-postgresql:1.0.7.RELEASE' // R2DBC - PostgresSQL
implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVer}" // Jackson Databind
// Runtime & Compile & test
runtimeOnly 'org.postgresql:postgresql' // Postgres
compileOnly 'org.projectlombok:lombok' // Lombok
annotationProcessor 'org.projectlombok:lombok' // Lombok
testImplementation 'io.projectreactor:reactor-test:3.7.1' // Reactor Test
testImplementation 'org.springframework.boot:spring-boot-starter-test' // JUnit
testRuntimeOnly 'org.junit.platform:junit-platform-launcher' // JUnit
}
3. 주요 비즈니스 로직 수행 : UserServiceImpl
💡 주요 비즈니스 로직 수행 : UserServiceImpl
1. 파라미터로 전달받은 JSON 데이터를 기반으로 사용자 아이디를 조회합니다.
2.1. 아이디가 존재하면 0의 값을 최종 반환합니다.
2.2. 아이디가 존재하지 않는다면 사용자를 등록합니다.
- 사용자가 등록되면 해당 정보를 기반으로 이메일을 전송합니다.
- 이메일 전송은 별도의 실행은 하지 않고 결과를 기다리지 않습니다.
- 최종적으로 성공 응답 처리가 완료되면 1을 반환합니다.
@Transactional
@Override
public Mono<Integer> userRegister(UserEntity userEntity) {
// [Service] 사용자 아이디를 조회합니다.
return this.findUserByUserId(userEntity.getUserId())
// [CASE1] 아이디가 존재하면, 0의 결과값을 반환합니다.
.flatMap(existingUser -> Mono.just(0)) // 이미 존재하는 사용자인 경우 0 반환
// [CASE2] 아이디가 존재하지 않는다면, 사용자를 등록합니다.
.switchIfEmpty(
// [Service] 사용자를 등록합니다.
userRepository.save(userEntity)
.flatMap(savedUser -> {
MailTxtSendDto mailDto = MailTxtSendDto.builder()
.emailAddr(savedUser.getUserEmail())
.subject("회원가입을 축하합니다!")
.content("환영합니다. 회원가입이 완료되었습니다.")
.build();
// 이메일 전송을 별도로 실행하고 결과를 기다리지 않음
emailService.sendTxtEmail(mailDto)
.subscribe(
null,
error -> log.error("이메일 전송 실패: {}", error.getMessage())
);
// 즉시 성공 응답 반환
return Mono.just(1);
})
)
// [CASE3] 회원가입 실패시, 오류메시지와 0의 값을 반환합니다.
.onErrorResume(e -> {
log.debug("회원가입 처리 중 오류 발생: {}", e.getMessage());
return Mono.just(0); // 에러 발생 시 0 반환
});
}
4. 결과확인
💡 결과확인
- 수행한 API에 대해서 상세 로그를 통해서 결과를 확인합니다.
4.1. 스레드 로그 확인
💡 스레드 로그 확인
- 로그를 보면 여러 개의 서로 다른 actor-tcp-nio 스레드(1부터 10까지)가 동시에 같은 SQL 문을 실행하고 있음을 확인할 수 있습니다.
- 이는 Spring WebFlux의 비동기-논블로킹 특성을 보여주는 것으로, 하나의 요청이 순차적으로 처리되는 것이 아니라 여러 스레드가 동시에 병렬로 처리되고 있다는 것을 의미합니다.
4.2. 비동기 병렬 처리 수행
💡 비동기 병렬 처리 수행
- 아래의 로그를 확인하더라도 ‘일괄적’으로 SELECT 작업을 수행하고 INSERT 작업을 수행하는 하고 있지는 않습니다.
- 각각 스레드별로 맞게 SELECT를 수행하고 INSERT를 수행하고 있음을 확인할 수 있습니다.
오늘도 감사합니다. 😀
반응형