자바 4부터 새로운 입출력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 패키지를 위한 서비스 제공자 클래스 |
데이터를 입출력한다는 목적을 동일하지만, 방식에 있어서 큰 차이가 있습니다.
| 구분 | IO | NIO |
|---|---|---|
| 입출력 방식 | 스트림 방식 | 채널 방식 |
| 버퍼 방식 | 넌버퍼 | 버퍼 |
| 비동기 방식 | 지원 안 함 | 지원 |
| 블로킹 / 넌블로킹 방식 | 블로킹 방식만 지원 | 모두 지원 |
IO
스트림 기반입니다. 스트림은 입력과 출력 스트림으로 구분되기 때문에 InputStream과 OutputStream을 별도로 생성해야 합니다.
NIO
채널 기반입니다. 채널은 스트림과 달리 양방향으로 입력과 출력이 가능합니다. 그렇기 때문에 입력과 출력을 위한 별도의 채널을 만들 필요가 없습니다.
IO
출력 스트림이 1바이트를 쓰면 입력 스트림이 1바이트를 읽습니다. 그리고 스트림에서 읽은 데이터를 즉시 처리하기 때문에 스트림으로부터 입력된 전체 데이터를 별도로 저장하지 않으면, 입력된 데이터의 위치를 이동해 가면서 자유롭게 이용할 수 없습니다.
NIO
기본적으로 버퍼를 사용해서 입출력을 합니다. 읽은 데이터를 버퍼에 저장하기 때문에 버퍼 내에서 데이터의 위치를 이동해 가면서 필요한 부분만 읽고 쓰는 것이 가능합니다.
IO
read() 또는 write() 메소드를 호출하면 데이터가 입력되기 전까지 스레드는 블로킹(대기 상태) 됩니다. IO 스레드가 블로킹되면 다른 일을 할 수 없고 블로킹을 빠져나오기 위해 인터럽트도 할 수 없기 때문에 스트림을 닫는 것이 블로킹을 빠져나오는 유일한 방법입니다.
NIO
블로킹과 넌블로킹 특징을 모두 가지고 있습니다. NIO 블로킹은 스레드를 인터럽트함으로써 빠져나올 수 있다는 것이 IO 블로킹과의 차이점입니다. NIO는 입출력 작업 준비가 완료된 채널만 선택해서 작업 스레드가 처리하기 때문에 작업 스레드가 블로킹되지 않습니다.
NIO 넌블로킹의 핵심 객체는 멀티플렉서(multiplexor)인 셀렉터(selector)
셀렉터는 복수 개의 채널 중에서 준비 완료된 채널을 선택하는 방법을 제공
IO
연결 클라이언트 수가 적고, 전송되는 데이터가 대용량이면서 순차적으로 처리될 필요성이 있을 경우에는 IO로 구현하는 것이 좋습니다.
NIO
연결 클라이언트 수가 많고, 하나의 입출력 처리 작업이 오래 걸리지 않는 경우에 사용하는 것이 좋습니다.
NIO에서는 데이터를 입출력하기 위해 항상 버퍼를 사용해야 합니다.
버퍼는 읽고 쓰기가 가능한 메모리 배열
데이터 타입에 따라 분류될 수 있고, 어떤 메모리를 사용하느냐에 따라 다이렉트와 넌다이렉트로 분류할 수도 있습니다.

MappedByteBuffer는 파일의 내용에 랜덤하게 접근하기 위해서 파일의 내용을 메모리와 맵핑시킨 버퍼
| 구분 | 넌다이렉트 버퍼 | 다이렉트 버퍼 |
|---|---|---|
| 사용하는 메모리 공간 | JVM의 힙 메모리 | 운영체제의 메모리 |
| 버퍼 생성 시간 | 빠름 | 느림 |
| 버퍼의 크기 | 작음 | 큼(큰 데이터를 처리할 때 유리 |
| 입출력 성능 | 낮음 | 높음(입출력이 빈번할 때 유리) |
각 데이터 타입별로 넌다이렉트 버퍼를 생성하기 위해서는 각 Buffer 클래스의 allocate()와 wrap() 메소드를 호출하고, 다이렉트 버퍼는 ByteBuffer의 allocateDirect() 메소드를 호출합니다.
ByteBuffer byteBuffer = ByteBuffer.allocate(100);
CharBuffer charBuffer = CharBuffer.allocate(100);
byte[] byteArray = new byte[100];
ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray);
char[] charArray = new char[100];
CharBuffer charBuffer = CharBuffer.wrap(charArray, 0, 50);
// charBuffer는 추가적으로 CharSequence 타입의 매개값을 갖는 wrap() 메소드를 제공
CharBuffer charBuffer = CharBuffer.wrap("NIO 입출력은 버퍼를 이용합니다.");
// 100개의 byte값 저장
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100);
// 50개의 char값 저장
CharBuffer charBuffer = ByteBuffer.allocateDirect(100).asCharBuffer();
// 25개의 int값 저장
IntBuffer intBuffer = ByteBuffer.allocateDirect(100).asIntBuffer();
운영체제마다 바이트 처리 순서에 차이가 있습니다. 앞쪽 바이트부터 먼저 처리하는 것을 Big endian이라고 하고, 뒤쪽 바이트부터 먼저 처리하는 것을 Little endian이라고 합니다.

