2024년 1월 25일

코딩 - Data Streaming in Spring Boot

◼︎ 환경
  • Model : MacBook Pro (14-inch, 2021)
  • CPU : Apple M1 Pro
  • MENORY : 16GB
  • DISK : 512 GB SSD
  • OS : macOS 13.2.4 (22F66)
  • TOOLS : Visual Studio Code, Java 11, Gradle, Docker
  • Version Control : GitHub
  • Programming Language : Java
  • Framework : Spring Boot 2.7.12, Spring Security 5.7.7
  • DBMS : MySql 8.0.33

동영상 같이 크기가 큰 파일을 서버에서 다운로드 하는 경우 좀더 효과적인 방법이 없을까 고민을 하다 스트리밍 기술을 사용하는 것을 고민해보았다.

기술 적용에 앞서 구글 검색을 통하여 관련 자료를 찾아보았다.  (검색어 : data streaming in java, data streaming in spring boot). 

검색 결과 제시된 "Streaming Data with Spring Boot RESTful Web Service" 자료가 많은 도움이 되었다.

이어 ChatGPT 와  Bard 을 사용하여 좀더 알아 보았다. 참고로 한글 질의 응답 역시 만족할 만한 수준이었다.
 
"how to implements data streaming in spring boot. "

  • Bard : Kafka (실시간 데이터 스트리밍을 위한 분산 메시징 플랫폼) 또는 HTTP 이용하는 비동기 방법으로 답변.
  • ChatGPT: Spring Boot 환경에서 WebFlux 기술과 Reactor 을 이용한 비동기 방법으로 답변.

웹 기반의 스트리밍 구현을 목표로 하기 때문에 "HTTP Streaming" 키워드로 추가 질의를 했다,
Bard 보다는 ChatGPT 가 좀더 원하는 답변을 제시했다.

"how to implements data streaming in spring boot."

  • Bard : @StreamingResponseBody 와 stream() 함수를 사용하는 방업으로 답변.
  • ChatGPT: Spring Boot 환경에서 WebFlux 기술과 Reactor 을 이용한 비동기 방법으로 답변.

ChatGPT 답변이 적절해보였고 단일 파일을 스트리밍하는 경우이기 때문에 제시된 Flex 가 아닌 Mono 을 사용하는 것이 적절해 보였다. 추가로 "HTTP 기술에서 파일을 이어서 받기 위한 기술"을 추가로 질의 해서 "Range 요청 헤더" 을 사용하여 가능함을 확인했다. (이 질의에 대해서는 Bard 가 좀더 유용했다. Bard가 언급한 Transfer-Encoding 헤더에 "chunked" 값 설정은 Spring Boot 에서는 자동으로 처리한다고 한다. )

Spring boot 에서 데이터 스트리밍을 구현하는 방법에는 크게 3가지가 있다.

Streaming in Spring

1. HttpServletResponse’s OutputStream 

가장 오랜 방법으로 콘텐츠를 HttpServletResponse 객체의 OutputStream 에 데이터를 쓰기를하는 방법이다.  


2. StreamingResponseBody as return type

Spring Boot 환경에서 콘텐츠를 스트리밍하는 쉬운 방법은 StreamingResponseBody 객체를 사용하는 것이다. 보통 ResponseEntity 와 함께 사용하는데, 이렇게 하면 헤더와 HTTP 상태 값을 설정할 수 있어 보다 세밀하게 동작을 제어할 수 있다.

StreamingResponseBody 를 사용하는 경우 Spring MVC 에서 비동기 요청 실행을 위한 TaskExecutor 를 구성하는 것이 권장된다. 운영 환경이라면 비동기 처리를 위한 스레드 수를 제한하도록 TaskExecutor 를 구성하는 것이 좋다. 그렇지 않으면 서버 리소스에 과부하가 걸릴 수도 있다.

TaskExecutor@Configuration 어노테이션이 정의된 클래스에서 TaskExecutor 빈을 생성하고 구성하면 된다. 일반적으로 ThreadPoolTaskExecutor 구현을 사용하여 정의한다. 또는 YAML 설정을 사용할 수 있다.


YAML 파일 설정은 Java Config 에서 정의한 TaskExecutor 빈을 대체한다. (Java Config 에서 설정한 값과 YAML 파일에서 설정한 값이 충돌할 경우, YAML 파일의 값이 우선된다.) 

3. Spring WebFlux publishers

Spring WebFlux 는 Spring 5 버전에 도입된 기술로 Spring MVC 와 유사하게 동작한다. Spring WebFlux는 완전한 "Non-blocking reactive streams" 을 지원하고 있다.

"Non-blocking reactive streams" 는 비동기적이고 이벤트 기반 프로그래밍을 강조하는 리액티브 프로그래밍 패러다임과 관련된 개념이다. 이는 데이터 흐름을 비차단(Non-blocking) 방식으로 처리할 수 있는 방법을 제공하는데, 주로 Reactive Streams API 와 관련이 있다.

