1. 네트워크 IO와 자원 효율
서버 프로그램은 기본적으로 네트워크 기반 프로그램이다.
많은 서버는 HTTP 프로토콜을 이용해 클라이언트와 데이터를 주고받는다.
데이터 처리를 담당하는 DB 역시 TCP 기반 프로토콜을 사용해 네트워크로 데이터를 주고받는다.
Redis를 메모리 캐시로 사용할 때도 내부적으로는 네트워크 통신을 통해 데이터가 오간다.
서버 API 또한 마찬가지로 네트워크 통신을 기반으로 동작한다.
네트워크를 통해 데이터를 주고받는 과정은 단순화하면 다음과 같다.
- 출력 스트림을 통한 데이터 발신
- 입력 스트림을 통한 데이터 수신
이 과정에서 데이터 입출력이 실행되는 동안 스레드는 데이터 전송이 완료될 때까지 대기하며 그 시간 동안 아무 작업도 수행하지 않는다.
이를 IO 블로킹이라고 하며 블로킹이란 특정 작업이 완료될 때까지 스레드가 멈춰 대기하는 상태를 의미한다.
스레드가 데이터 전송에 시간을 소요한다는 것은 해당 시간 동안 CPU가 그 스레드에 대해 실제 연산을 수행하지 못하고 유휴 상태로 남게 된다는 의미다.
이를 보완하기 위해 동시에 실행되는 스레드의 개수를 늘려 IO 대기 시간 동안 다른 작업을 처리하도록 설계할 수 있다.
그러나 스레드 생성에는 명확한 한계가 존재한다.
너무 많은 스레드를 생성하면 메모리 사용량이 급격히 증가하고 이는 메모리 병목의 원인이 된다.
설령 메모리를 충분히 확보하더라도 실행 중인 스레드를 전환하는 과정에서 컨텍스트 스위칭이 빈번하게 발생해 추가적인 성능 비용이 발생할 수 있다.
이러한 문제를 해결하기 위한 대표적인 방법으로 다음 두 가지를 들 수 있다.
- 가상 스레드나 고루틴과 같은 경량 스레드 사용
- 논블로킹 또는 비동기 IO 사용
2. 가상 스레드로 자원 효율 높이기
입출력 작업 동안 스레드가 장시간 대기하지 않도록 하면서 기존 코드 구조를 크게 변경하지 않고 CPU 효율을 높이는 방법 중 하나가 경량 스레드를 사용하는 것이다.
경량 스레드는 OS가 직접 관리하는 스레드가 아니라 JVM과 같은 언어 런타임이 관리하는 스레드다.
OS가 CPU에서 실행할 스레드를 스케줄링하듯 언어 런타임이 OS 스레드 위에서 실행할 경량 스레드를 스케줄링하는 구조를 가진다.
JVM을 예로 들면 JVM은 내부적으로 플랫폼 스레드를 생성해 이를 기반으로 작업을 수행한다.
일반적으로 CPU 자원과 실행 환경에 맞춰 플랫폼 스레드 풀을 구성하며 필요에 따라 스레드 수를 조정한다.
가상 스레드를 경량 스레드라고 부르는 이유는 플랫폼 스레드보다 훨씬 적은 자원을 사용하기 때문이다.
가상 스레드는 힙 메모리에 수백 바이트에서 수십 킬로바이트 수준의 메모리를 사용하며 호출 스택의 깊이에 따라 메모리를 동적으로 확장하거나 축소한다.
캐리어 스레드란 가상 스레드를 실제로 실행하는 플랫폼 스레드를 의미한다.
CPU가 여러 스레드를 번갈아 실행하듯 하나의 캐리어 스레드 역시 여러 가상 스레드를 번갈아 실행하게 된다.
2.1. 네트워크 IO와 가상 스레드
가상 스레드는 실행 중 블로킹 연산을 만나면 플랫폼 스레드로부터 언마운트되고 실행이 일시 중단된다.
이때 분리된 플랫폼 스레드는 실행 대기 중인 다른 가상 스레드와 연결되어 다시 실행을 이어간다.
이러한 동작 덕분에 플랫폼 스레드는 특정 가상 스레드의 IO 대기로 묶이지 않고 다른 작업을 처리할 수 있다.
블로킹 연산에는 ReentrantLock Thread.sleep과 같은 기존의 블로킹 API가 포함된다.
이러한 연산으로 인해 가상 스레드가 블로킹되더라도 플랫폼 스레드는 다른 가상 스레드를 실행할 수 있어 CPU 자원을 효율적으로 활용할 수 있다.
다만 Java 23 기준으로 synchronized 블록에서 블로킹이 발생하는 경우 가상 스레드가 플랫폼 스레드로부터 언마운트되지 않는다.
이 경우 플랫폼 스레드 자체가 블로킹될 수 있으므로 사용 시 주의가 필요하다.
2.2. 가상 스레드와 성능
우리가 작성하는 서버 코드는 크게 다음 두 가지로 나눌 수 있다.
- IO 중심 작업: 네트워크 통신처럼 입출력이 주를 이루는 작업
- CPU 중심 작업: 정렬이나 계산처럼 연산이 주를 이루는 작업
가상 스레드의 장점이 가장 잘 드러나는 경우는 IO 중심 작업을 처리할 때다.
가상 스레드는 블로킹 시 언마운트되므로 플랫폼 스레드가 CPU를 낭비하지 않고 다른 작업을 수행할 수 있다.
반면 CPU 중심 작업에서는 블로킹이 거의 발생하지 않기 때문에 가상 스레드가 언마운트되지 않고 계속 실행된다.
이 경우 가상 스레드는 플랫폼 스레드와 큰 차이를 보이지 않으며 성능상 이점도 제한적이다.
또한 IO 중심 작업이라 하더라도 무조건 가상 스레드가 효과적인 것은 아니다.
플랫폼 스레드 수에 비해 가상 스레드 수가 충분히 많아야 스케줄링 효과를 기대할 수 있다.
2.3. 가상 스레드의 중요한 장점
가상 스레드의 가장 큰 장점 중 하나는 기존 코드를 크게 수정할 필요가 없다는 점이다.
이미 많은 라이브러리와 프레임워크들이 가상 스레드를 고려해 설계되었으며 비교적 낮은 변경 비용으로 도입이 가능하다.
3. 논블로킹 IO로 성능 더 높이기
논블로킹 IO 역시 자원을 효율적으로 사용하는 방법 중 하나로 입출력 작업이 완료될 때까지 스레드가 대기하지 않는 방식을 의미한다.
논블로킹 IO에서는 즉시 결과를 반환하지 않고 코드 흐름이 계속 진행된다.
이로 인해 데이터가 도착했는지 반복적으로 확인하거나 실행 가능한 IO 연산 목록을 조회한 뒤 해당 연산들을 순차적으로 처리하는 방식으로 구현할 수 있다.
이때 논블로킹은 호출 시점에 즉시 반환된다는 의미이며 비동기는 결과가 나중에 통지된다는 점에서 개념적으로 구분된다.
논블로킹 IO 방식은 하나의 스레드로 여러 클라이언트의 요청을 처리할 수 있다.
클라이언트 수가 증가하더라도 스레드 수는 일정하게 유지되므로 같은 메모리 자원으로 더 많은 연결을 처리할 수 있다는 장점이 있다.
3.1. 리액터 패턴
리액터 패턴은 논블로킹 IO를 기반으로 구현할 때 자주 사용되는 설계 패턴이다.
여러 이벤트를 효율적으로 처리하기 위해 이벤트 발생을 대기하다가 이벤트가 발생하면 이를 적절한 핸들러에 전달하는 구조를 가진다.
핸들러는 전달받은 이벤트에 대해 필요한 로직을 수행하며 전체 시스템은 이벤트 루프를 중심으로 동작한다.
다만 이 방식은 단일 스레드에서 루프를 지속적으로 실행하기 때문에
핸들러 내부에서 블로킹 작업이 발생하거나 CPU 중심 연산이 섞일 경우 전체 처리 흐름이 지연될 수 있다.
3.2. 프레임워크 사용하기
논블로킹 IO를 직접 구현하려면 이벤트 관리 상태 관리 에러 처리 등으로 인해 개발 비용이 크게 증가한다.
따라서 직접 구현하기보다는 검증된 프레임워크를 활용하는 것이 일반적으로 권장된다.
논블로킹 IO 개발을 지원하는 프레임워크나 런타임을 선택하면 복잡도를 크게 줄일 수 있다.
4. 언제 어떤 방법을 택할까?
논블로킹 IO나 가상 스레드를 적용하기 전에 가장 먼저 다음 사항을 검토해야 한다.
현재 성능상 문제가 실제로 존재하는가
그 문제가 네트워크 IO와 관련된 병목인가
구조 변경이 가능한 상황인가
명확한 문제 없이 단순히 성능이 좋아 보인다는 이유로 구조를 변경하는 것은 오히려 시간 낭비가 될 가능성이 크다.
또한 앞서 설명한 것처럼 CPU 중심 작업이 주를 이루는 경우에는 IO 병목을 해결하는 방식이 큰 효과를 내지 못할 수 있다.
따라서 논블로킹 IO나 가상 스레드를 도입하기 전에 실제 병목 지점을 충분히 분석하고 정말로 필요한 경우에만 적용하는 것이 바람직하다.
'책 리뷰 > 주니어 백엔드 개발자가 반드시 알아야 할 실무 지식' 카테고리의 다른 글
| 8장. 실무에서 꼭 필요한 보안 지식 (1) | 2026.01.20 |
|---|---|
| 6장. 동시성, 데이터가 꼬이기 전에 잡아야 한다 (0) | 2025.12.15 |
| 5장. 비동기 연동, 언제 어떻게 써야 할까 (0) | 2025.12.07 |
| 4장. 외부 연동이 문제일 때 살펴봐야 할 것들 (0) | 2025.11.29 |
| 3장. 성능을 좌우하는 DB설계와 쿼리 (0) | 2025.11.02 |