티스토리 뷰
개요
JDK1.4(Java 4)부터 새로운 입출력(New Input/Output, NIO)이라는 뜻에서 java.nio패키지가 포함되었다.
기존 IO패키지가 단방향 스트림 기반이었다면, NIO는 채널과 버퍼 기반의 구조로 입출력을 처리하며, 논블로킹I/O와 멀티플렉싱을 지원하도록 설계되었다.
스트림은 입력 스트림과 출력 스트림으로 구분되어 있기 때문에 데이터를 읽기 위해서는 입력 스트림을 생성해야했고, 데이터를 출력하기 위해서는 출력 스트림을 생성해야했다. 반면, NIO에서는 채널 기반으로 양방향 입출력 구조를 제공한다. 따라서 입력과 출력을 위해 별도의 스트림을 만들필요 없이 하나의 채널에서 처리 가능하다.
또한 기존 IO는 내부적으로 1바이트 단위로 처리하는 구조에 가까워, 성능을 높이기 위해 보조 스트림(BufferedInputStream, BufferedOutputStream)를 연결하여 별도의 버퍼를 사용했다. 반면, NIO는 버퍼를 중심으로 동작하도록 설계되어, 기본적으로 복수의 바이트를 한 번에 읽고 쓸 수 있으며, 그만큼 I/O 호출 횟수를 줄여 더 나은 성능을 보장한다.

무엇보다 Java NIO가 멀티플렉싱을 지원하는 핵심 컴포넌트 중 하나인 Selector가 있다.
이는 대규모 네트워크 애플리케이션에서 적은 리소스로 많은 연결을 처리할 수 있게 해주는 기반 기술이며, C10K문제를 해결하는 데에도 중요한 역할을 한다. (추가적인 내용은 해당 링크 참고바랍니다.)
C10K 문제로 살펴보는 서버 아키텍처와 커널 I/O의 진화
개요 C10K 문제란, 단일 서버가 동시에 10,000명(Concurrent 10K connections) 이상의 클라이언트와 연결을 유지하며 통신할 수 있도록 네트워크 소켓 처리 성능을 최적화하는 문제를 말한다.이 용어는 1999
ego2-1.tistory.com
Selector
Selector는 하나의 스레드로 여러 채널을 동시에 감시할 수 있도록 해주는 NIO의 핵심요소이다. 각 OS커널에서 제공하는 epoll, kqueue, IOCP 등의 시스템 콜을 추상화해 통합된 API로 제공한다.

Selector에 채널을 등록하면, 해당 채널에 이벤트가 발생했을 때 해당 채널에 대한 접근이 가능해진다.
이때 등록한 채널에서 어떤 타입의 이벤트를 감지할 것인지 명시적으로 지정해야하며, 아래와 같이 4가지 타입으로 분류된다.
SelectionKey.OP_ACCEPT
// 서버의 ServerSocketChannel에 새 TCP 연결 요청이 도착했을 때(클라이언트가 connect()를 호출한 상태)
// accept() 호출로 클라이언트와 연결 수립 가능
SelectionKey.OP_READ
// 연결된 커넥션에 클라이언트가 데이터를 보내와서, 수신 버퍼에 도착한 상태
// read() 호출로 데이터 읽기 가능
SelectionKey.OP_WRITE
// SocketChannel의 송신 버퍼에 여유가 있어 데이터를 쓸 수 있는 상태
// write() 호출로 데이터 전송 가능
SelectionKey.OP_CONNECT
// 클라이언트 측 SocketChannel이 connect() 호출 후, 연결이 완료되었는지 감지하는 이벤트
// 서버가 아닌 클라이언트 측에서 논블로킹 connect 후 finishConnect() 호출 타이밍을 알기 위해 사용
Selector가 관리하는 SelectionKey 집합 종류
Selector는 자신에게 등록된 채널들의 이벤트 상태를 감지하고 관리하기 위해, 내부적으로 SelectionKey 객체들을 세 가지 범주로 분류하여 추적한다. SelectionKey는 Selector에 등록된 각 채널을 식별하고, 그 채널이 어떤 이벤트에 관심 있는지, 현재 어떤 이벤트가 발생했는지를 관리하는 객체다.
keys(): 등록된 전체 키 집합
첫 번째는 keys()를 통해 접근가능한 key set으로 Selector에 등록된 모든 채널의 SelectionKey 집합을 반환한다. 채널이 Selector에 등록될 때마다 이를 식별하고 관리하기 위해 SelectionKey 객체가 생성된다.
즉, keys()를 통해 얻는 집합은 현재 Selector가 관리 중인 모든 채널과 그에 대응하는 SelectionKey들의 목록이며, NIO에서 Selector와 채널, SelectionKey 간의 관계를 이해하는데 가장 헷갈릴 수 있는 부분 중 하나다.