Little endian으로 동작하는 운영체제에서 만든 데이터 파일을 Big endian으로 동작하는 운영체제에서 읽는다면 ByteOrder 클래스로 데이터 순서를 맞춰야 합니다.
JVM은 Big endian으로 동작함
운영체제와 JVM의 바이트 해석 순서가 다를 경우에는 JVM이 운영체제와 데이터를 교환할 때 자동적으로 처리해주지만, 다이렉트 버퍼일 경우 운영체제의 native I/O를 사용하므로 운영체제의 기본 해석 순서로 JVM의 해석 순서를 맞추는 것이 성능에 도움이 됩니다.
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100).order(ByteOrder.nativeOrder());
| 속성 | 설명 |
|---|---|
| position | 현재 읽거나 쓰는 위치값이다. 인덱스 값이기 때문에 0부터 시작하며, limit보다 큰 값을 가질 수 없다. 만약 position과 limit의 값이 같아진다면 더 이상 데이터를 쓰거나 읽을 수 없다는 뜻이 된다. |
| limit | 버퍼에서 읽거나 쓸 수 있는 위치의 한계를 나타낸다. 이 값은 capacity보다 작거나 같은 값을 가진다. 최초에 버퍼를 만들었을 때는 capacity와 같은 값을 가진다. |
| capacity | 버퍼의 최대 데이터 개수(메모리 크기)를 나타낸다. 인덱스 값이 아니라 수량임을 주의 |
| mark | reset() 메소드를 실행했을 때에 돌아오는 위치를 지정하는 인덱스로서 mark() 메소드로 지정할 수 있다. 주의할 점은 반드시 position 이하의 값으로 지정해주어야 한다. position이나 limit의 값이 mark 값보다 작은 경우, mark는 자동 제거된다. mark가 없는 상태에서 reset() 메소드를 호출하면 InvalidMarkException이 발생한다. |
mark는 position보다 클 수 없고, position은 limit보다 클 수 없으며, limit은 capacity보다 클 수 없다.
0 <= mark <= position <= limit <= capacity
| 리턴 타입 | 메소드(매개 변수) | 설명 |
|---|---|---|
| Object | array() | 버퍼가 래핑(wrap)한 배열을 리턴 |
| int | arrayOffset() | 버퍼의 첫 번째 요소가 있는 내부 배열의 인덱스를 리턴 |
| int | capacity() | 버퍼의 전체 크기를 리턴 |
| Buffer | clear() | 버퍼의 위치 속성을 초기화(position=0, limit=capacity) |
| Buffer | flip() | limit을 position으로, position을 0 인덱스로 이동 |
| boolean | hasArray() | 버퍼가 래핑(wrap)한 배열을 가지고 있는지 여부 |
| boolean | hasRemaining() | position과 limit 사이에 요소가 있는지 여부(position < limit) |
| boolean | isDirect() | 운영체제의 버퍼를 사용하는지 여부 |
| boolean | isReadOnly() | 버퍼가 읽기 전용인지 여부 |
| int | limit() | limit 위치를 리턴 |
| Buffer | limit(int newLimit) | newLimit으로 limit 위치를 설정 |
| Buffer | mark() | 현재 위치를 mark로 표시 |
| int | position() | position 위치를 리턴 |
| Buffer | position(int newPosition) | newPosition으로 position 위치를 설정 |
| int | remaining() | position과 limit 사이의 요소의 개수 |
| Buffer | reset() | position을 mark 위치로 이동 |
| Buffer | rewind() | position을 0 인덱스로 이동 |


