Reactive Programming in Java — Good Time to Die
This article explains the reason for Reactive Programming, why it is not popular with Developers and with the introduction of Java Virtual…
medium.com
동기·비동기, IO·NIO, 그리고 Virtual Thread
글의 주장은 “리액티브 프로그래밍은 원래 블로킹 I/O 한계를 극복하기 위해 등장했지만, Java 21의 Virtual Thread 가 등장하면서 더 이상 필수적이지 않다고 합니다.
아래에 제 생각과 본문 내용에 대해 서술합니다.
헷갈리는 개념 1 - 동기 Vs 비동기
동기/비동기 처리는 일의 순서
에 대한 이야기 입니다.
동기적인 작업의 의미는, "이 일은 순서가 정해져있어서 이대로 실행되어야 해!" 이고
비동기 작업의 의미는, "이 일은 순서가 정해져있지 않으니 따로 실행해도 괜찮아" 입니다.
예를들어 권한 확인이 필요한 작업이 있다면 선제적으로 권한 체크를 하고 후에 로직을 태우는 동기적인 처리가 필요합니다.
반면 이메일/인증 코드 등을 보내고 그것을 확인하는 작업은 같이 비동기적으로 별도로 처리되어도 상관 없는 것 이죠
헷갈리는 개념 2 - IO Vs NIO
IO/NIO
는 Thread 점유
에 대한 이야기 입니다.
개발자들은 언제나 한정된 자원을 효율적으로 사용하고 싶어 합니다.
이건 Thread에 대해서도 마찬가지 인데요
기본적으로 I/O 작업이 발생하게 되면 Thread 는 Blocking 하게 됩니다.
DB에 요청보낸 데이터 혹은, 외부 API를 호출한 내용을 응답받기전까지 기다리게 됩니다.
그런데.. 앞서 말했듯 Thread는 비싼 자원입니다.
또 실질적으로 코드를 실행시키는 매우 중요한 자원이기 때문에, 어떻게하면 Thread 를 조금도 쉬지 않고 굴릴 수 있을까? 라는 고민에서 NIO
가 나오게 됩니다.
Synchronous Blocking Design
전통적인 방법의 구현입니다.
사용자 요청시 스레드를 물고와 여러 I/O 작업과, CPU 작업을 하나의 스레드로 처리하는 방식입니다.
Platform Thread 는 기본적으로 1MB의 스택 메모리를 사용하기 때문에 꽤나 비싼 작업입니다.
그렇기 떄문에 Thread Pool 을 만들어 재사용 하는것이죠
Asynchronous Blocking Design
직렬로 실행되는 작업을 병렬화 시키는 작업입니다.
DB 및 API에 접근해 데이터를 가져온다 했을 때, 단계마다 새로운 Thread 를 할당해 처리합니다.
@Async Vs Executor
본글에서는 Executor에 대해서만 언급하지만 @Async 어노테이션 또한 존재합니다.
@Async 어노테이션은 AOP 기반으로 동작하며 Executor 와 동일하게 지정된 스레드 풀에서 스레드를 가져와 비동기적으로 처리하게 됩니다.
몇가지 차이점은 Executor 구현이 더 복잡한 만큼 세밀한 제어가 가능하다는 것 이며, @Async는 제어의 주도권을 Spring 에게 맡기는 것에 있습니다.
하지만 Thread 를 여러 개 사용하는 것이기 때문에 엔터프라이즈 환경에서는 한계가 있어 보입니다.
Reactive Style Design
CompletableFuture
를 사용하면 스레드 갯수를 줄일 수 있습니다.
요청 스레드는 DB조회, API호출, 응답 전송이라는 파이프라인을 만들고 곧바로 스레드풀에 반환됩니다.(끝까지 실행하지 않고, 할 일만 넘겨줌) 그 후 별도의 스레드에서 해당 작업들이 처리됩니다.
CompletableFuture<String> dbFuture =
CompletableFuture.supplyAsync(() -> fetchFromDB()); // DB 조회
CompletableFuture<String> apiFuture =
CompletableFuture.supplyAsync(() -> fetchFromService()); // 외부 API 호출
// 두 결과 합치고 응답 보내는 파이프라인 구성
dbFuture.thenCombine(apiFuture, (db, api) -> db + " + " + api)
.thenAccept(result -> sendDataToUser(result));
System.out.println("Request Thread는 여기서 반환됨");
하지만 여전히 I/O 작업 발생시 Thread가 Blocking 되는 문제는 해결하지 못합니다.
Fully Reactive Style Design
전통적인 Spring MVC -> Spring Webflux 의 전환을 말합니다.
즉 NIO 를 사용하겠다는 의미이며 스레드가 I/O 작업을 기다리지 않고 다른 작업을 처리하다가
I/O 작업이 완료되면 OS 커널 레벨에서 콜백해줍니다.
다만 사용에 있어서 복잡성이 크게 증가하고 전통적인 MVC 와 스타일이 많이 달라져 신규 프로젝트가 아닌 경우 대부분은 MVC 로 구성되어 동작하는 것으로 알고있습니다.
Virtual Thread
Virtual Thread (이하 VT) 입니다.
WebFlux 는 프레임워크의 변경이 필요하지만 VT 는 자바 21로 버전만 올려주면 됩니다.
public void test() throws Exception {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 5; i++) {
int id = i;
executor.submit(() -> {
System.out.println("Start " + id + " in " + Thread.currentThread());
Thread.sleep(1000); // 블로킹처럼 보이지만 Carrier는 반환됨
System.out.println("End " + id + " in " + Thread.currentThread());
return null;
});
}
}
}
VT 는 일회용으로 사용되며 CPU 연산을 위해 Platform Thread (이하 Carrier Thread) 에 mount 되어 사용되다, I/O 작업이 발생되면 suspend 상태로 변하게 되며 Carrier Thread 를 unmount 해, 즉기 Thread Pool 로 반환되고 다른 VT 에서 재활용할 수 있도록 합니다.
2025-09-01T20:30:07.592+09:00 INFO 61502 --- [cs-study.content] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
Start 0 in VirtualThread[#64]/runnable@ForkJoinPool-1-worker-1
Start 1 in VirtualThread[#66]/runnable@ForkJoinPool-1-worker-2
Start 3 in VirtualThread[#68]/runnable@ForkJoinPool-1-worker-4
Start 4 in VirtualThread[#69]/runnable@ForkJoinPool-1-worker-5
Start 2 in VirtualThread[#67]/runnable@ForkJoinPool-1-worker-3
End 4 in VirtualThread[#69]/runnable@ForkJoinPool-1-worker-3
End 2 in VirtualThread[#67]/runnable@ForkJoinPool-1-worker-4
End 3 in VirtualThread[#68]/runnable@ForkJoinPool-1-worker-1
End 1 in VirtualThread[#66]/runnable@ForkJoinPool-1-worker-2
End 0 in VirtualThread[#64]/runnable@ForkJoinPool-1-worker-5
처음 VT를 물고간 worker (carrier thread)의 변경이 있음을 볼 수 있습니다.
이는 스레드가 I/O 작업을 대기하는 것이 아님을 증명합니다.
동기적으로 짜여진 코드를 NIO 로 간단하게 처리 가능하다니.. 아름답네요
마무리
다만 본글에서 말하는 VT 와 같은 도구들로 인해 Webflux 같은 기술들은 생명이 다했다 라는 부분은
동의할 수 없을 것 같습니다.
이유는... 제가 Webflux 에 대해 깊은 이해가 없어서 정확히 어떤점이 해결되는지 모르기 떄문입니다.
다만, VT 의 경우 실행 Context 를 VT가 가지고 있고 Carrier Thread 는 이를 실행만 하는 구조이다 보니
Context 와 관련해 세부적인 제어가 필요하다면 여전히 Webflux 같은 기술의 사용이 유용하지 않을까? 추측됩니다.
'Article' 카테고리의 다른 글
[읽은 글] 왜 Map은 Iterable이 아닐까? (0) | 2025.08.30 |
---|---|
[읽은 글] 왜 자바 Stream은 대규모 환경에 적합하지 않은가? (1) | 2025.08.29 |
[읽은 글] 메서드 시그니처에 List 대신 Collection/Iterable을 고려해야 하는 이유 (1) | 2025.08.29 |
[읽은 글] 자바를 2025년 처럼 사용하는 법 (1) (0) | 2025.08.29 |