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)으로 나뉩니다.
- 이 중에서 대중적으로 많이 사용되는 명령형 프로그래밍 방식과 가장 현대적인 개발 방법인 ‘선언형’ 프로그래밍 중 ‘함수형 프로그래밍’에 대해 알아봅니다.

 

 

[Java] 프로그래밍 패러다임 이해하기

해당 글에서는 프로그래밍 패러다임에 대해서 이해하고 각각의 용어에 대해서 이해를 돕기 위해서 작성한 글입니다. 1) 프로그래밍 패러다임 💡 프로그래밍(Programming) 이란? - 하나 이상의 관련

adjh54.tistory.com

 

 

💡 아래의 이미지는 Google 검색 중 프로그래밍 패러다임에 대해 검색을 하면 가장 많이 나오는 이미지인데, ‘프로그래밍 패러다임’을 진화론에 빗대어 표현한 이미지입니다.

- 기계어(MACHINE) → 어셈블리(ASSEMBLY) → 절차적(PROCEDURAL) → 객체 지향(OBJECT ORIENTED) → 함수형(FUNTIONAL)
- 이러한 진화는 프로그래밍의 추상화 수준이 높아지고 코드의 재사용성과 유지보수성이 향상되는 방향으로 발전해 왔음을 보여줍니다.

https://medium.com/free-code-camp/my-favorite-examples-of-functional-programming-in-kotlin-e69217b39112

 

 

 

2) 명령형 프로그래밍(Imperative Programming)


💡 명령형 프로그래밍(Imperative Programming)

- 프로그램의 상태를 변경하는 문장들을 순차적으로 작성하는 프로그래밍 패러다임을 의미합니다.

- 프로그램이 어떻게 실행되어야 하는지 단계별로 명시적으로 지시하고 상태 변경과 데이터 변경이 명시적으로 이루어지며, 순차적으로 실행 흐름을 가지며, 각 단계가 이전 단계에 의존적 방식입니다.
- 기존의 Spring MVC를 이용하여서 명령형 프로그래밍이 구현되어 왔습니다.

 

💡 아래와 같이 명령형 프로그래밍을 나타내는 대표적인 for문입니다.

- for문은 for(A;B;C;)에서 A는 조건화 식으로 반복문이 시작되기 전의 초기값을 지정하고, B는 true라는 조건식이 만족될 때까지 반복됩니다, C는 증감식으로 하나의 반복이 끝날 때마다 변화가 발생합니다.

- 이를 통해서 초기값에서 시작하여 순차적으로 실행 흐름을 가지고 최종결과를 얻어내는 명령형 프로그래밍의 대표적인 for문입니다.

https://ko.wikipedia.org/wiki/For_%EB%A3%A8%ED%94%84

 

 

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) 보다 많은 데이터가 전달이 되는 경우 시스템 과부하가 발생할 수 있습니다.
- 이에 따라서 백프레셔에서 이를 관리합니다.

https://velog.io/@zini9188/Spring-WebFlux-Project-Reactor

 

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 등 다양한 프로토콜을 지원하고 있어서 네트워크 애플리케이션 개발에 매우 유용합니다.

https://netty.io/

 

💡 전통적인 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를 수행하고 있음을 확인할 수 있습니다.

 

 

 

 

 

 

오늘도 감사합니다. 😀


 

반응형