Spring/스프링으로 시작하는 리액티브 프로그래밍

[Java] 리액티브 프로그래밍

꾸준함. 2024. 6. 11. 22:24

1. 리액티브 시스템과 리액티브 프로그래밍

 

1.1 리액티브 시스템(Reactive System)

  • reactive의 사전적 의미는 `반응을 하는`이라는 뜻이며 어떤 이벤트나 상황이 발생했을 때, 반응을 해서 그에 따라 적절하게 행동하는 것을 의미
  • 리액티브 시스템은 `반응을 잘하는 시스템`이며 클라이언트의 요청에 즉각적으로 응답함으로써 지연 시간을 최소화

 

1.2 리액티브 선언문(Reactive Manifesto)

  • 리액티브 선언문은 리액티브 시스템 구축을 위한 일종의 설계 원칙이자 리액티브 시스템의 특징
  • 리액티브 선언문을 통해 리액티브 시스템이 지향하는 바가 무엇인지 명확하게 알 수 있음
  • 리액티브 시스템의 설계 원칙에 따라 대규모 분산 시스템 또는 멀티코어 기반의 클라우드 시스템, 모바일 시스템 등 빠른 응답성을 바탕으로 유지보수와 확장이 용이한 시스템을 구축하는 데 활용 가능

 

https://medium.com/@emreozgoz/what-is-reactive-manifesto-38aecd0755a1

 

 

1.2.1 MEANS

  • MEANS는 리액티브 시스템에서 주요 통신 수단으로 무엇을 사용할 것인지 표현한 것
  • 그림에 나와 있는 비동기 메시지 기반의 통신을 통해 구성요소들 간의 느슨한 결합, 격리성, 위치 투명성을 보장

 

1.2.2 FORM

  • 메시지 기반 통신을 통해 어떠한 형태를 지니는 시스템으로 형성되는지 나타냄
  • 그림에서는 리액티브 시스템이 비동기 메시지 통신 기반하에 탄력성(Elastic)회복성(Resilient)을 가지는 시스템이어야 함을 보여줌
  • 탄력성이란 시스템의 작업량이 변화하더라도 일정한 응답을 유지하는 것을 의미
    • 시스템으로 유입되는 입력의 양과 무관하게 시스템에서 요구하는 응답성을 일정하게 유지하는 것
    • 일정한 응답성을 유지하기 위해 입력을 처리하기 위한 시스템 자원을 적재적소로 추가하거나 감소시켜 작업량의 변화에 적절히 대응하는 것

 

  • 회복성이란 시스템에 장애가 발생하더라도 응답성을 유지하는 것을 의미
    • 회복성이 없다면 장애 발생 시 시스템이 응답하지 못하는 심각한 문제에 직면
    • 이를 방지하기 위해 회복성은 리액티브 시스템의 핵심 설계 원칙
    • 시스템의 구성요소들이 독립적으로 분리되기 때문에 장애가 발생하더라도 전체 시스템은 여전히 응답 가능 상태이고 장애가 발생한 부분만 복구하면 됨

 

1.2.3 VALUE

  • 비동기 메시지 기반 통신을 바탕으로 한 회복성과 예측 가능한 규모 확장 알고리즘을 통해 시스템의 처리량을 자동으로 확장하고 축소하는 탄력성을 확보함으로써 즉각적으로 응답 가능한 시스템을 구축할 수 있음
  • 빠른 응답성을 바탕으로 유지보수(Maintainable)와 확장(Extensible)이 용이한 시스템

 

1.3 리액티브 프로그래밍(Reactive Programming)

  • 리액티브 시스템을 구축하는 데 필요한 프로그래밍 모델
  • 리액티브 시스템에서의 비동기 메시지 통신은 Blocking I/O 방식이 아닌 Non-Blocking I/O 방식의 통신
    • Blocking I/O 방식의 통신에서는 해당 쓰레드가 작업을 처리할 때까지 잔여 작업들은 차단되어 대기하며 대기 없이 처리하기 위해서는 별도의 추가 쓰레드를 할당해야 하므로 비용 소모
    • 반면, Non-Blocking I/O 방식의 통신에서는 쓰레드가 차단되지 않기 때문에 리액티브 시스템의 설계 원칙에 잘 부합함

 

1.4 리액티브 프로그래밍 특징

  • 위키피디아에서 리액티브 프로그래밍을 다음과 같이 정의
  • In computing, reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change

 