| 예외 | 설명 |
|---|---|
| BufferOverflowException | position이 limit에 도달했을 때 put()을 호출하면 발생 |
| BufferUnderflowException | position이 limit에 도달했을 때 get()을 호출하면 발생 |
| InvalidMarkException | mark가 없는 상태에서 reset() 메소드를 호출하면 발생 |
| ReadOnlyBufferException | 읽기 전용 버퍼에서 put() 또는 compact() 메소드를 호출하면 발생 |
채널을 통해 문자열을 파일이나 네트워크로 전송하려면 특정 문자셋UTF-8 EUC-KR으로 인코딩해서 ByteBuffer로 변환해야 합니다. 우선 문자셋을 표현하는 java.nio.charset.Charset 객체가 필요합니다.
Charset charset = Charset.forName("UTF-8"); // 매개값으로 주어진 문자셋
Charset charset = Charset.defaultCharset(); // 운영체제가 사용하는 디폴트 문자셋
encode() 메소드를 호출해서 Charset을 이용해서 문자열을 ByteBuffer로 변환합니다.
String data = ...;
ByteBuffer byteBuffer = charset.encode(data);
파일이나 네트워크로부터 읽은 ByteBuffer가 특정 문자셋으로 인코딩되어 있을 경우, decode() 메소드로 디코딩해야만 문자열로 복원할 수 있습니다.
ByteBuffer byteBuffer = ...;
String data = charset.decode(byteBuffer).toString();
int 타입은 4byte 크기를 가지므로 int[] 배열 크기 또는 IntBuffer의 capacity보다 4배 큰 capacity를 가진 ByteBuffer를 생성하고, ByteBuffer의 putInt() 메소드로 정수값을 하나씩 저장합니다.
int[] data = new int[] {10, 20};
IntBuffer intBuffer = IntBuffer.wrap(data);
ByteBuffer byteBuffer = ByteBuffer.allocate(intBuffer.capacity()*4);
for (int i=0; i<intBuffer.capacity(); i++) {
byteBuffer.putInt(intBuffer.get(i));
}
byteBuffer.flip();
파일이나 네트워크로부터 입력된 ByteBuffer에 4바이트씩 연속된 int 데이터가 저장되어 있을 경우, int[] 배열로 복원이 가능합니다.
ByteBuffer byteBuffer = ...;
IntBuffer intBuffer = byteBuffer.asIntBuffer();
int[] data = new int[intBuffer.capacity()];
intBuffer.get(data);
asXXXBuffer() 메소드를 사용해서 short, int, long, float, double 데이터를 가진 ByteBuffer와의 변환이 가능함
java.nio.channels.FileChannel을 이용하면 파일 읽기와 쓰기를 할 수 있습니다. FileChannel은 동기화 처리가 되어있기 때문에 멀티 스레드 환경에서 안전합니다.
// IO의 FileXXXStream의 getChannel() 메소드를 호출해서 얻을 수도 있음
FileChannel fileChannel = FileChannel.open(Path path, OpenOption... options);
첫 번째 path 매개값은 생성하거나 열고자 하는 파일의 경로를 Path 객체로 생성해서 지정하고, 두 번째 options 매개값은 열기 옵션 값입니다. StandardOpenOption의 열거 상수를 사용할 수 있습니다.
| 열거 상수 | 설명 |
|---|---|
| READ | 읽기용으로 파일을 연다. |
| WRITE | 쓰기용으로 파일을 연다. |
| CREATE | 파일이 없다면 새 파일을 생성한다. |
| CREATE_NEW | 새 파일을 만든다. 이미 파일이 있으면 예외와 함께 실패한다. |
| APPEND | 파일 끝에 데이터를 추가한다(WRITE나 CREATE와 함께 사용됨). |
| DELETE_ON_CLOSE | 채널을 닫을 때 파일을 삭제한다(임시 파일을 삭제할 때 사용). |
| TRUNCATE_EXISTING | 파일을 0바이트로 잘라낸다(WRITE 옵션과 함께 사용됨). |
FileChannel fileChannel = FileChannel.open(
Paths.get("C:/Temp/file.txt"),
StandardOpenOption.CREATE_NEW,
StandardOpenOption.READ,
StandardOpenOption.WRITE
);
// FileChannel을 더 이상 이용하지 않을 경우
filechannel.close();
// 쓰기: ByteBuffer에서 파일로 쓰여진 바이트 수를 리턴
int bytesCount = fileChannel.write(ByteBuffer src);
// 읽기: 파일에서 ByteBuffer로 읽혀진 바이트 수를 리턴
int bytesCount = fileChannel.read(ByteBuffer dst);
하나의 ByteBuffer를 사이에 두고, 읽기용 FileChannel과 쓰기용 FileChannel이 읽기와 쓰기를 교대로 번갈아 수행하도록 구현합니다.
public class Main {
public static void main(String[] args) {
Path from = Paths.get("file1.txt");
Path to = Paths.get("file2.txt");
FileChannel fileChannel_from = FileChannel.open(
form, StandardOpenOption.READ);
FileChannel fileChannel_to = FileChannel.open(
to, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
ByteBuffer buffer = ByteBuffer.allocateDirect(100);
int byteCount;
while(true) {
buffer.clear();
byteCount = fileChannel_from.read(buffer);
if (byteCount == -1) break;
buffer.flip();
fileChannel_to.write(buffer);
}
fileChannel_from.close();
fileChannel_to.close();
System.out.pritnln("파일 복사 성공");
}
}
단순히 파일을 복사할 목적이라면 File 클래스의 copy() 메소드를 사용하는 것이 편리합니다.
Path targetPath = Files.copy(Path source, Path target, CopyOption... options);
| 열거 상수 | 설명 |
|---|---|
| REPLACE_EXISTING | 타겟 파일이 존재하면 대체한다. |
| COPY_ATTRIBUTES | 파일의 속성까지도 복사한다. |
| NOFOLLOW_LINKS | 링크 파일일 경우 링크 파일만 복사하고 링크된 파일은 복사하지 않는다. |
public class Main {
public static void main(String[] args) {
Path from = Paths.get("file1.txt");
Path to = Paths.get("file2.txt");
Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING);
System.out.println("파일 복사 성공");
}
}
FileChannel의 read() write() 메소드는 파일 입출력 작업 동안 블로킹됩니다. 따라서 별도의 작업 스레드를 만들어야 하는데, 동시에 처리해야 할 파일 수가 많다면 스레드의 수도 그만큼 증가하기 때문에 문제가 됩니다. 그래서 자바 NIO는 비동기 파일 채널 AsynchronousFileChannel을 제공합니다.