SelectKeys(): 이벤트 발생 키 집합
두 번째는 selectedKeys()를 통해 접근 가능한 SelectionKey 집합이다. select() 메소드를 호출하면, Selector는 등록된 채널들 중 이벤트가 발생한 채널을 감지한다. selectedKeys()는 이때 이벤트가 발생한 채널들의 SelectionKey만을 모아 반환하는 집합이다.

cancelled-key set
마지막으로는 cancelled-key set이다. 이집합은 SelectionKey.cancel()이 호출되어 더 이상 유효하지 않지만, 아직 Selector 내부에서 완전히 제거되지 않은 키들을 임시 보관한다. 이 집합은 사용자가 직접 접근할 수 없으며, 다음 select() 호출시 내부적으로 정리된다.
즉, cancel()호출은 곧바로 key set에서 제거하는 것이 아니라, 우선 cancelled-key set에 등록되고, 이후 Selector가 다음 selector주기를 돌때 실제로 key set에서 제거된다.
Selctor를 이용한 채널 선택
Selector에 하나 이상 채널을 등록한 후에는 select() 메소드를 사용해 채널에 이벤트가 발생할 때까지 대기할 수 있다.
select() 메소드는 SelectionKey.OP_ACCEPT, OP_READ, OP_WRITE , OP_CONNECT와 같이 등록된 이벤트 중, 처리할 준비가 된 채널을 반환하는 메소드로 다음과 같이 3가지 방식으로 사용할 수 있다.
- select(): 등록한 이벤트에 대해 하나 이상의 채널이 준비될 때까지 블록된다. 이벤트를 처리할 수 있는 채널이 있다면 준비된 채널의 수를 반환한다.
- select(long timeout): 준비된 채널이 없을 때 계속 블록하지 않고 `timeout`까지만 블록한다. 해당 시간 안에 이벤트가 발생하면 그 즉시 반환되고, 아무 이벤트가 발생하지 않으면 timeout후 0을 반환한다.
- selectNow(): 준비된 채널이 있으면 즉시 반환하며, `select`와 달리 준비된 채널이 없어도 블록하지 않는다.(이벤트가 감지된 채널이 없다면 0을 반환)
Selector를 이용한 서버 구성 예시
// 1. 셀렉터 생성
Selector selector = Selector.open();
NIO에서 사용할 채널을 반드시 논블로킹 모드로 설정해야 Selector가 해당 채널의 상태를 감지할 수 있다.
// 2. 서버 소켓 채널 생성 및 설정
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.bind(new InetSocketAddress("localhost", 8080));
serverSocket.configureBlocking(false);
Selector에 서버 소켓 채널을 등록한다. 어떤 이벤트를 감지할지 지정해야 한다. 서버 입장에서는 일반적으로 클라이언트의 연결 요청(accept)이벤트를 감지한다.
// 서버 소켓 채널 selector 등록
serverSocket.register(selector, SelectionKey.OP_ACCEPT); // accept 이벤트 등록
이제 select()메소드를 통해 이벤트가 발생했는지 감지하고, 발생한 이벤트에 따라 적절한 처리를 수행하는 이벤트 루프(event loop)를 구성할 수 있다.
select()는 Selector에 등록된 채널 중, 등록된 이벤트(예: OP_ACCEPT, OP_READ 등)가 실제로 발생한 채널이 있을 때까지 대기하다가, 이벤트가 발생하면 해당 채널을 감지하고 반환한다.
while (true) {
// 첫 이벤트가 발생할 때까지 대기(블로킹)
selector.select();
// 발생한 이벤트 집합 반환
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 해당 키(채널)이 어떠한 이벤트에 준비가 되었는지 확인
if (key.isAcceptable()) {
// 클라이언트 연결 수락 처리
} else if (key.isReadable()) {
// 클라이언트로부터 데이터 수신 처리
} else if (key.isWritable()) {
// 클라이언트에게 데이터 전송 처리
}
// 이벤트 처리가 끝난 키는 반드시 selected-key set에서 제거해야 한다.
// 제거하지 않으면 다음 루프에서 같은 이벤트가 또 처리될 수 있어 중복 처리 문제가 발생한다.
iter.remove();
}
}
이 루프의 핵심은 SelectionKey객체가 어떤 이벤트에 준비되어 있는지 검사하여 각각 분기 처리한다는 점이다. key.isAcceptable(), key.isReadable(), key.isWritable() 같은 메소드를 통해 어떤 이벤트가 발생했는지 확인한 후, 그에 맞는 로직을 수행한다.
그리고 반복문 마지막에 반드시 iter.remove()를 호출해줘야한다. 이 호출은 해당 SelectionKey를 selectedKeys() 집합(set)에서 제거하는 역할을 하며, 제거하지 않으면 같은 이벤트가 다음 select() 호출 이후에도 여전히 감지된 것으로 남아 중복 처리되는 문제가 발생할 수 있다.
따라서 selectedKeys()는 이벤트가 발생한 키를 모아두는 임시 저장소 역할을 하며, 이벤트가 처리되었으면 해당 키는 제거해주는 것이 Selector 기반 루프에서 매우 중요한 규칙이다.
멀티플렉싱 기반 에코 서버 예제 코드
public class NioServer {
public static void main(String[] args) throws IOException {
// 1. 셀렉터 생성
Selector selector = Selector.open();
// 2. 서버 소켓 채널 생성 및 설정
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.bind(new InetSocketAddress("localhost", 8080));
serverSocket.configureBlocking(false);
serverSocket.register(selector, SelectionKey.OP_ACCEPT); // accept 이벤트 등록
System.out.println("서버 시작: 포트 8080");
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
selector.select(); // I/O 이벤트 발생 대기
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove();
// 3. 연결 수락 및 클라이언트 소켓 등록
if (key.isAcceptable()) {
SocketChannel client = serverSocket.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("새로운 클라이언트 연결됨");
}
// 4. 클라이언트에 메시지 수신 및 에코 전송
if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
buffer.clear();
int read = client.read(buffer);
if (read == -1) {
System.out.println("클라이언트 연결 종료");
client.close();
continue;
}
// 버퍼를 읽기 모드로 전환
buffer.flip();
client.write(buffer); // 클라이언트에게 데이터 전송
System.out.println("클라이언트로부터 받은 메시지 에코 응답 전송");
}
}
}
}
}

참고 자료
- java docs Selector, https://docs.oracle.com/javase/8/docs/api/java/nio/channels/Selector.html
- java docs SelectionKey, https://docs.oracle.com/javase/8/docs/api/java/nio/channels/SelectionKey.html
- Java NIO - Selector, https://hbase.tistory.com/39
- Java NIO와 멀티플렉싱 기반의 다중 접속 서버, https://jongmin92.github.io/2019/03/03/Java/java-nio/
- 비동기 서버에서 이벤트 루프를 블록하면 안 되는 이유 2부 - Java NIO와 멀티플렉싱 기반의 다중 접속 서버
'Java' 카테고리의 다른 글
| 다양한 I/O 모델을 지원하기 위한 Connector 구조 개선 (0) | 2025.05.31 |
|---|---|
| 오픈소스 분석 - Tomcat 소켓 I/O 동작 방식 파헤쳐보기(BIO, NIO Connector) (0) | 2025.05.28 |
| 접두사 기반 URL 탐색 구조 성능 측정 및 최적화 (0) | 2025.05.05 |
| Servlet과 Servlet Container 동작 과정의 이해 (3) | 2025.04.09 |
| 자바에서 스레드 풀 사용하기 (0) | 2025.02.18 |