Language/JAVA

[JAVA 21] 자바의 미래, 가상 스레드(Virtual Thread)에 대해서 - 컴도리돌이

컴도리돌이 2024. 5. 4. 09:00
728x90

전통적인 자바 스레드 

서버 애플리케이션에서 동시성 처리를 위해 스레드를 사용하는 것은 오랜 전통이었습니다. 예를 들어 스프링 프레임워크는 여러 요청을 처리하기 위해 멀티 스레드 모델을 채택한 프레임 워크입니다. 보통은 각 요청을 처리하는 데 하나의 스레드를 할당하는 방식으로 작동됩니다. 그러나 이 방식은 요청이 많아질수록 문제가 발생하게 됩니다. 

 

과거에는 운영 체제 스레드의 제약으로 인해 동시 요청에 대응하기 위해 스레드 수를 늘리는 것이 어려웠습니다. 이는 스레드가 비용이 높고, 시스템에 사용 가능한 스레드 수가 제한되기 때문이죠. 또한 자바의 스레드는 운영 체제 스레드의 Wrapper로 동작하기 때문에 I/O 작업을 만나면 블로킹되어 다른 작업을 수행할 수 없는 경우가 발생합니다. 예를 들어 스프링 애플리케이션 서버에서 요청을 처리할 때, 해당 요청을 처리하기 위해 특정 스레드가 CPU를 사용됩니다. 그러나 이 스레드가 네트워크 요청이나 파일 쓰기와 같은 I/O작업을 만나면 다른 작업을 수행할 수 없는 상태가 됩니다. 이는 시스템의 성능과 응답 시간을 저하시키는 원인이 됩니다. 

 


가상스레드의 목적

해결하고자 하는 문제

1. 높은 처리량(쓰루풋)의 서버 구축: 가상 스레드는 하드웨어 성능을 최대한 활용하여 높은 처리량을 달성하는 서버를 작성하는 데에 중점을 둡니다.

2. Blocking 처리의 문제 해결: 가상 스레드는 Blocking이 발생할 때 내부적으로 스케줄링을 통해 플랫폼 스레드가 대기하지 않고 다른 가상 스레드가 작업할 수 있도록 합니다. 이는 리액티브 프로그래밍의 Non-blocking과 같은 효과를 제공하며, 플랫폼 스레드의 리소스를 효율적으로 활용합니다.

3. 자바 플랫폼과의 조화: 가상 스레드는 자바 플랫폼의 디자인과 조화를 이루는 코드를 생성합니다. 기존의 자바 언어 구조를 유지하면서도 높은 처리량을 달성할 수 있도록 합니다. 실제로 가상스레드는 기존의 스레드를 그대로 받아옵니다. 😊

 

sealed abstract class BaseVirtualThread extends Thread
        permits VirtualThread, ThreadBuilders.BoundVirtualThread {

    ...
}

 

4. 디버깅 및 성능 테스트의 용이성: 기존의 Reactive programming은 스레드를 기반으로 하지 않았기 때문에 Webflux 등을 사용할 때 디버깅과 성능 테스트가 어려웠습니다. 하지만 가상 스레드는 기존 스레드 구조를 그대로 사용하므로 기존의 도구를 사용하여 디버깅 및 프로파일링이 용이합니다.

 


Reactive Programming, MVC, Virtual Thread

 

MVC 모델은 요청이 들어오면 해당 요청을 처리하기 위해 스레드를 할당하고, 이스레드가 해당 요청을 처리할 때까지 기다리는 방식으로 동작합니다. 이는 블로킹(Blocking) 방식으로, 요청이 처리되기까지 다른 작업을 수행하지 못하는 단점이 있습니다. 반면에 리액티브 프로그래밍은 Non-blocking 방식을 채택합니다. 이는 요청이 처리되는 동안 다른 요청이 기다리지 않고 동시에 처리될 수 있다는 것을 의미합니다. 하지만 리액티브 프로그래밍은 코드 해석과 작성이 정말 어렵습니다... 

 

리액티브 코드

 

가상 스레드는 리액티브 프로그래밍이 달성하고자 하는, 리소스를 효율적으로 사용하여 높은 처리량(throughput)을 감당하려는 목적이 동일합니다. 또한 Non-blocking에 대한 처리를 JVM 레벨에서 담당하기 때문에 기존의 플랫폼 스레드를 직접 사용하는 방식보다 효율적으로 스케줄링하여 처리량을 높일 수 있습니다.


가상 스레드 구조

기존 자바 스레드는 OS 스레드를 감싼 형태로 동작합니다. 애플리케이션 코드가 자바 스레드를 사용할 때 실제로는 OS 스레드를 사용하게 되는데, 이러한 스레드는 생성 및 관리 비용이 높아서 스레드 풀을 사용하여 관리해 왔습니다. 

 