Reactive Streams API 에 대하여 설명하기전에 "Reactive Streams" 의 주요한 용어에 대하여 알아보자. (LINE Engineering 블로그에서 가져온 내용) 

3.1 Stream

왼쪽의 전통적 방식은 요청 메시지를 메모리에 저장한 이후에 다음 절차를 진행할 수 있으며 응답을 위하여 요구되는 모든 데이터가 메모리에 적재 되어야 응답 메시지를 만들수 있다. 이 방식은 요청 메시지 처리 과정에서 필요한 메모리가 가용 메모리 용량을 초과하게 된다면 "Out of memory" 에러가 발생하거나 지나치게 많은 요청으로 인한 GC 로 성능 이슈가 발생하게 된다.  

우측 이미지와 같이 스트림 방식을 적용하면 적은 메모리 환경에서도 많은 양의 데이터를 처리할 수 있게된다. 가상의 작업 파이프라인을 만들고 요청 데이터를 구독(subscribe) 이벤트 과정을 통하여 파이프라인에 주입하고  작업이 종료되면 발행(publish) 이벤트를 통하여 최종적으로 처리할 수 있다. 이렇게 하면 서버는 많은 양의 데이터도 탄력적으로 처리할 수 있다.

3.2 Non-blocking

동기(synchronous) 방식에선 클라이언트가 서버에 요청을 보내면 응답을 받기 전까지 블로킹(blocking) 된다. 반대로 비동기(asynchronous) 방식에서는 블로킹(blocking) 발생되지 않기 때문에 다른 일을 계속 할 수 있다. 

3.3 백 프레셔(back pressure)

데이터 스트림을 처리하는 방식 중 하나로, 소비자(consumer)가 데이터를 처리하는 속도를 생산자(producer)에게 통지하여 데이터의 생성 속도를 조절하는 메커니즘이다. 이는 데이터의 과도한 생성으로 인한 자원 소모나 성능 저하를 방지하기 위한 중요한 개념 중 하나로, 아래와 같은 상황에서 필요하다:

  • 데이터 소비자가 처리 속도가 느린 경우: 생산자는 무한히 데이터를 생성할 수 있지만, 소비자가 처리하지 못하면 메모리가 과도하게 소비되고 성능이 저하될 수 있다.
  • 네트워크 통신에서의 부하: 네트워크 통신에서 데이터를 전송하는 경우, 데이터의 양이 처리 속도를 초과할 수 있다. 이런 경우 백 프레셔를 통해 생산자와 소비자 간의 조절이 필요하다.




백 프레셔는 Reactive Streams 프로토콜에서 주로 사용되며, Reactive Streams API는 이를 구현한 표준을 제공한다. API는 백 프레셔의 세 가지 상태를 정의하고 있다.

  • REQUESTED: 소비자가 몇 개의 아이템을 처리할 준비가 되었다고 통지하는 상태. 이 상태에서만 생산자는 데이터를 전송할 수 있다.
  • PENDING: 데이터를 요청한 상태이지만 아직 처리되지 않은 상태. 이 상태에서는 생산자가 데이터를 생성하지 않아야 한다.
  • CANCELLED: 소비자가 더 이상 데이터를 원하지 않거나 구독을 취소한 상태.

이러한 상태 전이를 통해 생산자와 소비자 간의 균형을 맞추어 데이터의 안정적인 전송을 가능하게 한다.

3.4 Reactive Streams API

Reactive Streams API는 자바 생태계에서 비동기적이고 이벤트 기반 프로그래밍을 지원하기 위한 표준 인터페이스와 프로토콜을 제공하는 API 이다. 이 API는 자바 9에서 java.util.concurrent.flow 패키지의 일부로 소개되었다.


Reactive Streams API의 주요 목표는 다음과 같다:
  • 비동기 및 이벤트 기반 프로그래밍: Reactive Streams는 비동기적인 환경에서 데이터나 이벤트의 효율적인 처리를 위한 기본 도구를 제공.
  • 백프레셔(Backpressure) 관리: 백프레셔는 소비자(구독자)가 생산자(발행자)에게 데이터를 처리할 수 있는 속도를 제어할 수 있는 메커니즘을 의미한다. Reactive Streams API는 백프레셔를 지원하여 데이터의 효율적인 처리를 가능하게 한다.
Reactive Streams API는 다음과 같은 핵심 인터페이스로 구성되어 있다:
  • Publisher (발행자): 데이터나 이벤트의 생산자를 나타내며, 여러 개의 구독자에게 데이터를 전송.
  • Subscriber (구독자): 데이터나 이벤트의 소비자를 나타내며, 발행자로부터 데이터를 받음.
  • Subscription (구독): 발행자와 구독자 간의 연결을 제어하며, 백프레셔를 통해 데이터 흐름을 조절.