1.4.1 선언형 프로그래밍(declarative programming)

  • 전통적인 프로그래밍 방식은 C언어와 같은 명령형 프로그래밍 방식으로 실행할 동작을 구체적으로 명시하는 프로그래밍 코드 형태
  • 반면, 선언형 프로그래밍 방식은 명령형 프로그래밍 방식과 달리 실행할 동작을 구체적으로 명시하지 않고 어떤 동작을 수행하겠다는 목표만 선언
    • 메서드 체이닝을 통해 입력받은 데이터를 어떤 식으로 처리할지 한눈에 알아볼 수 있기 때문에 코드가 간결해지고 가독성도 좋아지는 효과
    • 선언형 프로그래밍 방식은 함수형 프로그래밍으로 구성됨 (filter 메서드)

 

 

 

1.4.2 data streams and the propagation of change

  • data streams라는 것은 데이터가 지속적으로 발생한다는 의미
  • the propagation of change는 지속적으로 데이터가 발생할 때마다 이 것을 변화하는 이벤트로 보고, 해당 이벤트를 발생시키면서 데이터를 계속적으로 전달하는 것을 의미

 

1.5 리액티브 프로그래밍 코드 구성

  • 리액티브 프로그래밍 코드는 크게 Publisher, Subscriber, Data Source, Operator 등으로 구성됨

 

1.5.1 Publisher

  • 입력으로 들어오는 데이터를 제공하는 역할

 

1.5.2 Subscriber

  • Publisher가 데이터를 제공하는 역할을 수행하는 반면, Subscriber는 Publisher가 제공한 데이터를 전달받아서 사용하는 주체

 

1.5.3 Data Source

  • Publisher의 입력으로 들어오는 데이터를 대표하는 용어
  • Data Source를 리액티브 프로그래밍에서는 Data Stream이라고도 표현
    • Data Source는 말 그대로 원천 데이터이고 Data Stream은 Publisher의 입력으로 들어오는 데이터의 형태이기에 엄밀히 따지자면 둘의 의미는 사뭇 다르지만 둘 다 Publisher의 입력으로 전달되는 데이터라고 인지하고 있어도 무방함

 

1.5.4 Operator

  • 애플리케이션의 요구사항에 맞게 Publisher와 Subscriber 사이에서 적절한 가공 처리가 이루어지며 이 가공 처리를 담당하는 역할
    • Publisher로부터 전달된 데이터가 순수하게 아무런 처리를 거치지 않고 Subscriber에 전달되는 경우는 거의 없음

 

  • 리액티브 프로그래밍은 Operator로 시작해서 Operator로 끝난다고 해도 과언이 아님
    • 데이터를 생성하는 Operator부터 시작해서 데이터 필터링, 데이터 변환 등 리액티브 프로그래밍에는 수많은 Operator가 존재

 

2. 리액티브 스트림즈(Reactive Streams)

  • 리액티브한 코드를 작성하기 위해서는 리액티브 라이브러리가 있어야 하며 이 리액티브 라이브러리를 어떻게 구현할지 정의해 놓은 별도의 표준 사양
  • 정리하자면 데이터 스트림을 논블로킹이면서 비동기적인 방식으로 처리하기 위한 리액티브 라이브러리의 표준 사양
  • 리액티브 스트림즈 구현체는 다음과 같음
    • RxJava
    • Reactor (Spring Framework와 가장 궁합이 잘 맞는 구현체)
    • Akka Streams
    • Java 9 Flow API
    • etc.

 

2.1 리액티브 스트림즈 구성 요소

  • 리액티브 스트림즈를 통해 구현해야 되는 API 컴포넌트는 다음과 같음
    • Publisher
    • Subscriber
    • Subscription
    • Processor

 

컴포넌트 설명
Publisher 데이터를 생성하고 발행하는 역할
Subscriber 구독한 Publisher로부터 발행된 데이터를 전달받아 처리하는 역할
Subscription Publisher에 요청할 데이터의 개수를 지정하고 데이터의 구독을 취소하는 역할
Processor Publisher와 Subscriber의 기능을 모두 지님
Subscriber로서 다른 Publisher를 구독할 수 있고, Publisher로서 다른 Subscriber가 구독할 수 있음

 

https://www.toptal.com/custom-software-development/reactive-programming-with-java/

 

2.2 Publisher와 Subscriber의 동작 과정

 

https://www.youtube.com/watch?v=AoJVZ2sdqSc

 