// 방법 1:
AsychronousFileChannel fileChannel = AsychronousFileChannel.open(
Path file,
OpenOption... options
);
// 방법 2:
AsychronousFileChannel fileChannel = AsychronousFileChannel.open(
Path file,
Set<? extends OpenOption> options,
ExecutorService executor,
FileAttribute<?>... attrs
);
// 닫기
fileChannel.close();
read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer, A> handler);
write(ByteBuffer src, long position, A attachment, CompletionHandler<Integer, A> handler);
dst와 src 매개값은 읽거나 쓰기 위한 ByteBufferposition 매개값은 파일에서 읽을 위치이거나 쓸 위치. 파일의 첫 번째 바이트부터 읽거나 첫 번째 위치에 바이트를 쓰고 싶다면 position 값을 0으로 설정attachment 매개값은 콜백 메소드로 전달한 첨부 객체. 필요 없다면 null 대입 가능handler 매개값은 CompletionHandler<Integer, A> 구현 객체를 지정함. Integer는 입출력 작업의 결과 타입으로, read()와 write()가 읽거나 쓴 바이트 수를 나타냄A는 첨부 객체 타입으로 CompletionHandler 구현 객체를 작성할 때 임의로 지정 가능. 첨부 객체가 필요 없다면 A는 void가 됨CompletionHandler<Integer, A> 구현 객체는 비동기 작업이 정상적으로 완료된 경우와 예외 발생으로 실패된 경우에 자동 콜백되는 다음 두 가지 메소드를 가지고 있어야 함| 리턴 타입 | 메소드명(매개 변수) | 설명 |
|---|---|---|
| void | completed(Integer result, A attachment) | 작업이 정상적으로 완료된 경우 콜백 |
| void | failed(Throwable exc, A attachment) | 예외 때문에 작업이 실패된 경우 콜백 |
new CompletionHandler<Integer, A>() {
@Override
public void completed(Integer result, A attachment) {...}
@Override
public void faild(Throwable exc, A attachment) {...}
}