Java 11에서 Java 21로: 가상 스레드 도입과 실무 아키텍처의 변화
Java 21 가상 스레드의 개념과 외부 통신의 병목 해결 방안에 대해 개념에 대해 정리한 게시글입니다.
Spring Boot 2.7의 OSS 지원이 종료되었고, Spring Boot 3.X의 사용을 권장한다고 합니다. 이번 포스팅에서는 기존 Java 11의 한계부터 Java 21 도입 시 발생하는 변화, 그리고 실무 적용시 반드시 주의해햐 할 점을 정리해 보겠습니다.
기존 Java 11(플랫폼 스레드)의 한계
기존 톰캣(Tomcat)은 요청 1개당 1개의 OS 스레드를 할당하는 Thread-per-request 모델을 사용합니다.
- OS 스레드는 무겁고 생성 비용이 비싸서 보통 최대 200개(스레드 풀)로 제한합니다.
- 문제는 DB 조회나 외부 API를 호출할 떄 발생합니다. 응답을 기다리는 (I/O Blocking) 동안 스레드는 아무 일도 하지 않고 멈춰있습니다.
- 결국 트래픽이 몰리면 200개의 스레드가 모두 대기 상태에 빠져 고갈되고, CPU가 텅텅 비어있어도 서버가 먹통이 되는 병목 현상이 발생합니다.
Java 21 가상 스레드의 기능 (Mount / UnMount)
Java 21의 가상 스레드는 OS 스레드와 1:1 매칭되지 않습니다. JVM이 직접 관리하는 경량 스레드입니다. Spring Boot 3.2 이상에서는 applicaiton.yml 파일에 한줄만 추가하면 가상 스레드 기능을 사용할 수 있습니다.
1
spring.threads.virtual.enabled: true
- 마운트: 가상 스레드는 소수의 실제 OS(캐리어 스레드) 안에서 코드를 실행
- 언마운트: DB 응답 대기 등 I/O 블로킹이 발생하면, 가상스레드는 즉시 캐리어 스레드를 점유 해제하여 캐리어 스레드를 대기상태로 변경
- 결과: 톰캣의 200개 스레드의 제한이 무의미해지며, 수만 개의 가상 스레드가 생성되어 외부 연동 대기 시간을 효율적으로 관리하게 됨
주의해야될 점: DB 커넥션 풀 (HikariCP)
가상 스레드가 무한대로 증가되니, DB 커넥션 풀도 수천 개로 늘려야될거라 생각이 되지만, DB 서버의 CPU와 메모리가 한정 되어 있다보니, 서버 자체가 죽어버릴수 있는 상황이 발생됩니다.
- 톰캣 스레드에서 발생된 병목이 HikariCP 커넥션을 얻기 위한 대기열로 이동
- 해결책: DB 커낵션 풀은 물리적인 DB 서버 사양에 맞춰 유지해야됩니다. 가상스레드는 DB 연산보다는 다른 API 연동 등 외부 I/O에서 큰 장점이 있습니다.
구버전 JDBC 드라이버에서는 내부의
Synchronized블록 때문에 가상 스레드가 캐리어 스레드를 점유을 해제하지 못하는 스레드 피닝 현상이 발생합니다. 이를 해결하기 위해 최신 JDBC 드라이버로 업데이트 해야합니다.
외부 통신 API 표준: RestClient
가상 스레드를 통해 외부 API 통신을 하게 되면 효과적으로 활용 할 수 있습니다. 동기식 HTTP 요청을 처리하고 유연한 고수준 API 클라이언트입니다. 기존의 RestTemplate를 대체하며, WebClient와 유사한 FlunetAPI 방식을 제공합니다.
- 동기식 지원: 블로킹 방식의 동기식 통신을 지원
- 유연한 API: 메서드 체이닝 방식을 사용하여 가독성이 높음
여러 외부 API를 호출해야 할 때, 가상 스레드를 활용하면 직관적인 코드로 완벽하게 병렬 처리를 구현할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
class test{
public String fetchAllData() throws Exception {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<String> user = executor.submit(() -> restClient.get().uri("/user").retrieve().body(String.class));
Future<String> order = executor.submit(() -> restClient.get().uri("/order").retrieve().body(String.class));
// 3초가 걸리는 API가 있다면 전체 응답 시간은 단 3초로 단축됨
return user.get() + " / " + order.get();
}
}
}
외부 서버 장애로 대기 시간 관리를 위한: Circuit Breaker
가상 스레드가 스레드의 고갈을 막아준다고 하더라도 외부 서버가 완전히 죽었다면 타임아웃(ex: 3초)을 기다리며 사용자를 기다리게 할 필요가 없습니다. 이를 방지하기 위해 서킷 브레이커를 통해 해결할 수 있습니다.
- 외부 서버의 통신 실패율이 임계치를 넘어가면 Circuit이
OPEN됩니다. - 이후 들어오는 요청은 외부 서버를 호출하지 않고
Fallback을 반환합니다. - 서킷브레이커를 통해 무의미한 네트워크 대기 시간을 방어해 외부 시스템 장애가 내부 시스템으로 번지지 않고 안정적인 서버가 구현됩니다.
마무리
Java 11에서 21로의 업그레이드는 대규모 트래픽 처리와 성능 튜닝 측면에서 개발의 편의성을 크게 높여주는 것 같습니다. 특히 MSA 환경처럼 외부 서버 간의 통신이 잦은 경우, 기존의 복잡한 스레드 풀 관리 대신 가상 스레드를 통해 더 많은 요청을 빠르고 효율적으로 처리할 수 있어 실무에 적용하기 매우 좋은 기술이라고 생각합니다.