Java NIO(New I/O)는 자바의 표준 입출력 API인 java.io 패키지를 대체하거나 보완하는 기능을 제공하는 패키지입니다. 주로 대용량 데이터를 처리하거나 성능을 최적화할 필요가 있는 경우 사용됩니다. 자바 NIO는 비동기적이고, 블로킹이 아닌(non-blocking) 입출력 작업을 지원하는 것이 특징입니다.
1. blocking IO
블로킹 I/O는 입출력 작업이 완료될 때까지 현재 실행 흐름이 차단(block)되는 방식입니다. 입출력 작업이 시작되면 해당 작업이 완료될 때까지 다른 작업을 수행할 수 없습니다. 이러한 차단은 입출력 작업이 완료되기 전까지 스레드가 대기하며, 작업이 완료되면 스레드는 결과를 반환하고 다음 작업을 수행합니다.
스레드가 입출력 작업을 기다리는 동안 블로킹되기 때문에 동시에 여러 클라이언트의 요청을 처리하기에는 비효율적입니다.
ex) InputStream의 read() 메서드는 데이터를 읽을 때까지 현재 스레드를 차단합니다.
데이터가 도착할 때까지 스레드는 아무 작업도 수행하지 않고 대기합니다.
2. New IO
자바 4부터 새로운 입출력(NIO: New Input/Output)이라는 뜻에서 java.nio 패키지가 포함되고
자바 7로 버전업하면서 자바 IO와 NIO 사이의 일관성 없는 클래스 설계를 바로 잡고, 비동기 채널 등의 네트워크 지원을 강화한 NIO.2 API가 추가되었습니다.
NIO 패키지 | 포함되어 있는 내용 |
java.nio |
다양한 버퍼 클래스 |
java.nio.channels |
파일 채널, TCP 채널, UDP 채널 등의 클래스 |
java.nio.channels.spi |
java.nio.channels 패키지를 위한 서비스 제공자 클래스 |
java.nio.charset |
문자셋, 인코더, 디코더 API |
java.nio.charset.spi |
java.nio.charset 패키지를 위한 서비스 제공자 클래스 |
java.nio.file |
파일 및 파일 클래스에 접근하기 위한 클래스 |
java.nio.file.attribute |
파일 및 파일 클래스의 속성에 접근하기 위한 클래스 |
java.nio.file.spi |
java.nio.file 패키지를 위한 서비스 제공자 클래스 |
1. 버퍼(Buffer)
- 데이터를 일시적으로 저장하는 메모리 공간입니다.
- 버퍼는 Buffer 클래스(예: ByteBuffer, CharBuffer 등)로 구현되며, 읽기/쓰기 작업에서 중요한 역할을 합니다.
- NIO에서는 데이터를 직접 읽고 쓸 수 있는 스트림 대신 버퍼를 통해 작업합니다.
2. 채널(Channel)
- 입출력 데이터의 연결점 역할을 합니다.
- 전통적인 InputStream, OutputStream과는 달리, Channel은 양방향 입출력을 지원합니다.
- 주요 채널 클래스에는 FileChannel, SocketChannel, ServerSocketChannel, DatagramChannel 등이 있습니다.
- 채널은 버퍼와 결합되어 비동기적으로 데이터를 읽고 씁니다.
3. 비동기식 및 논블로킹 I/O (Asynchronous & Non-blocking I/O)
- 전통적인 I/O는 블로킹 방식입니다. 즉, 입출력 작업이 완료될 때까지 쓰레드가 작업을 멈추고 대기합니다.
- 반면, NIO는 논블로킹 모드를 지원하여 입출력 작업이 완료될 때까지 기다리지 않고, 즉시 다른 작업을 수행할 수 있게 합니다. 입출력 작업이 완료되면 해당 작업을 처리할 수 있습니다.
- 비동기 I/O를 위한 AsynchronousSocketChannel, AsynchronousServerSocketChannel 같은 클래스들이 있습니다.
4. 셀렉터(Selector)
- 논블로킹 I/O에서 여러 채널을 동시에 모니터링할 수 있게 해주는 객체입니다.
- Selector는 여러 채널의 이벤트(읽기, 쓰기 가능 상태)를 감지하고 해당 이벤트가 발생한 채널만 선택적으로 처리할 수 있게 합니다.
- 이를 통해 하나의 쓰레드가 여러 채널을 효율적으로 처리할 수 있습니다.
5. 파일 I/O (File I/O)
- NIO는 파일 시스템의 작업을 위한 강력한 기능을 제공합니다. FileChannel을 사용하여 파일의 데이터를 읽고 쓸 수 있습니다.
- 이 외에도 Paths, Files와 같은 유틸리티 클래스들을 통해 파일 복사, 이동, 삭제, 파일 속성 읽기 등의 작업을 효율적으로 처리할 수 있습니다.
Java NIO와 기존 I/O의 차이점
- Stream vs. Buffer: 기존 I/O는 스트림 기반이었으나, NIO는 버퍼 기반입니다.
- Blocking vs. Non-blocking: 기존 I/O는 블로킹 방식이지만, NIO는 논블로킹 방식도 지원합니다.
- 비동기 I/O: NIO는 비동기 I/O 작업을 위한 클래스를 제공합니다.
구분 | IO | NIO |
입출력 방식 | 스트림 방식 (단방향) | 채널 방식 (양방향) |
버퍼 방식 | 논버퍼 | 버퍼 |
비동기 방식 | 지원 안함 | 지원 |
블록킹/넌블로킹 방식 | 블로킹 방식만 지원 | 블로킹 / 넌블로킹 방식 모두 지원 |
논블로킹 I/O는 입출력 작업이 완료되지 않더라도 현재 실행 흐름이 차단되지 않는 방식입니다. 입출력 작업을 시작한 후에 다른 작업을 수행할 수 있으며, 입출력 작업이 완료되면 그 결과를 확인할 수 있습니다.이를 위해 자바에서는 NIO(Non-blocking I/O) 패키지를 제공하며, Selector, Channel, ByteBuffer 등의 요소를 사용하여 구현합니다.논블로킹 I/O는 단일 스레드에서 여러 개의 연결을 처리하는 데 유용합니다. 단일 스레드가 여러 개의 채널을 관리하고, 입출력 작업이 완료되지 않더라도 다른 작업을 수행할 수 있습니다. 이는 입출력 작업을 기다리는 동안 다른 클라이언트의 요청을 처리하는 데 유용하며, 동시 접속이 많은 서버 애플리케이션에 적합합니다.
1. Blocking I/O (BIO) 설정
설정 예시: 톰캣의 기본 설정은 BIO입니다. 이 경우, 요청을 처리하기 위해 각 클라이언트에 대해 별도의 스레드를 생성합니다.
- 동작 방식:
- 클라이언트가 요청을 보내면, 톰캣 서버는 새로운 스레드를 생성하여 해당 요청을 처리합니다.
- 스레드는 요청을 처리하는 동안 블로킹 상태가 되어, 다른 요청이 들어오더라도 대기해야 합니다.
- 이로 인해 많은 동시 클라이언트 요청이 들어오면, 스레드 수가 급격히 증가하고 서버의 자원이 소모됩니다.
- 장점:
- 구현이 간단하고 이해하기 쉽습니다.
- 스레드가 독립적으로 동작하여, 요청 처리 로직을 단순하게 구성할 수 있습니다.
- 단점:
- 스레드 생성과 관리로 인한 오버헤드가 발생합니다.
- 대량의 동시 요청을 처리하기 어려워 성능이 저하될 수 있습니다.
2. Non-blocking I/O (NIO) 설정
설정 예시: 톰캣에서 NIO를 사용하려면, server.xml 파일에서 connector의 protocol 속성을 org.apache.coyote.http11.Http11NioProtocol로 설정해야 합니다.
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="200" connectionTimeout="20000" />
- 동작 방식:
- 클라이언트가 요청을 보내면, 톰캣 서버는 요청을 처리하기 위해 스레드를 블로킹하지 않고 요청을 큐에 추가합니다.
- 선택자(Selector)를 사용하여 여러 클라이언트의 요청을 동시에 모니터링하고, 요청이 처리 가능한 상태가 되면 해당 스레드가 작업을 수행합니다.
- 이를 통해 여러 클라이언트 요청을 동시에 처리할 수 있으며, 필요할 때만 스레드를 사용합니다.
- 장점:
- 적은 수의 스레드로도 많은 동시 클라이언트 요청을 처리할 수 있습니다.
- 자원 사용이 효율적이며, 성능이 향상됩니다.
- 단점:
- 구현이 다소 복잡할 수 있으며, 비동기 프로그래밍 모델에 대한 이해가 필요합니다.
BIO 방식은 각 요청에 대해 별도의 스레드를 생성하여 요청을 처리하고, 블로킹되어 다른 요청을 대기하게 하므로 동시 처리 능력이 제한됩니다.
NIO방식은 비동기적으로 요청을 처리하여, 여러 클라이언트의 요청을 효율적으로 관리할 수 있어 성능이 향상됩니다.
Netty
: Netty는 Java로 작성된 비동기 이벤트 기반의 네트워크 애플리케이션 프레임워크입니다. 네트워크와 관련된 다양한 작업을 쉽게 처리할 수 있도록 설계되었습니다. Netty는 고성능, 확장성, 유연성, 다양한 프로토콜 지원 등의 특징을 갖고 있습니다.
- 비동기 이벤트 기반: Netty는 이벤트 기반의 비동기 프로그래밍 모델을 사용하여 네트워크 작업을 처리합니다. 이를 통해 블로킹하지 않고 다중 클라이언트를 효율적으로 처리할 수 있습니다.
- 고성능: Netty는 고성능을 제공하기 위해 설계되었습니다. 네트워크 작업에 대한 최적화된 IO 처리와 스레드 관리를 통해 높은 처리량과 낮은 지연 시간을 보장합니다.
- 유연성: Netty는 다양한 프로토콜을 지원하며, 커스터마이징이 쉽습니다. HTTP, HTTPS, TCP, UDP 등의 프로토콜을 지원하며, 이를 활용하여 다양한 네트워크 애플리케이션을 개발할 수 있습니다.
1. 셀렉터(Selector)
: Selector는 다중 채널을 모니터링하고, 그 중 입출력이 가능한 채널을 선택할 수 있는 기능을 제공합니다. 이를 통해 단일 스레드에서 여러 채널을 관리하고 다중 연결을 효율적으로 처리할 수 있습니다.

