2024년 3월 7일

코딩 - Cloud Ready : Spring Boot & Oracle Object Storage

◼︎ 환경

  • 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
  • Cache : ehcahce 2.10.9.2, Guava 31.1-jre
  • DBMS : MySql 8.0.33
  • Cloud : OCI (free tier account)

OCI 오브젝트 스토이지 버킷 생성

OCI 오브젝트 스토리지 버킷 생성은 다음 절차로 진행한다. ( 계정은 free tier 을 이용하였다. )

※ 스토이지 버킷(Storage Bucket)는 대부분의 클라우드 기반 객체 스토리지 서비스에서 사용되는 컨테이너 개념으로 Amazon S3, Google Cloud Storage, Microsoft Azure Blob Storage, Oracle Cloud Object Storage 등에서 사용된다. 파일이나 데이터를 버킷에 저장하면 클라우드 제공 업체가 제공하는 다양한 기능(데이터의 보관, 버전 관리, 접근 제어, 보안, 데이터 복제)을 활용할 수 있다.
  1. OCI 콘솔 왼쪽 상단의 햄버거 아이콘을 클릭
  2. 스토이지 를 클릭합
  3. 오브젝트 스토리지 및 아카이브 스토리지 아래에 있는 "버킷"을 클릭
  4. 버킷 생성 버튼을 클릭. 팝업 창이 열리면 "생성"을 클릭(기본 구성 유지)





API 연계를 위한 개인키 구성 파일 생성

프로그램에서 OCI 오브젝트 스토리지와 연결하려면 개인 키와 구성 파일을 생성해야 한다. 생성은 아래 절차로 진행한다.
  1. OCI 콘솔에서 프로필 세부 정보 페이지로 이동 
  2. API 키 탭으로 이동
  3. API 키 추가 버튼을 클릭 



  1. 프라이빗 키 다운로드 버튼을 클릭 파일을 다운로드 
  2. 다운로드한 비밀키 파일을 스프링 부팅 애플리케이션 루트에 복사
  3. 추가 버튼을 를 클릭 API 키를 추가 
  4. 구성 파일 미리보기 복사 링크를 클릭하여 구성파일 내용을 복사
  5. 스프링 부팅 애플리케이션 프로젝트 루트 폴더에 파일 확장자 없이 "config"라는 이름의 파일을 만들고 복사한 콘텐츠를 추가.  config 파일에서 다운로드한 개인 키가 저장된 경로를 업데이트 (<path to your private keyfile>)




스프링 부트 의존성 추가 

스프링 부트 애플리케이션 프로젝트에 OCI Java SDK 사용을 위한 의존성을 build.gradle 파일에 추가한다. 추가되는 의존성은 아래와 같다.

  • implementation 'com.oracle.oci.sdk:oci-java-sdk:3.36.0'
  • implementation 'com.oracle.oci.sdk:oci-java-sdk-objectstorage:3.36.0'
  • implementation 'com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey:3.36.0'


OCI 오브젝트 스토리지 클라이언트 클래스 구현

다음으로 앞서 생성한 OCI 오브젝트 스토리지 접속을 위한 클라이언트 클래스를 구현한다.


SDK 에서 제공하는 API 는 앞서 저장한 OCI config file 과 비밀키를 읽어드리기 위하여 파일위치를 지정하도록 되어 있다. 배포의 용의성을 위해서 프로젝트 리소스에서 읽어드릴 수 있도록 SDK 소스를 일부를 아래와 같이 수정하여 사용하였다. (src/main/resources/oci 경로에 config 와 privite_key.pem 파일 복사, config 파일 key_file 값을 oci/private_key.pem 수정 )
  1. ConfigFileAuthenticationDetailsProvider -> ClasspathConfigFileAuthenticationDetailsProvider.java
  2. SimplePrivateKeySupplier.java -> ClasspathPrivateKeySupplier.java


OCI 오브젝트 스토리지 테스트 클래스 구현

OCI 오브젝트 스토이지에 파일을 추가하려면 먼저 버킷이 필요하다. 생성된 버킷 목록을 조회하려면 compartmentId 와 namespaceName 값이 요구된다. 

⑴ namespaceName 값은 우측 상단 사용자 아이콘을 클릭하고, 테넌시 이름을 클릭 테넌시 상세 페이지로 이동한다. 테넌시 정보 구획에서 오브젝트 스토이지 네임스페이스 값이 namespaceName 이다.  




⑵ compartmentId 는 OCI 콘솔 > ID& 보안 > 구획 메뉴를 통하여 확인 할 수 있다. 목록에서 이름을 클릭, "구획정보"섹션에서 "OCID"를 값을 확인할 수 있는데 이 값이 해당 구획(compartment) 의 compartmentId 값이다.