동작 과정

  • Subscriber는 전달받을 데이터를 구독 (subscribe())
  • 다음으로 Publisher는 데이터를 발행할 준비가 되었음을 Subscriber에게 알림 (onSubscribe())
  • Publisher가 데이터를 발행할 준비가 되었다는 알림을 받은 Subscriber는 전달받기를 원하는 데이터의 개수를 Publisher에게 요청  (Subscription.request())
    • Publisher가 데이터를 발행하는 속도가 Subscriber가 처리하는 속도보다 더 빠를 경우 데이터가 쌓여 시스템 부하가 커지는 결과가 야기되므로 이를 방지하기 위해 데이터 개수를 제어하는 역할

 

  • 다음으로 Publisher는 Subscriber로부터 요청받은 만큼의 데이터를 발행 (onNext(data))
  • Publisher와 Subscriber 간에 데이터 발행, 수신, 요청의 과정을 반복하다가 Publisher가 모든 데이터를 통지하게 되면 마지막 데이터 전송이 완료되었음을 Subscriber에게 알림 (onComplete())
    • 만약 Publisher가 데이터를 처리하는 과정에서 에러 발생할 경우 Subscriber에게 알림 (onError())

 

2.3 코드로 확인하는 리액티브 스트림즈 컴포넌트

 

2.3.1 Publisher 인터페이스

 

 

부연 설명

  • subscribe 메서드 하나만 구현하면 되는 매우 단순한 함수형 인터페이스
  • 한 가지 특징은 subscribe() 메서드가 Subscriber가 아닌 Publisher에 정의되어 있다는 점
    • Publisher가 subscribe 메서드의 파라미터인 Subscriber를 등록하는 형태로 구독이 이루어짐

 

2.3.2 Subscriber 인터페이스

 

 

부연 설명

  • onSubscribe(): 구독 시작 시점에 Publisher에게 요청할 데이터의 개수를 지정하거나 구독을 해지하는 처리를 하는 역할을 수행하며 파라미터로 전달되는 Subscription 객체를 통해 이루어짐
  • onNext(): Publsher가 통지한 데이터를 처리하는 역할
  • onError(): Publisher가 데이터 통지를 위한 처리 과정에서 에러가 발생했을 때 해당 에러를 처리하는 역할
  • onComplete(): Publisher가 데이터 발행을 완료했음을 알릴 때 호출되는 메서드이며 데이터 발행이 정상적으로 완료된 후 후처리 필요시 onComplete() 메서드에서 처리 코드를 작성

 

2.3.3 Subscription

 

 

부연 설명

  • Subscriber가 구독한 데이터의 개수를 요청하거나 구독을 해지하는 역할
  • 자바의 익명 인터페이스의 특성을 잘 이용해 Publisher와 Subscriber 간 데이터를 주고받을 수 있음
  • request(): Publisher에게 데이터의 개수 요청
  • cancel(): 구독 해지

 

2.3.4 Processor

 

 

부연 설명

  • 별도로 구현할 메서드가 없는 인터페이스
  • Processor는 Publisher와 Subscriber의 기능을 모두 가지고 있기 때문에 Subscriber 인터페이스와 Publisher 인터페이스를 상속

 

2.4 리액티브 스트림즈 관련 용어

 

2.4.1 Signal

  • Publisher와 Subscriber 간에 주고받는 상호작용
  • 앞서 다루었던 onSubscribe, onNext, onComplete, onError, request, cancel 메서드를 singal이라고 표현
    • onSubscribe, onNext, onComplete, onError 메서드는 Subscriber 인터페이스에 정의되지만 해당 메서드들을 실제 호출해서 사용하는 주체는 Publisher이기 때문에 Publisher가 Subscriber에게 보내는 Signal
    • 마찬가지로 request와 cancel 메서드는 Subscription 인터페이스 코드에 정의되지만 해당 메서드들을 실제로 사용하는 주체는 Subscriber이므로 Subscriber가 Publisher에게 보내는 Signal

 

2.4.2 Demand

  • Subscriber가 Publisher에게 요청하는 데이터
  • Publisher가 아직 Subscriber에게 전달하지 않은 Subscriber가 요청한 데이터

 

2.4.3 Emit

  • Publisher가 Subscriber에게 데이터를 전달
  • Emit의 사전적 의미는 `방출`로 `데이터를 내보내다` 정도로 이해할 수 있음

 

2.4.4 Upstream/Downstream

 

 

부연 설명

  • just 메서드를 사용해서 데이터를 생성한 후 emit 하게 되는데 여기서 just 메서드는 리액티브 스트림즈의 컴포넌트 중 Publisher의 역할
    • 이후 메서드 체이닝 방식으로 filter 메서드 호출 후 연이어 map 메서드 호출
    • 메서드 체이닝 방식으로 호출할 수 있는 이유는 호출하는 각각의 메서드들이 모두 같은 타입의 객체를 반환하기 때문 (모두 Flux 타입)
    • 데이터 스트림의 관점에서 볼 때, just 메서드 호출을 통해 반환된 Flux 입장에서는 5번 라인의 filter 메서드 호출을 통해 반환된 Flux가 자신보다 더 하위에 있기 때문에 Downstream
    • 반면 filter 메서드 호출을 통해 반환된 Flux 입장에서는 just 메서드 호출을 통해 반환된 Flux가 자신보다 더 상위에 있기 때문에 Upstream

 

