[Tech] Java 21을 왜 프로젝트에 적용하려고 하였나

 

📣 9월 19일, Java 21이 출시됐대


 

이 프로젝트를 시작했던 10월 17일 기준으로 Backend팀에서는 출시한 지 얼마 안 된, Java 21 RC(정식 출시 후보버전)를 프로젝트에 적용하고자 하였다.

세 번의 프로젝트를 진행하며 SpringBoot 기반 백엔드 포지션을 주로 맡았기에 이번에는 신버전을 적용하며 이전과는 다른 경험의 차별점을 가질 수 있을까가 가장 먼저 든 생각이었다.

그렇게 기획 단계에서 Java 21 기능 핵심 요약 영상들을 찾아보고서 오랜만에 OS 개념도 복기하며 영상 몇 개를 재밌게 보았다.

(🔗: YOUTUBE - 가상스레드 특징, 최범균님)

 

그리고 그 버전의 호기심을 가지는 것에서 한 발짝 더 나아가면 다음과 같은 점을 고려해야 했다.


1. 현재 프로젝트의 요구사항에 대하여 적합하거나 개선점이 존재하는가?
2. 해당 버전으로 인해 다른 도구와의 호환 문제가 일어나지 않을 것인가?

(프로젝트를 완료하였기에 결과부터 말하자면 1번에 대해서는 JMeter 성능테스트 기반으로 성능 향상을 확인했고, 2번은 호환 문제가 없지 않아 있었다.) 

 

 위와 같은 점을 고려하기 위해서는 정리할 내용이 몇 있었으며, 그 정리 내용들을 아래에 옮겨 적었다.

 

 

 

1️⃣OS-Thread와 JAVA-Thread


✔ OS 레벨의 Thread부터 살펴보자,  그 전에 프로세스와 스레드의 개념을 다시 보자

기존 OS 지식에 의하면, Process는 '실행되는 프로그램 단위'이며 Thread는 'Process 내의 실행되는 흐름 단위'이다.

✔ Thread의 특징

우선, Stack 영역을 제외하고 Code, Data, Heap 영역을 공유하고 있다

  • Code: 프로그램의 실행 코드
  • Data: 초기화된 정적 변수 및 전역 변수
  • Heap: 동적 할당으로 생성된 객체 혹은 데이터 (메모리 할당 후 반환)
  • Stack: 함수 호출 시 생성되는 지역변수와 호출 정보

즉, 한 Thread가 Process 자원을 변경한다면 같은 Process 내에 존재하던 Thread가 변경 사항이 반영된 채로 자원에 접근하게 된다.
 여기서 기억할 것은 Thread 간의 공유 자원이 있으므로 충돌 가능성이 존재함이다.

 

✔ JAVA의 Thread(플랫폼 스레드)는 무엇인가

 

Platform Thread(기존 자바의 Thread 방식)

  • OS-level Thread를 랩핑(wrapping)하여 사용했다
  • JAVA에서 스레드를 이용한 코드는 실제 OS Thread를 이용하는 방식으로, 한계가 존재했다
  • SpringBoot 같은 Application은, 사용자의 요청 당 하나의 스레드(Thread for Request)를 사용하며, 한계가 존재했다

 

✔ OS Thread를 이용하는 것은 왜 문제인가

JAVA는 OS-level 스레드를 이용한다

  • OS-level 스레드에 직접 매핑되기에, 스레드 생성 시 리소스 소모(메모리 및 CPU 소모량이 많음)가 크게 발생하며 이로 인한 Context Switching비용도 높다
  • OS Kernel에서 사용할 수 있는 Thread 개수 자체가 제한적이다

JAVA는 스케일링 제한이 있다

  • 각 Thread가 고정된 Memory Stack을 할당받기에 너무 많은 Thread가 생성되면 메모리 소모량이 빠르게 증가한다

 

✔ Thread per Request는 왜 문제인가

  • Thread per Request 모델에서는 요청을 처리하는 Thread에서 I/O 작업을 처리할 때 Blocking이 일어난다
  • I/O 작업이나 장시간 실행 작업 등에 의해 Thread가 차단된다면, 해당 Thread는 다른 작업을 수행할 수 없다
  • 해당 Thread는 I/O 작업이 마칠 때까지 다른 요청을 처리하지 못하고 기다려야 한다
    요청량이 많지 않거나 Scale Out으로 커버할 수 있다면 문제가 없겠지만, 어쩔 수 없이 많은 요청을 처리해야 하는, 많은 동시 연결 혹은 고성능 시스템에서는 병목을 예방하기 위해 blocking으로 인한 낭비를 줄여야 할 것이다. 이를 위해 Reactive Programming이 발전하기도 했으나.... (ft. WebFlux의 비동기)

 

 Reactive Programming은 또 왜 문제인가?

우선, 위에서 언급한 동시 연결과 병목 예방을 위해 아래와 같이 해결한다.

  • Reactive Programming은 비동기 및 이벤트 기반 접근법을 사용한다(Spring MVC 모델과 대척되는 형식이다)
  • Reactive System은 데이터 흐름과 변화의 전파를 중심으로 하며, Thread는 필요할 때만 작업을 수행하고 대기 중인 작업이 없다면 Blocking 되지 않는다

그러나, 아래와 같은 문제가 존재한다 (벽이 좀 있다)

  • 비동기 및 이벤트 기반의 코드는 이해와 디버깅이 어려움
  • 리액티브 프로그래밍 모델과 프레임워크를 배우는 데에 러닝 커브가 존재

 