OCI 오브젝트 스토이지에 파일을 추가하는 것은 PutObjectRequest 을 생성하여 쉽게 구현할 수 있다. 주의할 점은 object name 은 버킷 내에서 고유 해야 한다는 점이다. 



이제 OCI 오브젝트 스토이지에 존재하는 파일 목록을 조회하는 테스트 코드를 만들어 본다. 오브젝트 스토이지는 위에서 생성한 것을 사용하였다.


bucket name 과 namespace 정보는 OCI 콘솔 > 스토이지 > 오브젝트 스토리지 및 아카이브 스토리지 버킷 목록 화면에서 생성된 버킷 이름을 클릭 상세 화면에서 확인할 수 있다.




파일 목록을 조회하고 객체에 대한 사전 인증된 요청 URL 을 생성하는 테스트 코드를 만들어 본다.

생성된 URL 은 1시간 동안 인증이 유효하며 사전 인증 요청들은 OCI 콘솔 > 스토이지 > 오브젝트 스토리지 및 아카이브 스토리지 > 버킷 > 버킷 세부정보의 좌측 리소스 목록중 사전인증된요청 을 클릭하면 내역을 확인할 수 있다. 

 

참고자료


2024년 2월 23일

코딩 - Cloud Ready : Spring Config Server

Spring Boot는 마이크로서비스 아키텍처(Microservices Architecture) 를 구현하는 데 많은 도구와 기능을 제공하고 있다. 이를 통해 애플리케이션을 작은 독립적인 서비스로 분할하고, 이러한 서비스를 개발, 배포 및 관리하는 데 도움을 준다.



이러한 기능중에서 유연성, 안정성, 보안성 향상을 위하여 설정의 집중화를 구현하는 Spring Cloud Config는 애플리케이션의 설정을 외부화하고 중앙에서 관리할 수 있게 한다. 이를 통해 설정 변경을 용이하게하고 여러 환경 간에 설정을 공유할 수 있다.

Config Server 구성하기


◼︎ 환경
  • 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
  • Cloud : Oracle Cloud Free Tier

Spring Cloud Config 서버 프로젝트는  ① gradle 명령을 사용하여 새로운 자바 응용프로그램 프로젝트를 생성하고 여기에 관련 의존성과 @SpringBootApplication 를 구현하는 클래스를 생성하는 방법과 ② Spring Initializer 을 이용하여 프로젝트를 다운로드하는 방식이 있다. 

① Gradle

brew 을 이용하여 gradle 을 설치한다. 


# brew update
# brew install gradle
# gradle init --type java-application

 Select build script DSL:
  1: Kotlin
  2: Groovy
 Enter selection (default: Kotlin) [1..2] 2

 Select test framework:
  1: JUnit 4
  2: TestNG
  3: Spock
  4: JUnit Jupiter
 Enter selection (default: JUnit Jupiter) [1..4] 4

 Project name (default: studio-config): 
 Enter target version of Java (min. 7) (default: 21): 11
 Generate build using new APIs and behavior (some features may change in the next minor release)?   (default: no) [yes, no] yes

 > Task :init
 To learn more about Gradle by exploring our Samples at   https://docs.gradle.org/8.6/samples/sample_building_java_applications.html

 BUILD SUCCESSFUL in 2m 17s
 1 actionable task: 1 executed

다음으로 build.gradle 파일에 org.springframework.cloud:spring-cloud-config-server 의존성을 추가한다.

build.gradle

dependencies {
implementation 'org.springframework.cloud:spring-cloud-config-server'
// lombok
implementation 'org.projectlombok:lombok'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}

springCloudVersion 속성은 gradle.properties 에 정의했다.

springBootVersion=2.7.18
springDependencyManagementVersion=1.1.4
springCloudVersion=2021.0.8
sourceCompatibility=11


가장 어려웠던 부분이 spring cloud version 을 확인하는것 과정이었다. 

마지막으로 @EnableConfigServer 을 구현하는 서버 소스를 추가한다. (소스는 ② 생성되는 코드들을 참고했다)

package architecture.studio.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class ConfigServer {

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

}

② Spring Initializer

브라우저를 열고 https://start.spring.io/ 에 접속한다.


config server 의존성을 선택하여 새로운 config server 프로젝트를 생성한다. (2.x 버전은 지원하지 않고 있다.)

Config Server 프로젝트 생성

이미 프로젝트가 존재했고 해당 프로젝트에서 config 을 분리하는 것으로 목표로 했기 때문에 ① 방식을 참고하여 서브 프로젝트를 추가하고 생성된 프로젝트에 설정을 추가하는 방식으로 구현했다.