2.4.5 Sequence

  • Reactor의 레퍼런스 문서에 자주 등장하는 용어
  • Publisher가 emit 하는 데이터의 연속적인 흐름을 정의해 놓은 것
  • Sequence는 Operator 체인 형태로 정의됨
  • 2.4.4에서 소개한 코드처럼 Flux를 통해 데이터를 생성, emit 하고 fitler 메서드를 통해 필터링한 후, map 메서드를 통해 변환하는 이러한 과정 자체를 바로 Sequence라고 부름
  • 정리하자면 Sequence는 다양한 Operator로 데이터의 연속적인 흐름을 정의한 것

 

2.4.6 Operator

  • just, filter, map 같은 메서드들을 리액티브 프로그래밍에서는 Operator라고 지칭
  • 리액티브 프로그래밍은 Operator로 시작해서 Operator로 끝난다고 해도 과언이 아님
    • 그만큼 Operator가 리액티브 프로그래밍의 핵심

 

2.4.7 Source, Original

  • '최초에 가장 먼저 생성된 무언가'라고 생각하면 됨
  • ex) Data Source, Source Publisher, Source Flux

 

2.5 리액티브 스트림즈의 구현 규칙

  • 리액티브 스트림즈 표준 사양에는 리액티브 스트림즈 컴포넌트를 어떻게 구현해야 되는지에 대한 규칙들이 컴포넌트별로 정의되어 있음

 

2.5.1 Publisher 구현을 위한 주요 기본 규칙

 

번호 규칙
1 Publisher가 Subscriber에게 보내는 onNext 시그널의 총 개수는 항상 해당 Subscriber의 구독을 통해 요청된 데이터의 총 개수보다 더 작거나 같아야 함
2 Publisher가 요청된 것보다 적은 수의 onNext 시그널을 보내고 onComplete 또는 onError를 호출하여 구독을 종료할 수 있음

단, IOT 디바이스 같이 Publisher가 처리할 데이터가 끊임없이 발생하는 무한 스트림의 경우, 처리 중 에러가 발생하기 전까지는 종료 자체가 없음
3 Publisher의 데이터 처리가 실패할 경우 onError 시그널을 보내야 함
4 Publisher의 데이터 처리가 성공적으로 종료되면 onComplete 시그널을 보내야 함
5 Publisher가 Subscriber에게 onError 또는 onComplete 시그널을 보내는 경우 해당 Subscriber의 구독은 취소된 것으로 간주
6 일단 onComplete 혹은 onError 시그널을 받으면 더 이상 시그널이 발생되지 않아야 함
7 구독이 취소되면 Subscriber는 결국 시그널을 받는 것을 중지해야 함

 

2.5.2 Subscriber 구현을 위한 주요 기본 규칙

 

번호 규칙
1 Subscriber는 Publisher로부터 onNext 시그널을 수신하기 위해 Subscription.request(n) 메서드를 통해 Demand 시널을 Publisher에게 보내야 함
2 Subscriber.onComplete() 및 Subscriber.onError(Throwable t)  메서드 내부에서 Subscription 또는 Publisher의 메서드를 호출할 경우 Race Condition에 빠질 수 있으므로 호출해서는 안됨
3 Subscriber.onComplete() 및 Subscriber.onError(Throwable t)는 시그널을 수신한 후 구독이 취소된 것으로 간주
4 구독이 더 이상 필요하지 않은 경우 Subscriber는 Subscription.cancel()을 호출해야 함
5 Subscriber.onSubscribe()는 지정된 Subscriber에 대해 최대 한 번만 호출되어야 함

 

2.5.3 Subscription 구현을 위한 주요 기본 규칙

 

번호 규칙
1 구독은 Subscriber가 onNext 또는 onSubscribe 내에서 동기적으로 Subscription.request 메서드를 호출하도록 허용해야 함
이는 request와 onNext 사이의 상호 재귀로 인해 발생할 수 있는 Stackoverflow를 피하기 위해 request가 다시 호출된다는 것을 분명히 하는 것
2 구독이 취소된 후 추가적으로 호출되는 Subscription.request(long n) 메서드는 효력이 없어야 함
3 구독이 취소된 후 추가적으로 호출되는 Subscription.cancle() 메서드는 효력이 없어야 함
4 구독이 취소되지 않은 동안 Subscription.request(long n)의 매개변수가 0보다 작거나 같으면 IllegalArgumentException 예외와 함께 onError 시그널을 보내야 함
5 구독이 취소되지 않은 동안 Subscription.cancle() 메서드는 Publisher가 Subscriber에게 보내는 시그널을 결국 중지하도록 요청해야 함
6 구독이 취소되지 않은 동안 Subscription.cancel() 메서드는 Publisher에게 해당 구독자에 대한 참조를 결국 삭제하도록 요청해야 함
7 Subscription.cancel(), Subscription.request() 호출에 대한 응답으로 예외를 던지는 것을 허용하지 않음