Reactive Streams API는 다양한 자바 라이브러리와 프레임워크에서 지원되고 있으며, 대표적으로는 Project Reactor, RxJava, Akka Streams, LINE Armeria 등이 있다. 이러한 라이브러리들은 Reactive Streams API의 기본 규약을 따르면서 각각의 특성과 확장성을 제공한다.

3.5 WebFlux

Spring WebFlux 는 스프링 5에서 소개된 리액티브 프로그래밍을 지원하는 모듈 중 하나로, 비동기 및 이벤트 기반 애플리케이션을 개발하기 위한 스프링의 리액티브 스택의 일부이다. 주로 Reactive Streams API를 기반으로 한 Project Reactor 를 사용하여 리액티브 프로그래밍을 지원하고 있다.

Spring WebFlux는 반응형 웹 애플리케이션을 구축하기 위한 다양한 컴포넌트와 기능을 제공한다. 예를 들어, reactive 한 컨트롤러, WebClient를 통한 비동기 HTTP 클라이언트, Flux 및 Mono와 같은 Project Reactor의 타입을 지원한다.

Spring Boot에서 reactive 프로그래밍을 하려면 일반적으로 다음과 같이 의존성을 추가하면 된다. 


Streaming in Spring

아래와 같은 가상의 목표를 설정하고 스트리밍 기술을 적용해보았다.

"큰용량의 파일을 스트리밍 기술을 적용하여 서버 자원을 독점하지 않게 하는 동시에 클라이언트(브라우져) 에서 역시 더 나은 경험을 제공한다." 

데이터를 스트리밍하는 여러가지 방법이 있었지만 간단하게 구현이 가능한 Reactive Mono 기술을 적용 동영상 파일을 스트리밍하는 코드를 작성해보았다. 

Reactive Mono는 Project Reactor 라이브러리에서 제공하는 개념 중 하나로 Reactive Streams API 스펙의 Publisher 구현체 중 하나이다. (Reactor는 Mono와 Flux라는 두 가지 주요 유형의 Publisher를 제공한다.)

Mono: Mono는 0 또는 1개의 요소를 발행할 수 있는 Reactive Streams 이다. Mono 는 단일 값 또는 오류를 발행(Publish)하고, 성공적으로 완료되었거나 아무 값도 발행하지 않았을 때 스트림(Stream)이 완료된다. 주로 비동기 작업의 결과물을 처리하거나 단일 값의 처리를 위해 사용된다.

파일 스트리밍은 비동기 방식의 단일 응답을 위한 Mono 와 RFC 7233 - Hypertext Transfer Protocol (HTTP/1.1): Range Requests 기술을 적용 클라이언트는 데이터 범위를 지정하여 요청하고 서버는 해당 범위의 데이터를 응답하도록 구현했다.

 
구현결과 확인을 위하여 간단하게 Vue 을 기반으로 동영상 파일을 video.js 을 사용하여 재생하는 기능을 구현하여 테스트 해보았다. 서버는 디폴트로 5M 단위로 나눠 응답하도록 했다. 
  • 나눠서 응답을 하는지 여부 ( 5M 단위로 ) :  나누어 스트리밍 방식으로 응답
  • 브라우저는 전체 파일을 응답받은 이후에 영상을 재생하는지 여부 : 나누어 응답을 받으면 바로 재생


작업을 하면서 한가지 이슈는 서버에서 5MB 단위로 나눠서 시도를 하는 경우에 클라이언트에 의하여 스트림이 강제로 종료되는 현상이 발생하였다. 클라이언트는 크롬 브라우저를 사용했는데 2번째 요청 건의 Content-Range: 값에서 특이점을 발견하였다. 예상했던것과 다르게 순차적으로 데이터를 요청하지 않는 것이다. 서버 역시 응답을 하는 동안에 클라이언트에 의해서 스트림이 종료되는 오류가 발생되었다. 
  1. Range:bytes=0- Content-Range:bytes 0-5242880/32625996
  2. Range: bytes=5242881- Content-Range: bytes 5242881-10485761/32625996
  3. Range: bytes=3604480- Content-Range: bytes 3604480-8847360/32625996
  4. Range: bytes=4390912- Content-Range: bytes 4390912-9633792/32625996
  5. Range : bytes=6291456- Content-Range: bytes 6291456-11534336/32625996
원인을 알수는 없었지만 서버가 응답 데이터의 크기를 3.59M (3768320) 한정하여 응답하는 경우 정상적으로 동작하는 것을 확인 할 수 있었다. 서버 역시 클라이언트에 의한 스트림이 종료되는 오류 없이 동작하였다. 


댓글 없음:

댓글 쓰기