존재하는 spring boot 프로젝트에 config-server 를 추가하는 경우는 "Supported Versions" 참고하여 호환되는 Spring Cloud 버전을 확인 하려 하였으나 https://spring.io/projects/spring-cloud 문서가 더 도움이 되었다.  문서에 따르면 2.6.x, 2.7.x 버전은 2021.0.x aka Jubilee 버전이 지원하고 있다.


2021.0 정보는 https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-2021.0-Release-Notes 확인 가능한데  2024년 2월 기준으로 '2021.0.8' 이 가장 최신이다. 이제 해당 버전으로 의존성를 추가한다.

설정정보 저장소

대부분의 자료들이 github 을 저장소로 사용하여 config server 을 구성하고 있는데 로컬 파일을 사용하는 방식으로 구성했다. 로컬 파일들은 jar 파일에 포함되어 배포 될수 있도록 src/main/resources/config 경로에 [application]-[profile].yml 규칙으로 생성하였다. 주의 할것은 이 파일들은 "--" 문자를 포함하면 오류가 발생한다. 


config server 에 대한 설정은 application.yml 에 아래와 같이 설정하였다.

application.yml
---
logging:
level:
root: debug
---
server:
port: 8888
spring:
application:
name: studio-config
profiles:
active: native
cloud:
config:
server:
native:
searchLocations: classpath:/config, classpath:/config/{application}, classpath:/config/{application}/{profile}


웹 브라우져에서 http://localhost:8888/[application]/[profile] 형식으로 호출하여 프로파일에 따른 설정을 확인할 수 있다.

암호화 

비밀번호와 같은 중요 정보들을 암호화하여 설정 파일을 구성하기 위한 목적으로  Config Server 가 제공하는 암호화 기능을 사용하였다 

Config Server 는 암호화 구현을 위하여 대칭(공유) 키 또는 비대칭 키(RSA 키 쌍)를 사용하여 할 수 있다. 비대칭 키가 보안 측면에서 더 우수하지만, 대칭 키 방식은 application.yml(application.properties) 파일에  에 대칭 키 문자 하나만 설정하면 되기 떄문에 더 편리하다고 할 수 있다.

대칭 키를 설정은 encrypt.key 에 개인 키 암호를 설정해야 한다.  (또는 ENCRYPT_KEY 환경 변수를 사용하여 값을 지정하는 방법도 있다.)

비대칭 키 방식은  keystore (JDK 에 포함된 keytool 유틸리티로 생성) 을 사용하는 이유에서  아래 설정들이 필요하다.
  • encrypt.keyStore.location : keyStore 파일 위치
  • encrypt.keyStore.password : KeyStore 파일에 접근을 위한 비빌번호
  • encrypt.keyStore.alias : KeyStore 내의 엔트리를 식별하는 고유한 이름. KeyStore는 공개 키, 개인 키 쌍 및 이에 대한 인증서를 저장하는 데 사용되는데, 이러한 엔트리는 각각 고유한 이름인 alias로 식별됨
※KeyStore에 저장되는 개별 항목을 "엔트리(entry)"라고 한다. 이 엔트리는 주로 공개 키와 개인 키 쌍과 그에 대응하는 인증서를 포함한다.

KeyStore 를 생성하고 비대칭키를 설정하는 것은  "Spring Cloud Config - Creating a Key Store for Testing" 참고했다.

참고로 긴 기간동안 키가 유효하도록 아래와 같이 10년동안 유효하게 키 스토어를 생성했다. 

keytool -genkeypair -alias testalias -keyalg RSA -keysize 2048 \
  -dname "CN=ConfigServer, OU=none, O=none, L=none, ST=none, C=KR" \
  -keystore server.jks -keypass testkeypass  \
  -storepass teststorepass -validity 3650

생성된 키는 src/main/resources/key 경로에 복사하고 아래와 같이 application.yml 파일에 설정을 추가했다.

application.yml
keyStore:
location: classpath:/key/server.jks
password: *****
alias: studiokey
secret: *****

암호화된 문자는 접두사 {cipher} 사용하여 설정하고 Config Server 은 요청시 설정정보를 복호화하여 전송한다.

application.yml
spring:
datasource:
url: "{cipher}AQCnTtoaV9juWvUutYNWNVyx9Bd+E9zUf+Mf1"
username: "{cipher}AQCHbvX1Sa9VyjTfaFLP2uSesfTCe0Wn"
password: "{cipher}AQBUIaBt+nzhl+mbyOlSb5MStLWkvZ1wjW2iwByefg34aLs="


암호화