자바에서 일반적으로 어떤 메서드를 호출하여 예외가 발생하면 메서드를 호출한 쪽으로 예외를 던지는데, 리액티브 스트림즈에서는 예외가 발생하면 해당 예외를 onError 시그널과 함께 보내도록 규정
8 구독은 무제한 수의 request 호출(무한 스트림)을 지원해야 하고 최대 2^63 - 1 개의 Demand를 지원해야 함

 

2.6 리액티브 스트림즈 구현체

 

2.6.1 RxJava

  • Rx는 Reactive Extensions의 줄임말
  • RxJava는. NET 환경의 리액티브 확장 라이브러리를 넷플릭스에서 자바 언어로 포팅하여 만든 JVM 기반의 대표적인 리액티브 확장 라이브러리
  • RxJava는 2.0부터 리액티브 스트림즈 사양을 지원하기 시작

 

2.6.2 Project Reactor

  • Spring Framework 팀에 의해 주도적으로 개발된 리액티브 스트림즈의 구현체
  • Reactor 3.x 버전이 Spring Framework 5 버전부터 리액티브 스택에 포함되어 리액티브 프로그래밍의 핵심 역할을 담당

 

2.6.3 Akka Streams

  • Akka는 JVM 상에서의 동시성과 분산 애플리케이션을 단순화해주는 오픈소스 툴킷
  • Akka는 Actor Model을 적극적으로 사용하는 대표적인 기술
    • 통신은 메시지를 통해서만 이루어지고 Actor들은 서로 독립적이기 때문에 느슨한 결합과 높은 응집력이 보장됨

 

  • Akka라는 Actor 기반의 동시성 모델을 사용하는 툴킷 위에 리액티브 스트림즈를 구현한 것이 Akka Streams

 

2.6.4 Java Flow API

  • Flow API는 Reactor, RxJava, Akka Streams처럼 리액티브 스트림즈를 구현한 구현체가 아니라 리액티브 스트림즈의 표준 사양인 SPI(Service Provider Interface)로써 자바 API에 정의되어 있음
    • JDBC처럼 사용자들이 리액티브 스트림즈와 관련된 API를 사용하기 위해서 하나의 인터페이스만 바라볼 수 있는 단일 창구로서의 역할을 기대하기 때문이라고 추측

 

2.6.5 그 외

  • Android 플랫폼에서 사용하는 RxAndroid
  • Javascript에서 사용하는 RxJS
  • Kotlin 기반의 Reactive Extension인 RxKotlin
  • etc.

 

3. Blocking I/O와 Non-Blocking I/O

  • I/O는 일반적으로 컴퓨터 시스템이 외부의 입출력 장치들과 데이터를 주고받는 것을 의미
  • ex) 데이터베이스에서 데이터를 조회하거나 추가하는 작업인 DB I/O
  • ex) 웹 애플리케이션에서 다른 웹 애플리케이션으로 네트워크 통신을 할 경우 네트워크 I/O

 

3.1 Blocking I/O

  • 하나의 쓰레드 I/O에 의해 차단되어 대기하는 것을 Blocking I/O
    • ex) A 서버에서 B 서버에게 특정 데이터를 요청할 때 B 서버가 응답할 때까지 A 서버는 대기

 

https://medium.com/@firatatalay/blocking-and-non-blocking-asynchronous-nature-of-node-js-3cef1f22dde9

 

  • Blocking I/O 방식의 문제점을 보완하기 위해 멀티 쓰레딩 기법으로 추가 쓰레드를 할당하여 차단된 시간을 효율적으로 사용할 수 있지만 CPU 대비 많은 수의 쓰레드를 할당하는 멀티 쓰레딩 기법은 다음과 같은 문제점이 존재
    • 프로세스의 정보를 PCB에 저장하고 리로드하는 동안 즉 context switching 하는 동안  CPU가 다른 작업을 하지 못하고 대기
    • context switching이 많아질수록 CPU의 전체 대기 시간은 길어지기 때문에 성능 저하 유발
    • 쓰레드의 경우 프로세스 정보를 공유하므로 쓰레드 정보인 TCB만 저장 및 리로드 하기 때문에 비교적 빠르지만 이 역시 대량의 멀티 쓰레드가 지속적으로 생성될 경우 문제가 생길 수 있음
    • JVM에서는 쓰레드마다 스택 영역의 일부를 할당하는데 서블릿 컨테이너 기반의 자바 애플리케이션에 들어오는 요청 수가 임계점을 넘어서면 시스템이 감당하기 힘들 정도로 메모리 사용량이 늘어날 수 있음
    • SspringBoot의 내장 톰캣은 사용자의 요청을 효과적으로 처리하기 위해 쓰레드 풀을 사용하는데 대량의 요청이 발생하게 되어 쓰레드 풀에서 사용 가능한 유휴 쓰레드가 없을 경우 응답 지연 발생 가능

 

