이전 글에서는 Netty의 소개를 읽고 관련 있는 기본 지식들에 대해 알아보았다. (여기 클릭!)
이번 시간에는 어떻게 해서 Netty가 출현하게 되었는지를 정리하려고 한다.
설명이 좀 부족할 수 있지만... 액기스와 중요한 부분만 캐치하고 흐름대로 정리하려고 한다.
물론 다 옛날에 분명 배웠고 기억도 가물가물하다. 하지만 다시 복기하는 차원에서...
1. 초기 웹 : Static Web Server
초기에는 정적 웹 서버만 제공을 했다.
정적 웹 서버란, 말 그대로 정적 파일만 제공하는 서버이다.
HTML, CSS, Javascript 같은 단순한 파일을 그대로 전달해주고, 서버 쪽에서 별도 동적 연산을 하지 않는다.
우선 웹페이지를 가져오려면 우리 브라우저에 웹 서버에 파일을 요청한다.
그러면 server가 그것을 찾아서 읽고 browser에 전달한다.
이 과정에서 필요한 정적 파일을 제공하는 서버인 것이다.
그러면 이를 통해서 생각해 볼 수 있는 단점은? 복잡한 동적 처리가 불가능하다는 것이다.
서버와 DB연동이 필요한 복잡한 업무는 처리가 안됐다.
그래서 이를 처리하기 위해서 CGI가 등장했다.
2. CGI와 Servlet의 등장
CGI의 등장
CGI란 Common Gateway Interface의 약자로 웹 서버가 HTTP 혹은 HTTPS 유저 요청을 처리하기 위해 외부의 프로그램을 실행하도록 하기 위한 인터페이서 명세다.
CGI의 목적에 대해서 위키피디아에 상세히 나와있는데,
웹 서버(Web Server)가 클라이언트(브라우저)의 요청을 외부 프로그램(스크립트 혹은 실행 파일)에 넘기고, 그 프로그램이 처리한 뒤 결과를 서버가 클라이언트에 돌려주는 일련의 규칙(specification)이다.
사실 그렇게 흥미가 많지는 않아서... 필자는 그러려니 하고 넘어갔다.
뭐 당연... 이게 좋았으면 사실 이것만 썻겟지?
상태유지 관리가 어렵고, 보안 문제 등의 이슈로 대안이 많이 제시되었는데,
우리가 익히 아는 Servlet이 이 이후에 등장하게 된다.
Servlet의 등장
Servlet은 요청-응답 모델을 통해 서버의 기능을 확장하는 Java 클래스입니다[1].
핵심 특징
- HTTP 요청과 응답을 Java 코드로 직접 처리 가능
- 모든 Servlet은 Servlet 인터페이스를 구현해야 함
- 생명주기 메서드: init(), service(), destroy()
- 스레드 단위로 요청 처리 (CGI보다 효율적)
출처 : [1] Oracle Java EE Tutorial: https://docs.oracle.com/javaee/6/tutorial/doc/bnafe.html
이 Servlet을 사용하면서 요청을 스레드 단위로 처리하고,
자바 코드에서 직접 HTTP 요청과 응답을 다룰 수 있게 되면서 기존의 CGI보다는 편리해졌다.
하지만 위의 Servlet에 대한 정의와 설명에 대해서 잘 읽어보면 이런 부분이 있다.
All servlets must implement the Servlet interface, which defines lifecycle methods
Servlet는 즉 그냥 규격이고 직접 구현은 안되었다는 단점이 있다.
아니... 그러면 우리가 여태 쓰는 톰캣은 도대체 뭐야?
하는데... 이 Servlet의 규약을 기반으로 구현한 Tomcat을 우리는 Servlet Container 라고 한다
Servlet Container? Servlet의 명세를 구현한 실행 환경이라는 말이다.
즉, Servlet을 직접 실행할 수 있는 그릇 역할을 하는 것을 Servlet Container라고 한다.
Tomcat 은 웹 서버의 기능에 Servlet을 구현하여 동적웹 기능을 추가했고,
이것을 우리는 WAS(Web Application Server)라고 한다.
내용이 너무 많아서 이걸 다 일일히 분석할 수는 없었지만, GPT의 도움을 받아 핵심 특징 구절을들 뽑아달라 했더니
Tomcat의 버전에 따라서 공식 문서를 볼 수 있었다.
핵심적으로 알 수 있은 부분은 물론... 다들 익히 알겠지만 Thread 하나당 요청이 들어오고 Blocking I/O를 기본으로 동작한다.
Tomcat 공식 문서의 Connector 설명을 보면[2], 핵심 동작 방식은 다음과 같다.
Tomcat의 요청 처리 방식
1. 요청 1개당 스레드 1개 할당
2. 동시 요청이 많아지면 maxThreads까지 스레드 생성
3. 그 이상은 acceptCount까지 큐에 대기
4. 더 많은 요청은 "connection refused" 에러 즉, 요청당 스레드를 할당하는 Blocking I/O 방식이다.
출처 : Tomcat 7.0 Documentation: https://tomcat.apache.org/tomcat-7.0-doc/realm-howto.html
각각의 요청은 그 요청의 지속동안 하나의 쓰레드를 필요로한다.
사용가능한 요청 처리 쓰레드보다 더 많은 요청이 동시 요청이 들어오면 추가적인 쓰레드가 최대치까지 생성된다.
만약 더 많은 요청이 들어오면 최대 설정치까지 Connector에 의해 생성된 서버 소켓 안에 쌓일 것이다.
더더욱 많은 동시 요청은 자원 처리가 가능할 때 까지 연결 거절 에러를 받는다.
다른 글을 보면 Tomcat 의 Advanced I/O 기능에 대해 설명한다.
NIO기능도 제공을 하는거 같다.
CommetEvent, CometFilter, Comet timeouts를 포함하는 Commet support가 있고 Asynchronous하게 파일을 제공하는 방법도 있다.
Comet applications are event driven applications that hold HTTP connections open waiting for events.
Comet 애플리케이션은 HTTP 연결을 열린 상태로 유지하면서 이벤트를 기다리는 이벤트 기반 애플리케이션이다.
잠깐만... 뭔가 이상하다. 연결을 열린 상태로 유지해? 뭔가 어디서 본거같지 않은가?
이전에 폴링이라는 단어를 들어봤을 것이다.
그렇다. tomcat은 실제로 http를 기준으로 사용해서 연결을 끊지 않고 실시간 비동기 처리를 가능하게 만들었던 것이다.
즉, 유사 비동기를 구현해서 제공하던 것이다.
애초에 Tomcat 은 목적 자체가 비동기 처리와는 알맞지 않다.
Tomcat의 본질은 Jakarta Servlet, JSP, WebSocket 등의 명세를 구현하는 서버이다.
즉, HTTP 기반의 동기 통신을 목적으로 설계되었기 때문에 비동기 처리에는 근본적인 한계가 있다.
출처 : [3] Apache Tomcat: https://tomcat.apache.org/
Tomcat의 본질은 Servlet, JSP, EL, WebSocket 스펙을 구현하는 서버라는 것이다.
Jakarta Servlet 명세에 따르면, 모든 Servlet Container는 HTTP를 필수로 지원해야 한다.
출처 : [4] Jakarta Servlet Specification: https://jakarta.ee/specifications/servlet/
애초에 비동기를 기반으로 만든 것이 아닌 HTTP 를 통한 동기 통신에 기반을 두고 있는 것이다.
이쯤되면 톰캣의 단점이자 Blocking I/O 의 한계를 정리해보자.
- 요청 1개당 스레드 1개 필요
- DB 조회, 외부 API 호출 등 대기 시간 동안 스레드가 묶임
- 접속자가 많아지면 스레드 수 폭증 → CPU·메모리 한계 발생
- 스레드 풀(Thread Pool)로 최적화했지만 근본적 한계 존재
- 따라서 대규모 동시 접속 서비스(채팅, 스트리밍, 게임) 에서는 부적합
3. Non-blocking I/O (Java NIO)의 등장
Java 1.4에 NIO 기능이 추가되었다.
NIO는 New I/O로 그대로 읽으면 I/O 신기능이다.
공식문서를 보는거보다는, 어느 사이트에서 Java NIO 에 대한 튜토리얼을 만들어놨는데
그것이 다음이다.
Java 1.4에 NIO(New I/O) 기능이 추가되었습니다.
Java NIO의 핵심 특징
1. Non-blocking I/O 지원
- 스레드가 I/O 작업 중에도 다른 작업 수행 가능
- 채널이 버퍼로 데이터를 읽는 동안 스레드는 다른 작업 처리
2. Channel과 Buffer
- 기존 IO: Byte Stream, Character Stream 사용
- NIO: Channel과 Buffer를 통해 데이터 처리
- 데이터는 항상 Channel ↔ Buffer 간 이동
3. Selector (핵심!)
- 하나의 Selector가 여러 Channel을 감시
- 이벤트 발생 시에만 처리 (Event Loop 개념)
- 단일 스레드로 다중 연결 처리 가능
정리하면, Non-blocking을 지원하고, Channel과 Buffer를 통해서 NIO가 작동하며, Selector가 여러 채널의 이벤트를 감시하기에 하나의 스레드가 여러 채널을 감시할 수 있다.
Channel은 I/O 작업을 수행할 수 있는 엔티티를 위한 연결을 담당한다.
Buffer는 특정 원시 타입의 데이터를 위한 컨테이너로, NIO에서 데이터를 담고 읽고 쓰는 역할을 한다. 용량과 한계가 있으며 위치도 있다. 스레드 안전하지 않으니 동기화가 필요하다.
어휴 벌써부터 NIO 쓰기 피곤하네.... 공부할게 너무 많다.
아 이거 누가 쉽게 안해주나... 좀 편리하게 쓰려면 프레임워크를 쓰면 좋지 아니한가?
4. 혼돈의 시대(?), Netty의 탄생
직접 쓰기 어려운 NIO를 쉽게 쓸 수 있는 프레임워크가 등장했다.
Netty란?
NIO 기반의 네트워크 애플리케이션 프레임워크로, TCP/UDP 소켓 서버 개발을 크게 간소화한다.
Netty의 강점
- "빠르고 쉽다"가 성능 저하를 의미하는 게 아님
- FTP, SMTP, HTTP 등 다양한 프로토콜 구현 경험을 바탕으로 설계
- 개발 편의성, 성능, 안정성, 유연성을 모두 달성 즉, NIO의 복잡함은 숨기고 장점만 쉽게 사용할 수 있게 해주는 프레임워크다!
출처 : [5] Netty Official: https://netty.io/
5. 결론
Web Server → CGI → Servlet → Servlet Container → Blocking I/O 한계 → Non-blocking I/O → Netty
- 범용 HTTP 서버는 일반 웹 요청에는 충분하지만,
- 실시간 서비스(채팅, 게임), 대규모 트래픽, 특수 목적 최적화에는 한계 존재
이번 시간에는 쭈욱... 글에 대해 찾아서 정리를 해봤다.
사실 내가 한건 별거 없다. 여러 정보가 있는데 GPT에 물어봐서 공식 문서에 대해 물어보고 읽고 간결하게 정리했다.
시간이 난다면 이 글들을 읽어보는게 좋겟다.
다만 흐름을 알고자 필자는 엄~청 축약을 했으니 오해말길!
다음에는 Netty의 구조와 사용법에 대해 알아보려고 한다.
참고:
https://en.wikipedia.org/wiki/Common_Gateway_Interface
https://docs.oracle.com/javaee/6/tutorial/doc/bnafe.html
https://tomcat.apache.org/tomcat-7.0-doc/realm-howto.html
https://tomcat.apache.org/tomcat-6.0-doc/aio.html
https://jakarta.ee/specifications/servlet/6.0/jakarta-servlet-spec-6.0.html
'Spring > Spring Framework' 카테고리의 다른 글
| [Netty] Netty가 뭐에요? - 4탄 : Spring MVC 와 Spring Web Flux 그리고 Netty 예시 코드 탐방 (0) | 2025.10.07 |
|---|---|
| [Netty] Netty가 뭐에요? - 3탄 : Netty의 특징과 구조 및 NIO 테스트 (2) | 2025.09.28 |
| [Netty] Netty가 뭐에요? - 1탄 : Netty 란? - 용어 정리 (3) | 2025.09.18 |