2. 이벤트(Event)
- ChannelActive : 채널이 활성화될 때 발생하는 이벤트입니다. 채널이 연결되었거나 채널에 입출력 작업이 준비된 경우에 발생합니다.
- ChannelInactive : 채널이 비활성화될 때 발생하는 이벤트입니다. 채널이 닫히거나 연결이 끊어진 경우에 발생합니다.
- ChannelRead : 채널에서 데이터를 읽을 때 발생하는 이벤트입니다. 채널에 데이터가 도착했을 때 발생하며, 읽은 데이터를 처리하는 작업을 수행할 수 있습니다.
- ChannelReadComplete : 채널에서 데이터 읽기가 완료되었을 때 발생하는 이벤트입니다. 채널에서 모든 데이터를 읽었을 때 발생하며, 이벤트 핸들러에서 데이터 처리가 완료되었음을 알리는 데 사용됩니다.
- ChannelWritabilityChanged : 채널의 쓰기 가능 상태가 변경될 때 발생하는 이벤트입니다. 채널의 출력 버퍼가 가득 차거나 비어있을 때 발생합니다.
- UserEvent: 사용자 정의 이벤트입니다. 개발자가 직접 이벤트를 정의하고 발생시킬 수 있습니다.
3. Netty핵심 컴포넌트
컴포넌트 | 설명 |
Channel | 인바운드 아웃바운드 운송수단 |
콜백 (Callback) | 인바운드, 아웃바운드 핸들러에서 사용
|
Future | 비동기 작업의 결과를 저장 |
이벤트와 핸들러 | 이벤트그룹 : 이벤트 그룹은 여러 개의 이벤트 루프를 포함하고 있으며, 각각의 이벤트루프는 하나의 쓰레드에서 동작합니다. 이벤트 그룹은 주로 두 가지 종류로 나뉩니다:
핸들러 :
![]() |
하나의 이벤트 루프에서 두 개의 채널에서 WRITE 이벤트가 발생하면, 네티의 이벤트 루프는 순차적으로 각 채널의 WRITE 이벤트를 처리합니다.
기본적으로 네티의 이벤트 루프는 단일 쓰레드에서 실행되기 때문에, 이벤트 루프가 한 번에 하나의 이벤트를 처리합니다. 따라서 두 개의 채널에서 WRITE 이벤트가 발생하면, 이벤트 루프는 한 번에 하나의 채널의 WRITE 이벤트를 처리하고 다음 채널의 WRITE 이벤트를 처리합니다.
Netty서버, 클라이언트
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.68.Final</version> <!-- 또는 최신 버전 --> </dependency>
import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.ChannelHandlerContext; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class NettyServer implements CommandLineRunner { @Override public void run(String... args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new SimpleChannelInboundHandler<String>() { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) { System.out.println("Received message: " + msg); ctx.writeAndFlush("Hello from Netty Server\n"); } }); ChannelFuture f = b.bind(8080).sync(); System.out.println("Netty server started on port 8080"); f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } }
import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.ChannelHandlerContext; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class NettyClient implements CommandLineRunner { @Override public void run(String... args) throws Exception { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.SO_KEEPALIVE, true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) { System.out.println("Received from server: " + msg); } }); } }); ChannelFuture f = b.connect("localhost", 8080).sync(); f.channel().writeAndFlush("Hello from Netty Client\n"); f.channel().closeFuture().sync(); } finally { group.shutdownGracefully(); } } }
ByteBuf Class
- readByte(), writeByte(): 바이트를 읽고 씁니다.
- readBytes(int length), writeBytes(byte[] bytes): 바이트 배열을 읽고 씁니다.
- copy(): 버퍼의 복사본을 만듭니다. (새로운 객체)
- slice(): 부분 버퍼를 만듭니다.
- duplicate(): 현재 버퍼의 복사본을 만듭니다. (중복된 객체)
- release(): 버퍼의 메모리를 해제합니다.
- readerIndex() 메서드를 사용하여 현재 읽기 인덱스를 확인할 수 있습니다.
- writerIndex() 메서드를 사용하여 현재 쓰기 인덱스를 확인할 수 있습니다.
- setIndex(int readerIndex, int writerIndex) 메서드를 사용하여 읽기 인덱스를 설정할 수 있습니다.
- 읽기 인덱스 (Reader Index):
- 읽기 인덱스는 현재 읽을 위치를 나타냅니다.
- read 메서드를 호출하면 읽기 인덱스가 이동하며, 읽은 데이터의 크기만큼 증가합니다.
- 초기에는 0으로 설정되며, 데이터를 읽을 때마다 증가합니다.
- 쓰기 인덱스 (Writer Index):
- 쓰기 인덱스는 현재 쓸 위치를 나타냅니다.
- write 메서드를 호출하면 쓰기 인덱스가 이동하며, 쓴 데이터의 크기만큼 증가합니다.
- 초기에는 0으로 설정되며, 데이터를 쓸 때마다 증가합니다.
ByteBuf는 몇가지 특징을 더 가지고 있는데 inbound handler에서 사용되는 ByteBuf는 자동으로 release 하지 않으며 release를 직접 수행해야합니다. outbound handler에서 사용되는 ByteBuf는 write 한 이후에 자동적으로 release 수행합니다.
메모리 누수겁출 옵션
java -Dio.netty.leakDetection.level=ADVANCED
DISABLED | 메모리 누수 검출 기능이 비활성화됩니다. 디폴트 값입니다. |
SIMPLE | 메모리 누수 검출이 활성화되지만, 경고가 발생한 클래스의 이름만 로깅됩니다. |
ADVANCED | 메모리 누수 검출이 활성화되고, 누수가 발생한 클래스의 스택 트레이스까지 로깅됩니다. |
submit()
비동기적으로 작업을 실행하고, 결과를 Future로 반환합니다.
오버로딩된 여러 버전이 있으며, Callable 또는 Runnable을 인자로 받습니다.
Future<T> submit(Callable<T> task); Future<?> submit(Runnable task); Future<T> submit(Runnable task, T result);
invokeAll()
여러 작업을 실행하고, 모든 작업의 결과를 Future의 리스트로 반환합니다.
모든 작업이 완료될 때까지 대기합니다.
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
invokeAny()
여러 작업을 실행하고, 첫 번째로 완료된 작업의 결과를 반환합니다.
어떤 작업이 완료될 때까지 대기합니다.
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
shutdown()
더 이상 새로운 작업을 받지 않고, 이미 제출된 작업이 완료될 때까지 대기합니다.
이후에는 스레드 풀을 종료합니다.
shutdownNow()
현재 실행 중인 모든 작업을 중단하고, 대기 중인 작업을 취소합니다.
즉시 실행 중인 작업을 중단하려고 시도하고, 남아 있는 작업의 리스트를 반환합니다.
isShutdown()
shutdown() 메서드가 호출되었는지 여부를 반환합니다.
isTerminated()
모든 작업이 완료되었는지 여부를 반환합니다.
boolean isTerminated();
awaitTermination()
shutdown() 메서드가 호출된 후, 모든 작업이 완료될 때까지 대기합니다.
최대 대기 시간을 지정할 수 있습니다.
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
https://velog.io/@dm911/Java-blocking-IO-%EC%99%80-non-blocking-IO-%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C
Java blocking IO 와 non blocking IO 에 대해서..
blocking IO 와 non blocking IO 에 대해서 알아보고 예시를 작성해보자.
velog.io
https://blog.naver.com/horajjan/220464906740
[Java] NIO 패키지(java.nio) 소개
'이것이 자바다, 19장'을 인용하였다 자바 4부터 새로운 입출력(NIO: New Input/Output)이라는...
blog.naver.com
'WEB개발 > JAVA' 카테고리의 다른 글
제너릭 Generic (0) | 2024.12.18 |
---|---|
메서드 참조, function, consumer, supplier (0) | 2024.12.18 |
[JAVA] 메모리 스택, 힙 (0) | 2022.10.31 |
[JAVA] Thread-Safe, Concurrent Collection class, Double Checked Locking (0) | 2021.09.23 |
JMX(MBean), JOLOKIA, SpringBoot, Actuator (0) | 2021.09.01 |