3.2 Non-Blocking I/O

  • Blocking I/O와 달리 쓰레드가 차단되지 않음
  • 작업 쓰레드의 종료 여부와 관계없이 요청한 쓰레드가 차단되지 않음
    • 하나의 쓰레드로 많은 수의 요청을 처리할 수 있음
    • Blocking I/O 방식보다 더 적은 수의 쓰레드를 사용하기 때문에 Blocking I/O에서 멀티 쓰레딩 기법을 사용할 때 발생한 문제점들이 발생하지 않기 때문에 CPU 대기 시간 및 사용량에 있어 효율적

 

  • 그럼에도 불구하고 Non-Blocking I/O도 문제점 존재
    • 쓰레드 내부에 CPU를 많이 사용하는 작업이 포함된 경우 성능 저하 유발
    • 사용자의 요청에서 응답까지의 전체 과정 중 Blocking I/O 요소가 포함된 경우 쓰레드가 차단되면서 병목 구간이 발생할 수 밖에 없기 때문에 Non-Blocking I/O의 이점을 발휘하기 힘듦

 

3.3 Spring Framework에서의 Blocking, Non-Blocking I/O

  • Spring MVC 기반의 웹 애플리케이션은 Blocking I/O 방식을 사용
  • 앞서 Blocking I/O의 단점으로 언급한 문제점들이 Spring MVC에서 그대로 나타나기 때문에 대안으로 나온 것이 Spring WebFlux

 

  Spring MVC Spring WebFlux
I/O 방식 Blocking I/O Non-Blocking I/O
쓰레드 request 당 하나의 쓰레드를 사용 적은 수의 쓰레드로 많은 수의 요청을 처리

 

 

3.4 Spring WebFlux 도입 시 고려사항

Spring WebFlux가 Non-Blocking I/O 방식을 지원하기 때문에 Blocking I/O 방식인 Spring MVC에 비해 성능적 우위를 보이지만 무조건적으로 도입하기보다는 현실적으로 고려해야 할 사항이 몇 가지 있습니다.

 

3.4.1 러닝 커브

  • Spring MVC의 경우 DI, AOP와 같은 Spring Framework의 핵심 개념들을 어느정도 이해하고 있다는 가정하에 학습 난이도가 비교적 낮음
  • Spring WebFlux의 경우 앞서 언급한 Spring Framework의 핵심 개념들 뿐만 아니라 리액티브 스트림즈라는 표준 사양을 구현한 구현체를 능숙하게 사용할 줄 알아야 하기 때문에 비교적 러닝 커브가 가파름

 

3.4.2 리액티브 프로그래밍 경험이 있는 개발 인력 확보하기 어려움

  • 프로덕트 개발 후 운영 단계로 접어들 경우 SM 인력들을 확보해야하는데 Spring MVC 경험이 있는 인력은 확보하기 용이하지만 선언형 프로그래밍이자 Non-Blocking I/O 방식인 리액티브 프로그래밍 지식을 갖춘 숙련된 개발 인력을 확보하는 것은 상대적으로 어려움
  • 이 때문에 Spring WebFlux 기반의 프로젝트는 개발 인력 면이나 기술적 측면에서 위험 부담을 더 감수해야 할 가능성이 높음

 

3.5 Non-Blocking I/O 방식의 통신이 적합한 시스템

 

3.5.1 대량의 요청 트래픽이 발생하는 시스템

  • 애플리케이션에서 발생하는 요청 트래픽이 충분히 감당할 수준일 경우 서블릿 기반의 Blocking I/O 방식의 애플리케이션으로 충분
  • 하지만 대량의 요청 트래픽으로 인해 병목 현상이 자주 발생할 경우 Spring WebFlux 기반 애플리케이션으로 전환을 고려해 볼 만함
    • 단순히 서버 scale-up, scale-out을 하기에는 투입되는 비용 대비 효율적이지 않을 수 있음
    • Spring WebFlux 도입 시 상대적으로 적은 컴퓨팅 파워를 사용함에 따라 저비용으로 고수준의 성능을 이끌어낼 수 있음

 