2️⃣Virtual Thread



(좌: 플랫폼 스레드(전통적 방법), 우: 가상스레드(New))

 Platform Thread와 Virtual Thread의 차이

구조적 차이

Virtual Thread는 'Virtual Thread Scheduling' 작업이 추가로 존재하며, 이 스케줄링이 Blocking을 관리한다

  • Platform Thread: Blocking 발생 시 대기
  • BirtualThread: Blocking 발생 시, Scheduling을 통해 Carrier 스레드는 다른 가상스레드의 작업을 처리한다

✔ Carrier 스레드는 무엇인가

스케줄러를 통해 가상스레드를 할당하는 플랫폼 스레드

  • 가상스레드는 Blocking 발생 시, 내부 스케줄링을 통해 실제 작업을 처리하는 Carrier 스레드가 다른 가상스레드의 작업을 처리하게 된다

 

 

3️⃣Java 21을 쓰기 위한 세팅


 SpringBoot 3.2 이상

프로젝트에 Java 21을 사용하기 위해서 가장 큰 허들은 SpringBoot 3.2를 적용하는 것이었다.

기존에는 SpringBoot 2.x만 썼으니, SpringBoot 3.x대로 업그레이드하면서 이에 따른 변경들이 필요했다.

예를 들어,

  • javax패키지 -> jakarta 패키지로 변경
  • SpringFox 미지원, 따라서 SpringDoc으로 설정
  • QueryDsl 관련 설정 변경
  • SpringSecurity에서 제거된 메소드가 다수이므로 공식문서 기반 수정 필요
  •  RestClient 객체를 통한 외부 API 통신 작업(선택사항)

등이 있었다.

 

✔ application.yml에서 enbled 설정

spring:
	threads:
     	  virtual:
          	   enabled: true

 

이제 내부에서 스레드를 사용하는 코드를 쓰거나, 테스트 도구를 통해 I/O 블로킹 상황을 연출하며 성능 변화를 체크할 수 있다

 

 

4️⃣ Java 21를 통한 프로젝트 변화


✔ Timing에서는 어떤 변화를 기대할 수 있는가?

  • Timing의 SpringBoot ServerPython Server의 이미지처리 API의 호출을 전달하는 ProxySever의 역할을 한다
  • 이전의 Java 버전의 경우, Python Server의 응답시간은 3초 ~ 3분 정도의 응답이 걸리기 때문에, 일정 이상의 요청량이 들어오면 I/O 블로킹 상태가 되어 응답 성능이 크게 떨어지거나 요청이 무시될 가능성이 있다
  • Java 21버전의 경우, Python Server의 요청이 블로킹 되면, Virtual Thread가 내부적으로 스케줄링을 활용하여 다른 대기 중인 다른 요청을 해결하게 되므로, 성능 향상이 가능할 것이다

 

✔ 변화가 있었는가?

 블로킹 상황의 기존의 플랫폼 스레드 처리 방식의 결과

해석

I/O 블로킹 상황에서 300번의 시도를 하였을 때, 평균 처리 속도는 3239ms이며, 처리량은 66.5/sec

 

블로킹 상황의 가상 스레드 처리 방식의 결과

해석

I/O 블로킹 상황에서 300번의 시도를 하였을 때, 평균 처리 속도는 2266ms이며, 처리량은 78.3/sec

 

 

즉, 평균 처리 속도가 1.5배 빨라졌으며, 평균 처리량은 1.2배 가량 많다.

Samples로 나타나는 요청량 300이 3000 정도로 늘어났다면, 이 성능 차이는 훨씬 눈에 띄었을 것이다.

 

5️⃣ 마지막으로


🧚‍♂️ 어땠어요?

최신버전을 사용한다는 건 그만큼 참고 문서나 자료가 없다는 것이다. 

GPT의 경우,  프로젝트 진행 초기에는 2021년까지의 자료를 기반으로 정보를 제공했고, 프로젝트 중후반 쯤에 2023년 초까지의 데이터를 반영하였다. 그럼에도 불구하고 2023년 9월에 출시된 Java 21 버전은 이전 버전들과 달리 적용된 변화에 대한 직접적인 해결책을 찾기 어려웠다.

 

이러한 경험은 프로젝트 수행에 있어 오히려 즐거우면서도 배움의 질을 향상시켰다. AI에 대한 의존도를 최소화하고 자체적인 문제 해결 능력을 키울 수 있는 기회가 되었기 때문이다.

 

종종 주변에서 "GPT 없이는 개발 못하겠다."라는 말이 들리곤 한다. 사실, GPT의 도움으로 개발 속도가 몇 배로 빨라진 것은 부인할 수 없다. 하지만 진정한 이해와 문제 해결은 단순한 표면적 해결책을 적용하는 것으로는 이루어지지 않는다.

 

따라서 이번 프로젝트를 통해 OS 측면 등을 고려하여 성능 향상에 초점을 맞추는, 그리고 그러한 목적을 이루고자 여러번의 시행착오를 얻는 것이 나의 성장에 도움이 되는 귀한 기회였다고 생각한다.

 

 

 

Reference

Java EE에서 jakarta EE로 변환

스프링 부트 2에서 스프링 부트3로 업그레이드 가이드
Spring Boot 3.0 Migration Guide
WebSecurityConfiguraerAdapter Deprecated 대응법

Virtual Thread란 무엇일까?
OS, 프로세스와 스레드의 차이
IT World - 자바 동시성에 대한 새로운 접근 '가상 스레드'