암호화를 위한 키 설정을 하고 나면 ConfigServer 가 제공하는 암호화와 복호화를 RESTful API 을 호출하여 데이터를 암호화 하고 값을 {cipher} 을 접두사로 하여 application.yml 파일에 값을 수정한다.  

Config Server 는 /encrypt 및 /decrypt 엔드포인트를 공개하고 있다(이들 엔드포인트는 안전하고 인증된 클라이언트만 접속 가능한 것으로 가정). 만약 설정 파일을 값을 암호화 하고자 하는 경우 아래와 같이 /encrypt 엔드포인트를 POST 로 호출하여 암호화 할 수 있다. 

curl -X POST \

  -H "Content-Type: application/json" \

  -d 'jdbc:mysql://xxx.xxx.xxx:3306/xxxdb?serverTimezone=Asia/Seoul' \          

  http://localhost:8888/encrypt


복호화 역시 동일한 방법으로 /decrypt 를 호출하면 된다.

Config Server 에서 설정 불러오기

Spring boot 프로그램에서 앞에서 생성한 Config Server 에서 설정을 읽어드리도록 하려면 ❶ Config Server 의 경우와 같이 org.springframework.cloud:spring-cloud-config-server 의존성을 추가하고 ❷
Config Server 에서 설정을 읽어드리도록 application.yml 설정을 추가한다. 

/application.yml

logging:
level:
root: INFO
spring:
application:
name: studio
config:
import: optional:configserver:http://localhost:8888


추가로 dev , prod 프로파일을 application.yml 이 아닌 테스트를 위하여 프로그램을 실행 할 떄 prod 인자를 사용하여 개발 과 운영 모드로 동작할 수 있도록 아래와 같은 설정을 build.gradle 에 추가하였다. 이를 통하여 개발과 운영을 동일한 코드로 구현하고 테스트를 위하여 프로그램을 실행할 떄 프로파일을 인자로 넘겨서 처리하도록 할 수 있었다.  

build.gradle

tasks.named("bootRun") {
if (project.hasProperty('profile')) {
jvmArgs = ["-Dspring.profiles.active=${project.getProperty('profile')}"]
}
}



./gradlew bootRun -Pprofile=dev

locathost:8888/studio/dev 에 해당하는 /config/studio-dev.yml 을 읽어드림.


./gradlew bootRun -Pprofile=prod 

locathost:8888/studio/prod 에 해당하는 /config/studio-prod.yml 을 읽어드림.


참고 설정 및 소스


2024년 2월 8일

사용기 - M1 MacBook Pro 에서 VMware Fusion 기반 윈도우 11 Arm 버전 설치하기

ARM 용 공식 윈도우 설치 ISO 파일을 가지고 있다는 가정하에 작성하였다. ISO 파일을 가지고 바로 설치가 가능한 것이 아주 편리했다.

1. VMware Fusion 을 실행하고 File > New .. (⌘N) 메뉴를 실행한다.

2. ISO 이미지 파일을 "Install from disc or image" 영역에 끌어다 놓는다.

3. Firmware Type 는 UEFI (통합 확장 펌웨어 인터페이스) 을 선택했다.  UEFI 는 시스템의 부팅 속도, 보안, 디스크 지원 및 사용자 경험을 향상시킨다고 한다. 가상 환경에서 설치할 때도 이러한 이점을 활용할 수 있으며, 대부분의 경우 UEFI 를 권장한다.
4. 암호화 옵션은  TPM (Trusted Platform Module) 지원을 위해 필요한 파일들만 암호화 함을 선택하였다. 비밀번호는 나중을 위해서 반드시 기억해야 한다. 
6. 옵션 수정 없이 진행을 계속한다. 
7. 이름을 기입하고 저장을 클릭하면 가상머신 설정이 완료된다.  다음으로 윈도우 설치가 이어 진행된다.
8. 윈도우 설치과정에서 네트워크는 인식을 하지 못하는데 "인터넷에 연결되어 있지 않음" 을 선택하고 다음으로 넘어간다.
 9. 설치를 완료하면 네트워크 연결이 불가한 것을 확인할 수 있는데 이는 아래의 방법으로 VMware Tools 을 설치를 통해서 해결할 수 있다.
  1.  VMware Fusion 메뉴바에서 Virtual Machine -> Install VMware Tools. 클릭
  2.  VMware Tools ISO 가 가상머신 D:\ 에 마운트 되고 setup.exe 파일을 실행하여 설치를 진행한다.

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) 한정하여 응답하는 경우 정상적으로 동작하는 것을 확인 할 수 있었다. 서버 역시 클라이언트에 의한 스트림이 종료되는 오류 없이 동작하였다.