3.5.2 마이크로 서비스 기반 시스템

  • 마이크로 서비스 기반의 시스템은 시스템 특성상 서비스들 간 많은 수의 I/O가 지속적으로 발생
    • 특정 서비스들 간의 통신에서 Blocking으로 인한 병목 현상 발생 시 해당 서비스뿐만 아니라 다른 서비스에도 영향을 끼칠 확률이 높음
    • 이 때문에 Spring WebFlux 같은 Non-Blocking  I/O 방식의 기술이 반드시 필요

 

3.5.3 스트리밍 또는 실시간 시스템

  • 리액티브 프로그래밍은 HTTP 통신이나 데이터베이스 조회와 같은 일회성 연결 뿐만 아니라 끊임없이 들어오는 무한한 데이터 스트림을 전달받아 효율적으로 처리 가능

 

4. 리액티브 프로그래밍을 위한 사전 지식

  • 자바 8+ 버전부터 람다 표현식이 도입됨에 따라 함수형 프로그래밍 기법을 사용할 수 있게 됨
    • 정확히는 함수형 인터페이스를 이용해서 함수형 프로그래밍과 같은 효과를 낼 수 있게 됨

 

4.1 함수형 인터페이스

  • 함수형 인터페이스는 단 하나의 추상 메서드만 정의되어 있음
  • 함수형 프로그래밍 세계에서는 함수를 값으로 취급하기 때문에 어떤 함수를 호출할 때 함수 자체를 파라미터로 전달할 수 있음
  • 자바에서도 이렇게 함수를 값으로 취급할 수 있는 기능이 자바 8 버전부터 추가되었고 그것이 바로 함수형 인터페이스
    • 자바 8 버전 이전에도 단 하나의 추상 메서드만 있는 인터페이스는 존재했기 때문에 함수형 인터페이스를 사용한 것이나 마찬가지
    • ex) Comparator 인터페이스

 

함수형 인터페이스인 Comparator

 

부연 설명

  • compare()라는 단 하나의 추상 메서드가 정의되어 있고 함수형 인터페이스를 나타내는 @FunctionalInterface 어노테이션이 붙어있음
  • 자바 8부터 지원하는 default 메서드로 reversed(), thenComparing() 같은 메서드들이 정의되어 있지만 메서드 개수 자체는 한 개가 아니지만 추상 메서드는 단 하나

 

4.2 람다 표현식

  • 인터페이스의 익명 구현 객체를 전달하던 방식을 조금 더 함수형 프로그래밍 방식에 맞게 표현할 수 있는 방법
  • 람다 표현식은 단 하나의 추상 메서드를 가지는 인터페이스인 함수형 인터페이스를 구현한 클래스의 메서드 구현을 단순화한 표현식
    • 함수형 인터페이스의 메서드를 람다 표현식으로 작성해서 다른 메서드의 파라미터로 전달할 수 있음
    • 람다 표현식에서는 메서드 파라미터의 타입이 내부적으로 추론되어 생략 가능함
    • 정리하자면 함수형 인터페이스를 구현한 클래스의 인스턴스를 람다 표현식으로 작성해서 전달

 

https://www.geeksforgeeks.org/lambda-expressions-java-8/

 

4.3 메서드 레퍼런스(Method Reference)

  • 람다 표현식을 사용하여 함수형 인터페이스의 추상 메서드를 간결하게 작성할 수 있는데 이를 조금 더 간결하게 작성할 수 있는 방법이며 유형은 네 가지
    • ex) System.out.println() -> System.out::println

 

https://www.linkedin.com/pulse/journey-method-references-unleash-power-simplicity-md-jewel/

 

4.3.1 ClassName::static method 유형

  • 가장 흔한 유형으로 클래스의 정적 메서드

 

 

부연 설명

  • commons-lang 3의 StringUtils upperCase 메서드 파라미터는 람다 파라미터로 전달되는 값을 사용하는데 컴파일러가 내부적으로 람다 표현식의 파라미터 형식을 추론할 수 있기 때문에 람다 파라미터와 upperCase 메서드의 파라미터 부분을 생략 가능

 

4.3.2 ClassName::instance method 유형

  • 두 번째 유형은 클래스에 정의된 인스턴스 메서드

 

 

부연 설명

  • String 클래스의 인스턴스 메서드인 toUpperCase() 메서드 사용
  • 람다 표현식의 형식 추론에 의해 메서드 레퍼런스로 축약이 가능

 

4.3.3 object::instance method 유형

  • 세 번째 유형은 클래스의 객체인 object::instance method 형태
  • 외부에서 정의된 객체의 메서드를 호출할 때 사용

 

 

부연 설명

  • 외부 클래스인 PaymentCalculator 인스턴스의 getTotallPayment() 메서드 호출

 

