목차
- 네티 : 비동기식 이벤트 기반 네트워크 프레임 워크
Netty : 유지 관리가 용이한 고성능 프로토콜 서버와 클라이언트를 신속하게 개발하기 위한 비동기식 이벤트 기반 네트워크 애플리케이션 프레임워크
많은 동시 연결을 다뤄야 할 때, 연결마다 스레드를 만드는 모델과 이벤트 루프 모델의 자원 사용을 비교해 보자. 처리 가능한 연결 수는 메시지 크기·업무 로직·하드웨어에 따라 달라지므로 특정 수치를 보장하지 않는다.
(1) 블로킹 입출력 다중연결
-
각각 새로운 클라이언트 소켓마다 새로운 스레드를 할당해야함
-
여러 스레드가 입출력 데이터가 들어오기를 기다리며 무한정 대기 상태로 유지될 수 있다.(리소스낭비)
-
스레드가 스택 메모리를 할당해야 하는데, 운영체제마다 다르지만 스텍의 기본 크기는 64KB에서 1MB 까지 차지할 수 있다.
-
JVM이 물리적으로 많은 수의 스레드를 지원할 수 있지만, 동시 접속이 한계에 이르기 훨씬 전부터 컨텍스트 전환에 따른 오버헤드가 심각한 문제가 될 수 있다.
(2) 자바 NIO
자바는 블로킹 시스템 호출 방식 외에도 네이티브 소켓 라이브러리에는 오래전 부터 네트워크 리소스 사용률을 세부적으로 제어할 수 있는 논블로킹 호출이 포함돼 있다.
(!) 셀렉터
-
한 스레드로 여러 소켓을 동시에 연결 처리 할 수 있다.
-
이 모델은 블로킹 입출력 모델에 비해 전체적으로 훨씬 개선도니 리소스 관리 효율을 보여준다.
-
적은 수의 스레드로 더 많은 연결을 처리할 수 있으므로, 메모리 관리와 컨텍스트 전환에 따르는 오버헤드가 감소
-
입출력을 처리하지 않을 때는 스레드를 다른 작업에 활용 가능.
Socket Socket Socket
| | |
R/W R/W R/W
| | |
| | |
----- Selector ---
|
|
Thread
1.1 네티의 특징
설계
-
단일 API로 블로킹과 논 블로킹 박식의 여러 전송 유형을 지원
-
단순하지만 강력한 스레딩 모델
-
진정한 비연결 데이터그램 소켓 지원
-
재사용 지원을 위한 논리 컴포넌트 연결
이용 편의성
-
자세한 JavaDoc와 광범위한 예제,
-
사용하는 Netty 버전과 전송 방식에 맞는 JDK 및 native 의존성을 확인해야 한다.
성능
-
적절한 파이프라인과 작업 분리 아래에서 적은 스레드로 여러 연결을 처리할 수 있다. 처리량과 지연은 실제 부하로 측정한다.
-
폴링과 재사용을 통한 리소스 소비 감소
-
메모리 복사 최소화
견고성
-
느린 연결과 과부하로 인한 메모리 압박은 여전히 가능하다. 버퍼·큐·프레임 크기에 상한을 둔다.
-
읽기와 쓰기의 속도가 다르면 쓰기 버퍼가 커질 수 있으므로 채널의 쓰기 가능 상태와 백프레셔를 살핀다.
보안
-
완벽한 SSL/TLS 및 StartTLS 지원
-
애플릿이나 OSGI 같은 제한된 환경에서도 이용가능
커뮤니티 주도 개발
- 자주 릴리즈 됨
1.2.2 비동기식 이벤트 기반 네트워킹
-
비동기(Asynchronous), 즉 동기화 되지 않은 이벤트
-
논블로킹 네트워크 연결은 작업 완료를 기다릴 필요가 없게 해줌
-
완전 비동기 입출력은 이 특징을 바탕으로 한단계 더 나아감
-
비동기 메서드는 즉시 반환하며 작업이 완료되면 직접 또는 나중에 이를 통지
-
셀렉터는 적은 수의 스레드로 여러 연결에서 이벤트를 모니터링할 수 있게 해줌
1.3 네티의 핵심 컴포넌트
-
Channel
-
콜백
-
Future
-
event and handler
1.3.1 channel
-
하나 이상의 입출력작업을 수행할 수 있는 하드웨어, 장치, 파일, 네트워크 소켓, 프로그램컴포넌트와 같은 엔티티에 대한 열린 연결
-
channel은 들어오는(인바운드)데이터와 나가는(아웃바운드) 데이터를 위한 운송수단이다.
-
channel은 열거나 닫고, 연결하거나 연결을 끊을 수 있다.
1.3.2 콜백
-
콜백(callback)은 다른 메서드로 자신에 대한 참조를 제공할 수 있는 메서드
-
다른 메서드에서 이 참조가 가리키는 메서드를 필요할 때 호출할 수 있음
-
네티에서는 이벤트를 처리할 때 내부적으로 콜백을 이용
-
콜백이 트리거되면 ChannelHandler 인터페이스의 구현을 통해 이벤트를 처리할 수 있다.
1.3.3 future
-
작업이 완료되면 이를 애플리케이션에 알리는 방법
-
비동기 작업의 결과를 담는 placeholder 역할을 함
-
미래의 어떤 시점에서 작업이 완료되면 그 결과를 접근할 수 있게 해줌
1.3.4. event and handler
-
네티는 작업의 상태 변화를 알리기 위해 고유한 이벤트를 이용
-
발생한 이벤트를 기준으로 적절한 동작을 트리거 할 수 있다.
(로깅, 데이터변환, 흐름제어, 어플리케이션 논리)
- 인바운드 데이터나 연관된 상태 변화로 트리거되는 이벤트
(연결 활성화 or 비활성화, 데이터 읽기, 사용자 이벤트, 오류 이벤트)
- 아웃바운드 이벤트는 미래에 한 동작을 트리거하는 작업의 결과
(원격 피어로 연결 열기 또는 닫기, 소켓으로 데이터 쓰기 또는 플러시)
연결 하나가 처리되는 순서
클라이언트가 TCP 연결을 만들면 서버 채널이 연결을 수락하고, 새 자식 채널이 worker EventLoop에 등록된다. 채널마다 ChannelPipeline이 있으며, 수신 바이트는 프레임 분리·디코딩을 거쳐 업무 핸들러에 도착한다. 핸들러가 writeAndFlush()를 호출하면 출력 이벤트는 인코더를 거쳐 소켓으로 전송된다.
sequenceDiagram participant C as 클라이언트 participant B as 서버 채널 participant E as EventLoop participant P as ChannelPipeline C->>B: TCP 연결 요청 B->>E: 자식 채널 등록 C->>P: 바이트 수신 P->>P: 프레임 분리와 디코딩 P->>E: 업무 핸들러 호출 E->>P: writeAndFlush 응답 P-->>C: 인코딩된 바이트
이 흐름에서 ChannelFuture는 비동기 작업의 완료 또는 실패를 확인하는 수단이다. 예를 들어 bind()의 반환값을 확인하지 않으면 서버가 포트를 열었다고 잘못 판단할 수 있다. 반대로 EventLoop 안에서 future.sync()처럼 완료를 기다리는 동작을 반복하면 그 EventLoop가 맡은 다른 채널 처리도 막힌다. 부팅 코드에서 기다리는 것과 I/O 핸들러 안에서 기다리는 것은 맥락이 다르다.
ByteBuf와 처리량의 실제 제약
수신 데이터는 흔히 ByteBuf로 전달된다. Netty의 버퍼는 참조 카운트로 관리되므로 마지막 소비자가 해제하거나 다음 핸들러로 소유권을 넘겨야 한다. 두 쪽 모두 해제하면 이미 해제된 버퍼를 사용하고, 아무도 해제하지 않으면 메모리가 샌다. SimpleChannelInboundHandler는 소비한 메시지의 자동 해제를 제공하지만, 메시지를 비동기 작업에 보관한다면 그 수명 규칙을 다시 확인한다.
연결이 많다는 사실만으로 서버가 안정적이지는 않다. 클라이언트가 천천히 읽으면 서버의 쓰기 버퍼가 커지고, 길이 제한 없는 프레임은 큰 메시지로 메모리를 채울 수 있다. EventLoop에서 느린 JDBC 호출을 직접 실행하면 네트워크 이벤트까지 지연된다. 따라서 한 연결당 입력 프레임 최대 크기, 읽기 타임아웃, 처리 큐 길이, 쓰기 가능 상태를 정하고 부하 테스트에서 실제 메모리·지연을 본다.
| 보이는 현상 | 먼저 볼 곳 |
|---|---|
| 연결은 됐지만 응답이 없음 | 프레임 경계, flush(), 핸들러 이벤트 전파 |
| 연결 수가 늘 때 지연 급증 | EventLoop의 블로킹 작업, 작업 큐, CPU |
| direct memory 증가 | ByteBuf 해제와 쓰기 버퍼 |
| 일부 메시지가 깨짐 | TCP 조각을 한 메시지로 가정했는지 |
| 서버 시작 성공처럼 보이지만 접속 불가 | bind() 성공 여부와 실제 바인딩 주소 |
처음 구현할 때는 작은 에코 서버에서 bind(), 파이프라인, 프레임 경계를 확인한 뒤 업무 코드를 넣는다. NIO·epoll 중 무엇이 빠른지 먼저 가정하기보다 동일한 프로토콜과 부하에서 비교한다.