1. BIO (Blocking I/O)
가장 기본적인 네트워크 I/O 모델로, 모든 I/O 작업이 블로킹 방식으로 동작합니다. 서버는 클라이언트 연결을 기다리는 accept() 호출과 데이터 읽기/쓰기 과정에서 모두 스레드가 멈춥니다.
서버 예제
package network.bio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class BioServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("127.0.0.1", 8888));
while (true) {
Socket clientSocket = serverSocket.accept(); // 블로킹 호출
new Thread(() -> processClient(clientSocket)).start();
}
}
static void processClient(Socket socket) {
try {
byte[] buffer = new byte[1024];
int bytesRead = socket.getInputStream().read(buffer); // 블로킹 read
System.out.println(new String(buffer, 0, bytesRead));
socket.getOutputStream().write(buffer, 0, bytesRead);
socket.getOutputStream().flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
클라이언트 예제
package network.bio;
import java.io.IOException;
import java.net.Socket;
public class BioClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8888);
socket.getOutputStream().write("HelloServer".getBytes());
socket.getOutputStream().flush();
System.out.println("메시지 전송 완료, 응답 대기 중...");
byte[] response = new byte[1024];
int length = socket.getInputStream().read(response);
System.out.println(new String(response, 0, length));
socket.close();
}
}
BIO의 특징
ServerSocket.accept()는 클라이언트가 연결될 때까지 블로킹InputStream.read()와OutputStream.write()도 블로킹- 각 연결마다 별도의 스레드가 필요하여 리소스 소모가 큼
- 연결 수가 적은 환경에서는 단순하고 유지보수가 쉬움
2. NIO (Non-blocking I/O)
NIO는 셀렉터(Selector)를 사용하여 단일 스레드로 여러 채널을 관리할 수 있습니다. 채널을 논블로킹 모드로 설정하고, 이벤트가 발생한 채널만 처리합니다.
단일 스레드 NIO 서버
package network.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NioServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress("127.0.0.1", 8888));
serverChannel.configureBlocking(false); // 논블로킹 모드
System.out.println("서버 시작됨: " + serverChannel.getLocalAddress());
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 이벤트 발생 시까지 블로킹
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
processKey(key);
}
}
}
static void processKey(SelectionKey key) {
if (key.isAcceptable()) {
try {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(key.selector(), SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
} else if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(512);
try {
int bytesRead = clientChannel.read(buffer);
if (bytesRead != -1) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, bytesRead));
ByteBuffer response = ByteBuffer.wrap("HelloClient".getBytes());
clientChannel.write(response);
}
clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
리액터(Reactor) 패턴을 적용한 NIO
셀렉터가 연결 수락만 담당하고, 실제 I/O 처리는 별도의 워커 스레드 풀에서 수행합니다.
package network.nio;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ReactorServer {
private ExecutorService workerPool = Executors.newFixedThreadPool(50);
private Selector selector;
public static void main(String[] args) throws IOException {
ReactorServer server = new ReactorServer();
server.initialize(8000);
server.startListening();
}
public void initialize(int port) throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(port));
this.selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("리액터 서버 시작 성공!");
}
public void startListening() throws IOException {
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
workerPool.execute(new IoTask(key));
}
}
}
}
static class IoTask extends Thread {
private SelectionKey key;
IoTask(SelectionKey key) {
this.key = key;
}
@Override
public void run() {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream output = new ByteArrayOutputStream();
try {
int bytesRead;
while ((bytesRead = client.read(buffer)) > 0) {
buffer.flip();
output.write(buffer.array(), 0, bytesRead);
buffer.clear();
}
output.close();
byte[] data = output.toByteArray();
ByteBuffer writeBuffer = ByteBuffer.allocate(data.length);
writeBuffer.put(data);
writeBuffer.flip();
client.write(writeBuffer);
if (bytesRead == -1) {
client.close();
} else {
key.interestOps(key.interestOps() | SelectionKey.OP_READ);
key.selector().wakeup();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
NIO의 핵심 개념
- 셀렉터가 여러 채널의 이벤트를 모니터링
- 채널은 논블로킹 모드로 설정하여 I/O 대기 없음
- 버퍼(ByteBuffer)를 통해 데이터를 읽고 쓰기
- 이벤트 기반으로 불필요한 컨텍스트 스위칭 감소
3. AIO (Asynchronous I/O)
AIO는 운영체제 커널이 I/O 작업 완료를 비동기적으로 통지하는 모델입니다. NIO와 달리 폴링(polling)이 필요 없으며, CompletionHandler를 통해 결과를 받습니다.
단일 스레드 AIO 서버
package network.aio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
public class AioServer {
public static void main(String[] args) throws Exception {
final AsynchronousServerSocketChannel serverChannel =
AsynchronousServerSocketChannel.open()
.bind(new InetSocketAddress(8888));
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel client, Object attachment) {
serverChannel.accept(null, this);
try {
System.out.println("클라이언트 연결됨: " + client.getRemoteAddress());
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
System.out.println(new String(attachment.array(), 0, result));
client.write(ByteBuffer.wrap("HelloClient".getBytes()));
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
while (true) {
Thread.sleep(1000);
}
}
}
스레드 풀을 사용한 AIO 서버
package network.aio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AioThreadPoolServer {
public static void main(String[] args) throws Exception {
ExecutorService threadPool = Executors.newCachedThreadPool();
AsynchronousChannelGroup channelGroup =
AsynchronousChannelGroup.withCachedThreadPool(threadPool, 1);
final AsynchronousServerSocketChannel serverChannel =
AsynchronousServerSocketChannel.open(channelGroup)
.bind(new InetSocketAddress(8888));
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel client, Object attachment) {
serverChannel.accept(null, this);
try {
System.out.println("클라이언트 연결됨: " + client.getRemoteAddress());
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
System.out.println(new String(attachment.array(), 0, result));
client.write(ByteBuffer.wrap("HelloClient".getBytes()));
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
while (true) {
Thread.sleep(1000);
}
}
}
AIO의 특징
- 운영체제 커널이 I/O 완료를 콜백으로 통지
- NIO보다 추상화 수준이 높아 코드가 간결
- Linux에서는 NIO와 내부 구현이 유사하여 성능 차이가 크지 않음
- Windows 환경에서는 IOCP 기반으로 동작
4. Netty
Netty는 NIO를 기반으로 한 고성능 네트워크 프레임워크로, AIO 스타일의 비동기 처리를 지원합니다. 내부적으로 NIO를 사용하지만 AIO와 유사한 프로그래밍 모델을 제공합니다.
Maven 의존성
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.72.Final</version>
</dependency>
Netty 서버 구현
package network.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
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.util.CharsetUtil;
public class NettyServerExample {
public static void main(String[] args) {
new NettyEchoServer(8888).start();
}
}
class NettyEchoServer {
private int port;
public NettyEchoServer(int port) {
this.port = port;
}
public void start() {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new EchoHandler());
}
});
try {
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println("Netty 서버 시작됨 - 포트: " + port);
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
class EchoHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println("서버: 데이터 수신됨");
ByteBuf buffer = (ByteBuf) msg;
System.out.println("수신 내용: " + buffer.toString(CharsetUtil.UTF_8));
ctx.writeAndFlush(msg);
ctx.close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
Netty 클라이언트 구현
package network.netty;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
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.util.ReferenceCountUtil;
public class NettyClientExample {
public static void main(String[] args) {
new NettyClient().connect();
}
}
class NettyClient {
private void connect() {
EventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
System.out.println("채널 초기화 완료!");
ch.pipeline().addLast(new ClientMessageHandler());
}
});
try {
System.out.println("서버에 연결 중...");
ChannelFuture future = bootstrap.connect("127.0.0.1", 8888).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
workerGroup.shutdownGracefully();
}
}
}
class ClientMessageHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("채널 활성화됨.");
ChannelFuture future = ctx.writeAndFlush(
Unpooled.copiedBuffer("HelloNetty".getBytes())
);
future.addListener((ChannelFutureListener) f ->
System.out.println("메시지 전송 완료!")
);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
ByteBuf buffer = (ByteBuf) msg;
System.out.println("서버 응답: " + buffer.toString());
} finally {
ReferenceCountUtil.release(msg);
}
}
}
Netty의 주요 특징
- bossGroup과 workerGroup으로 구분된 이벤트 루프 그룹 사용
- bossGroup: 클라이언트 연결 수락 담당
- workerGroup: 실제 I/O 처리 담당
- 파이프라인(Pipeline)을 통한 핸들러 체인 방식
- 비동기 이벤트 기반 아키텍처
- Linux 환경에서 높은 성능 제공
모델 비교
- BIO: 간단하지만 스레드 리소스 소모가 큼, 소규모 연결에 적합
- NIO: 단일 스레드로 다중 연결 처리 가능, 폴링 필요
- AIO: 비동기 콜백 기반, 코드가 간결하나 Linux에서 성능 이점이 제한적
- Netty: NIO 기반 고성능 프레임워크, 생산성과 성능의 균형