4.3.4 ClassName::new 유형

  • 네 번째 유형은 생성자
  • 람다 표현식의 내부에서 어떤 클래스의 생성자를 사용해야 될 경우, 람다 표현식에서 형식 추론이 가능해지면 ClassName::new와 같이 축약 가능

 

 

4.4 함수 디스크립터(Function Descriptor)

  • 리액티브 프로그래밍 학습을 진행하다 보면 람다 표현식의 파라미터 개수나 파라미터 타입, 반환 타입 등을 보면서 해당 람다 표현식이 자바에서 지원하는 어떤 함수형 인터페이스에 해당되는지 알아야 할 때가 있음
  • 함수 디스크립터는 함수 서술자, 함수 설명자 정도로 이해할 수 있는데, 실제로는 일반화된 람다 표현식을 통해서 해당 함수형 인터페이스가 어떤 파라미터를 가지고 어떤 값을 반환하는지 설명해 주는 역할

 

함수형 인터페이스 함수 디스크립터
Predicate<T> T -> boolean
Consumer<T> T -> void
Function<T, R>
T -> R
Supplier<T> () -> T 
BiPredicate<L, R> (L, R) -> boolean
BiConsumer<T, U> (T, U) -> void
BiFunction<T, U, R> (T, U) -> R

 

4.4.1 Predicate

  • 구현해야 되는 추상 메서드가 하나의 파라미터를 가지고, 반환 값으로 boolean 타입의 값을 반환하는 함수형 이터페이스
  • 결과적으로 `T -> boolean`은 T 타입의 람다 파라미터와 boolean 타입의 값을 반환하는 Predicate의 함수 디스크립터가 되는 것

 

 

부연 설명

  • test()라는 하나의 메서드를 구현해야 되는데 해당 test() 메서드는 파라미터가 T 타입이고 boolean 값을 반환
  • 가장 흔하게 볼 수 있는 적용 사례는 stream의 filter 메서드
    • filter 메서드는 파라미터로 Predicate을 가지며 filter 메서드 내부에서 해당 Predicate을 사용하여 test 메서드의 리턴 값이 true인 데이터만 필터링

 

 

4.4.2 Consumer

  • 데이터를 소비하는 역할을 수행하며 데이터를 소비한다는 의미는 반환 값이 없다는 의미와 같음
  • Consumer의 함수 디스크립터를 보면 `T -> void`라고 되어 있는데 이는 Consumer에 정의된 추상 메서드의 파라미터가 T 타입이고 반환 값이 없다는 의미

 

 

부연 설명

  • accept()라는 하나의 메서드를 구현해야 되는데, accept() 메서드는 파라미터가 T 타입이고 반환하는 값은 없음
  • Consumer는 전달받은 데이터로 어떤 처리를 하지만 결과 값을 반환할 필요가 없을 경우 사용
    • ex) Scheduler에 의해 주기별로 특정 작업을 수행한 후, 반환 값을 리턴할 필요 없는 경우가 대부분인 배치 처리

 

 

부연 설명

  • 필터링된 암호 화폐를 람다 표현식과 함께 addBookmark() 메서드의 파라미터로 전달
  • saveBookmark() 메서드의 리턴 타입이 없으므로 Consumer를 사용하기에 적절

 

4.4.3 Function

  • 추상 메서드가 하나의 T 타입 파라미터를 가지며 반환 값으로 R 타입의 값을 반환

 

 

부연 설명

  • Function은 apply라는 하나의 메서드를 구현해야 되는데 해당 메서드는 파라미터가 T 타입이고, 반환 타입이 R 타입
  • 함수 내에서 어떤 처리 과정을 거친 후 그 결과로 특정 타입의 값을 반환하는 전형적인 함수 역할을 하기 때문에 Function이라는 이름을 가짐

 

4.4.4 Supplier

  • 추상 메서드가 파라미터를 갖지 않으며 반환 값으로 T 타입의 값만 반환

 

 

부연 설명

  • get이라는 하나의 메서드를 구현해야 되는데, get 메서드는 파라미터가 없으며 반환 타입이 T 타입인 값을 반환
  • Supplier는 이름 그대로 어떤 값이 필요할 때 데이터를 제공하는 용도로 사용

 

4.4.5 BiPredicate, BiConsumer, BiFunction

  • Bi로 시작하는 함수형 인터페이스는 함수형 인터페이스에서 구현해야 하는 추상 메서드에 전달하는 파라미터가 하나 더 추가되어 두 개의 파라미터를 가지는 함수형 인터페이스
  • 자바에서 지원하는 기본 함수형 인터페이스의 확장형

 

참고

  • 스프링으로 시작하는 리액티브 프로그래밍 (황정식 저자)
반응형