반면에 가상 스레드는 OS스레드를 감싸지 않고 JVM이 자체적으로 스레드를 관리합니다. 이를 통해 가상 스레드는 가상 스레드 풀 없이도 사용할 수 있습니다. JVM은 가상스레드를 OS 스레드와 연결하고, 스케줄링합니다. 이 작업을 mount/ unmount라고 하며, 이전에는 플랫폼 스레드라고 불렀던 부분을 Carrier 스레드라고 합니다. (가상 스레드를 실제 OS 스레드로 연결해 준다는 의미를 갖습니다.) 

 

 

가상 스레드는 기존 스레드와는 달리 내부 스케줄링을 통해 블로킹 시간을 최소화합니다. 블로킹이 발생하면 Carrier 스레드가 다른 가상 스레드로 작업을 전환하여 Non-blocking 이점을 유지할 수 있습니다. 즉 정리하면, 기존 스레드는 블로킹이 발생하면, 무한적으로 기다려야 했는데, 가상 스레드에서는 블로킹이 발생하면 내부 스케줄링을 통해서 실제 작업을 처리하는 Carrier 스레드를 통해서 다른 가상 스레드의 작업을 처리하게 됩니다. 도식화하면 다음과 첨부 그림과 같습니다. 

 

 

하지만 위와 같은 구조에서는 가상 스레드가 요청에 따라 무한적으로 늘어날 수 있기 때문에 전통적인 플랫폼 스레드와 동일한 비용, 컨텍스트 비용이 발생하면 감당하기 어렵기 때문에, 자원 사용량이 다음과 같이 정의됩니다. 

 

카카오 태크 유튜브

 

JDK 21의 신기능 

 

가상 스레드 사용법

 

1. 프로젝트 생성

 

2. Main.java 작성

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) throws Exception{
        run();
    }

    public static void run() throws Exception {
        // Virtual Thread 방법 1
        Thread.startVirtualThread(() -> {
            System.out.println("Hello Virtual Thread");
        });

        // Virtual Thread 방법 2
        Runnable runnable = () -> System.out.println("Hi Virtual Thread");
        Thread virtualThread1 = Thread.ofVirtual().start(runnable);

        // Virtual Thread 이름 지정
        Thread.Builder builder = Thread.ofVirtual().name("JVM-Thread");
        Thread virtualThread2 = builder.start(runnable);

        // 스레드가 Virtual Thread인지 확인하여 출력
        System.out.println("Thread is Virtual? " + virtualThread2.isVirtual());

        // ExecutorService 사용
        try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i <3; i++) {
                executorService.submit(runnable);
            }
        }
    }
}

 

첫 번째 thread 방법은 startVirtualThread를 사용한 방식이고, 두 번째는 Thread.ofVirtual() 방식으로 사용합니다.

virtualThread1, virtualThread2는 똑같이 Thread의 이름을 갖고 사용하기 때문에, 이 스레드가 플랫폼 스레드인지 가상 스레드인지 구분하기 위해서는 isVirtual이라는 메서드를 사용해서 가상 스레드로 동작하는지 아닌지 확인할 수 있습니다. 

 

실제로 위와 같이 low level의 코드를 작성할 일은 전혀 없을 거예요 😆

 

 


Spring Boot(MVC) 적용법 (3.2 이상)

오늘 글에서 제일 중요한 부분입니다. 과연 스프링 부트에서 어떻게 적용하는지가 제일 궁금하잖아요 😊

스프링부트에서 다음과 같이 yaml을 작성하면 가상 스레드를 사용할 수 있습니다. 다음과 같이 가상스레드를 사용하면, 내부에서 발생하는 톰캣, was에 대한 처리를 가상 스레드가 처리가 되게끔 해줍니다. 

spring:
   threads:
       virtual:
           enabled: true

 


 

Spring Boot(MVC) 적용법 (3.x)

// Web request 를 처리하는 tomcat이 virtual Thread를 사용하여 유입된 요청을 처리
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
  return protocolHandler -> {
    protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
  };
}

// Async Task에 Virtual Thread 사용
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
public AsyncTaskExecutor asyncTaskExecutor() {
   return new TaskExecutorAdaper(Executors.newVirtualThreadPerTaskExecutor());
}​

 

 

Virtual Threads in Java (Project Loom) - HappyCoders.eu

Virtual threads in Java (Project Loom): What are virtual threads? Why do we need them? How do they work? How to use them?

www.happycoders.eu

 

Virtual Thread란 무엇일까? (1)

Software Developer, I love code.

findstar.pe.kr

 

Java의 미래, Virtual Thread | 우아한형제들 기술블로그

JDK21에 공식 feature로 추가된 Virtual Thread에 대해 알아보고, Thread, Reactive Programming, Kotlin coroutines와 비교해봅니다.

techblog.woowahan.com

 

[Java] 기존 자바 스레드 모델의 한계와 자바 21의 가상 스레드(Virtual Thread)의 도입

1. 가상 스레드의 도입 배경 [ 기존 자바 스레드 모델의 문제와 한계 ] 자바 개발자들은 약 30년 동안 서버 애플리케이션의 동시성 처리를 위해 스레드를 사용해왔다. 대표적으로 스프링 프레임워

mangkyu.tistory.com