지난 포스팅에서는 무심코 사용하는 문자열(String) 결합 연산이 어떻게 CPU와 메모리를 낭비하는지 코드 레벨의 튜닝 방법을 알아보았습니다. 서버 내부의 로직을 아무리 깔끔하게 최적화하더라도, 결국 현대의 애플리케이션은 혼자서 동작하지 않습니다. 데이터베이스에서 데이터를 읽어오고, 외부 결제 API를 호출하고, 클라이언트에게 응답을 내려주는 등 끊임없이 외부와 통신해야 합니다.
이러한 네트워크 통신이나 파일 입출력 과정에서 발생하는 지연을 I/O(Input/Output) 병목이라고 부릅니다. 백엔드 시스템 장애의 가장 큰 원인 중 하나인 이 I/O 병목 현상의 진단법과, 이를 근본적으로 해결하기 위해 도입된 Java NIO(New I/O) 아키텍처에 대해 깊이 있게 분석해 보겠습니다.
1. 전통적인 블로킹(Blocking) I/O의 치명적 한계
과거 전통적인 Java 웹 서버(예: 초창기 Tomcat)는 클라이언트의 요청이 들어올 때마다 하나의 스레드를 할당하는 ‘Thread-per-Request’ 모델을 사용했습니다. 이 방식은 근본적으로 블로킹(Blocking) 방식으로 동작합니다.
- 대기(Wait)의 늪: 스레드가 외부 API를 호출하거나 DB에 쿼리를 던지면, 응답(데이터)이 스트림(Stream)을 통해 도착할 때까지 해당 스레드는 아무 일도 하지 못하고 그 자리에 멈춰(Blocked) 대기해야 합니다.
- 리소스 고갈: 만약 외부 결제 API 서버가 느려져 응답에 5초가 걸린다면, 수천 명의 사용자가 몰릴 때 수천 개의 스레드가 일제히 5초 동안 멈춰 있게 됩니다. 결국 서버의 스레드 풀(Thread Pool)은 텅 비게 되고, 추가로 들어오는 사용자 요청은 모두 거절(Timeout)되는 대형 장애로 이어집니다.
2. I/O 병목 현상 진단 방법
이러한 I/O 병목은 서버의 CPU 사용률만 봐서는 절대 찾을 수 없습니다. (스레드가 대기 상태이므로 오히려 CPU는 0%에 가깝게 쉴 수도 있습니다.) 실무에서는 다음과 같은 지표로 I/O 병목을 진단합니다.
- 스레드 덤프 분석: 덤프 파일 내에 수많은 스레드가
java.net.SocketInputStream.socketRead0와 같은 네트워크 읽기(Read) 대기 상태인TIMED_WAITING이나RUNNABLE(Native)상태로 멈춰 있다면 100% 외부 I/O 병목입니다. - APM 지표 (외부 호출 시간): 핀포인트나 데이터독 같은 APM 도구에서 전체 API 응답 시간 중 ‘External Call(외부 호출)’ 구간이 차지하는 비율이 비정상적으로 높게 나타납니다.
3. 혁신적인 해결책: Java NIO(New I/O)의 등장
이러한 블로킹 I/O의 한계를 극복하기 위해 Java 1.4부터 NIO(New Input/Output) 라이브러리가 도입되었습니다. NIO는 기존의 단방향 스트림(Stream) 대신, 양방향 통신이 가능한 채널(Channel)과 데이터를 담는 버퍼(Buffer)라는 개념을 사용합니다. 하지만 가장 핵심적인 기술은 바로 셀렉터(Selector)입니다.
- 멀티플렉싱(Multiplexing)과 셀렉터: 셀렉터는 ‘단 1개의 스레드로 수천 개의 네트워크 채널(연결)을 동시에 관리’할 수 있게 해주는 마법 같은 이벤트 통지 매커니즘입니다.
- 논블로킹(Non-Blocking) 통신: 외부 API를 호출한 후 스레드는 응답을 기다리며 멈춰있지 않습니다. 즉각 메인 풀로 돌아가 다른 요청을 처리합니다. 그러다 외부에서 응답 데이터가 버퍼에 도착하면, 셀렉터가 이벤트(Event)를 발생시켜 스레드에게 “데이터가 준비됐으니 이제 가져가서 처리해!”라고 알려줍니다.
4. 실무 도입: Netty 프레임워크와 Spring WebFlux
NIO의 개념은 훌륭하지만, 개발자가 직접 Java 기본 API로 셀렉터와 채널을 다루는 것은 예외 처리가 너무 복잡하여 악몽에 가깝습니다. 그래서 실무에서는 이 복잡한 NIO 네트워크 레이어를 안전하고 강력하게 래핑(Wrapping)한 Netty(네티) 프레임워크를 표준으로 사용합니다.
최근 마이크로서비스 아키텍처(MSA)가 대세가 되면서 외부 API 호출이 잦아지자, 스프링 진영에서는 이 Netty(NIO)를 기반으로 동작하는 Spring WebFlux(웹플럭스)를 내놓았습니다. WebFlux를 사용하면 단 몇 십 개의 적은 스레드만으로도 기존 블로킹 방식(Spring MVC)에서는 뻗어버릴 수만 명의 동시 접속 트래픽을 지연 없이 매끄럽게 처리하는 극강의 퍼포먼스를 낼 수 있습니다.
결론: 대규모 트래픽 처리를 위한 패러다임 전환
서버의 CPU나 메모리를 늘리는 스케일 업(Scale-up)에는 물리적, 비용적 한계가 있습니다. 현대의 엔터프라이즈 서버들은 제한된 하드웨어 자원 안에서 성능을 극대화하기 위해 동기식/블로킹 방식에서 비동기식/논블로킹(NIO) 아키텍처로 빠르게 전환하고 있습니다. I/O의 흐름을 이해하고 병목을 통제하는 자만이 진정한 대용량 트래픽을 감당할 수 있습니다.
이것으로 코드 및 인프라 최적화 파트를 모두 마칩니다. 다음 포스팅부터는 시리즈의 대미를 장식할 ‘실전 트러블슈팅(문제 해결) 사례’ 섹션으로 넘어가, 서버 운영 중 가장 자주 만나는 불청객, ‘OutOfMemoryError(OOM) 발생 원인 4가지와 해결 시나리오’에 대해 집중 해부해 보겠습니다.