이번에 업무에서 NIO를 사용할 일이 있어서 공부하면서 내용을 정리해보고자 합니다.
해당 포스팅은 palpit Vlog님의 블로그의 내용을 보면서 정리한 내용입니다.
NIO란?
NIO
란 New Input/output
의 약자로 Java4부터 버전1이 지원이 되었으며 Java7부터는 버전2가 지원이 되었습니다.
NIO의 대표적인 기능으로는 Non-blocking이 있으며,
IO vs NIO
구분 | IO | NIO |
---|---|---|
입출력 방식 | Stream | Channel |
버퍼 방식 | Non-Buffer | Buffer |
비동기 방식 | 지원 X | 지원 |
Blocking / Non-Blocking 방식 | Blocking Only | Blocking / Non-Blocking |
적합한 케이스 | 연결 클라이언트가 적고, IO가 큰 경우 (대용량) | 연결 클라이언트가 많고 IO 처리가 작은 경우 (저용량) |
Stream vs Channel
Stream
IO
의 입출력 방식- 입력 스트림과 출력 스트림을 따로 만들어줘야 함
Channel
NIO
의 입출력 방식- 양방향으로 입출력이 가능
- 입출력을 위한 별도의 채널을 만들 필요 X
- 하나의 파일에서 데이터를 읽고 쓴다면, FileChannel 하나만 필요
Non-Buffer vs Buffer
IO
에서는 출력 스트림이 1바이트를 쓰면, 입력 스트림에서 1바이트를 읽습니다. 하지만 이런 형태는 느리기 때문에,
버퍼를 사용해서 여러개의 바이트를 한꺼번에 입력받고 저장했다가 한번에 출력하는 형태로 사용하여 성능을 높입니다.
IO는 기본적으로 버퍼를 지원하지 않기 때문에, 버퍼를 제공해주는 보조 스트림인 BufferedInputStream
과 BufferedOutputStream
을 이용합니다.
NIO
는 기본적으로 버퍼가 지원이 되기때문에 IO
보다 성능이 좋습니다. 채널은 버퍼에 저장된 데이터를 출력하고 입력된 데이터를 버퍼에 저장합니다.
Blocking vs Non-Blocking
Blocking
: IO
의 방식으로 각각의 스트림에서 read()
와 write()
가 호출이 되면 데이터가 입력되고, 데이터가 출력되기전까지,
스레드는 블로킹(=멈춤) 상태
가 됩니다. 이렇게 되면 작업이 끝날때까지 기다려야 하며, 그 이전에는 해당 IO 스레드는 사용할 수 없게 되고,
인터럽트도 할 수 없습니다. 블로킹을 빠져나오려면 스트림을 닫는 방법 밖에 없습니다.
Non-Blocking
: NIO
는 Blocking
과 Non-Blocking
모두 지원을 하며, NIO
의 Blocking 상태
에서는 Interrupt
를
이용하여 빠져나올수도 있습니다. Non-Blocking
은 입출력 작업이 준비가 되면 채널만 선택해서 처리하기 때문에 입출력 작업시 스레드가 멈추지 않습니다.
IO와 NIO의 Blocking 모드 차이
IO
와 NIO
모두 Blocking 모드
를 지원하지만, 블로킹을 빠져나오기 위한 방법이 다릅니다.
IO
는 Interrupt
를 이용해도 빠져나갈 수 없고, 오직 Stream을 닫는
수밖에 없지만,
NIO
는 Interrupt
를 통해서 빠져나갈 수 있습니다.
종류 | 블로킹을 나가는 방법 |
---|---|
IO |
Close Stream |
NIO |
Intterupt & Close Stream |
Path(경로 정의)
NIO
에서 파일의 경로를 지정하기 위해서는 Path
를 사용합니다, 경로는 절대경로, 상대경로 모두 지원
합니다.
import java.nio.file.Paths
Path path = Paths.get(경로(String 타입));
Path path = Paths.get(경로(URI 타입));
예시
import java.nio.file.Path;
import java.nio.file.Paths;
public class test {
public static void main(String[] args) {
Path path = Paths.get("/Users/jungwoon/test.txt");
System.out.println("fileName: " + path.getFileName()); // 선택된 파일의 이름
System.out.println("parentName: " + path.getParent()); // 선택된 파일의 상위 디렉토리 정보
System.out.println();
for (int i = 0; i < path.getNameCount(); i++) {
System.out.println("(" + i + ") : " + path.getName(i));
}
}
}
결과
fileName: test.txt
parentName: /Users/jungwoon
(0) : Users
(1) : jungwoon
(2) : test.txt
FileSystem
운영체제의 파일 시스템도 아래와 같은 방법으로 접근할 수 있습니다.
FileSystem fileSystem = FileSytems.getDefault();
예시
import java.io.IOException;
import java.nio.file.*;
public class test {
public static void main(String[] args) throws IOException {
FileSystem fileSystem = FileSystems.getDefault();
for (FileStore fileStore : fileSystem.getFileStores()) {
System.out.println("Driver : " + fileStore.name()); // 드라이버 명
System.out.println("Type : " + fileStore.type()); // 파일 시스템 종류
System.out.println("Total Space : " + fileStore.getTotalSpace()); // 전체 드라이브 공간
System.out.println("Usable Space : " + fileStore.getUsableSpace()); // 사용가능한 드라이브 공간
System.out.println("------------------------------------------");
}
}
}
결과
Driver : /dev/disk1s1
Type : apfs
Total Space : 500068036608
Unallocated Space : 219067645952
Usable Space : 216195403776
------------------------------------------
Driver : devfs
Type : devfs
Total Space : 340992
Unallocated Space : 0
Usable Space : 0
------------------------------------------
Driver : /dev/disk1s4
Type : apfs
Total Space : 500068036608
Unallocated Space : 497920528384
Usable Space : 216195403776
------------------------------------------
Driver : map -hosts
Type : autofs
Total Space : 0
Unallocated Space : 0
Usable Space : 0
------------------------------------------
Driver : map auto_home
Type : autofs
Total Space : 0
Unallocated Space : 0
Usable Space : 0
------------------------------------------
와치 서비스(WatchService)
와치 서비스는 디렉토리 내부에서 파일의 변화(생성, 삭제, 수정)를 감지하는데 사용합니다.
디렉토리(Path)에 WatchService
를 등록하면 해당 디렉토리의 변화를 감지합니다.
import java.io.IOException;
import java.nio.file.*;
import java.util.List;
public class test {
public static void main(String[] args) throws IOException {
WatchService watchService = FileSystems.getDefault().newWatchService();
Path path = Paths.get("/Users/jungwoon");
path.register(
watchService, // 위에서 만든 와치 인스턴스 등록
StandardWatchEventKinds.ENTRY_CREATE, // 생성에 대해서 감지
StandardWatchEventKinds.ENTRY_MODIFY, // 수정에 대해서 감지
StandardWatchEventKinds.ENTRY_DELETE); // 삭제에 대해서 감지
new Thread(() -> {
try {
while (true) {
WatchKey watchKey = watchService.take(); // 이벤트에 해당하는 WatchKey를 가져오기
List<WatchEvent<?>> list = watchKey.pollEvents(); // WatchKey에 해당하는 이벤트 목록 가져오기
for (WatchEvent watchEvent : list) {
WatchEvent.Kind kind = watchEvent.kind(); // 어떤 이벤트인지 종류 확인 (생성, 수정, 삭제)
String fileName = ((Path) watchEvent.context()).getFileName().toString(); // 어떤 파일인지 이름 가져오기
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
System.out.println("Created : " + fileName);
} else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
System.out.println("Modified : " + fileName);
} else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
System.out.println("Deleted : " + fileName);
}
}
// 다 쓴 watchKey는 초기화를 해야하는데 안하면 동일한 watchKey가 큐에 또 다시 들어가게 됨
// 초기화에 성공하면 true, 실패하면 false
if (!watchKey.reset())
break;
}
watchService.close(); // 끝나면 서비스 종료
}
catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
결과
Modified : .zsh_history
Created : test2.txt
Modified : .zsh_history
Modified : .zsh_history
Deleted : test.txt
Deleted : test2.txt
버퍼
버퍼는 저장되는 데이터 타입
과 어떤 메모리
를 사용하느냐에 따라 구분이 됩니다.
데이터 타입에 따른 버퍼
NIO
버퍼는 저장되는 데이터 타입에 따라서 별도의 클래스가 제공이 되는데 모두 Buffer Class
를 상속받고 있습니다.
출처 : Google Images
Direct Buffer vs Non Direct Buffer
버퍼가 사용하는 메모리 위치에 따라서 다이렉트 버퍼
와 넌 다이렉트 버퍼
로 분류할수 있습니다.
구분 | Direct Buffer | Non Direct Buffer |
---|---|---|
사용 공간 | OS의 메모리 | JVM의 힙 메모리 |
버퍼 생성 속도 | 느리다 | 빠르다 |
버퍼의 크기 | 크다 | 작다 |
I/O 성능 | 높다 | 낮다 |
적합한 케이스 | 한 번 생성해놓고 재사용시 적합 | 빈번하게 생성해야할때 적합 |
다이렉트 버퍼
는 채널을 사용
해서 버퍼의 데이터를 읽고 저장할 경우에만 운영체제의 native I/O를 사용
합니다.
만약 채널을 사용하지 않고 ByteBuffer의 get() / put() 메서드 사용시에는 내부적으로 JNI를 호출
해야하기 때문에
JNI 호출이라는 오버 헤더가 추가
됩니다.
생성방법
넌 다이렉트 버퍼
의 생성 (아래 예시는 ByteBuffer
를 만드는 예시 입니다, 다른 종류의 버퍼들도 가능합니다.)
ByteBuffer buffer = ByteBuffer.allocate(100); // 최대 100Byte를 저장하는 넌 다이렉트 버퍼
다이렉트 버퍼
의 생성 (아래 예시는 ByteBuffer
를 만드는 예시 입니다, 다른 종류의 버퍼들도 가능합니다.)
ByteBuffer buffer = ByteBuffer.allocateDirect(100); // 최대 100Byte를 저장하는 다이렉트 버퍼
Little endian vs Big endian (바이트 해석순서)
데이터 처리시 바이트의 처리 순서는 운영체젬다 차이가 있는데, 크게 Big endian
방식과 Little endian
방식이 있습니다.
Big endian
:앞쪽
바이트부터 먼저 처리Little endian
:뒤쪽
바이트부터 먼저 처리
아래 코드를 이용하여, 자신의 OS의 endian 타입도 가져올 수 있습니다.
저는 Mac OS
를 사용하는데 LITTLE_ENDIAN
이네요
System.out.println("os name : " + System.getProperty("os.name"));
System.out.println("type of endian : " + ByteOrder.nativeOrder());
결과
OS Name : Mac OS X
Type of endian : LITTLE_ENDIAN
JVM
은 기본적으로 BIG_ENDIAN
을 사용하는데, 보통은 JVM
이 자동적으로 처리를 해주기 때문에 문제가 없습니다.
하지만 다이렉트 버퍼
일 경우에는 운영체제의 기본 해석 순서와 맞춰주는게 성능에 도움이 됩니다.
해석 순서가 다를때 맞춰주는 방법은 다음과 같이 order(ByteOrder.nativeOrder())
를 이용할 수 있습니다.
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100).order(ByteOrder.nativeOrder());
버퍼의 위치 속성
- position : 현재 읽거나 쓰는 위치값,
0 ~ limit
- limit : 버퍼에서 읽거나 쓸 수 있는 위치의 한계, 이 값은
capacity
보다 작거나 같은 값을 가진다. - capacity : 버퍼의 최대 데이터 개수 (
인덱스가 아니라 크기
,크기가 7일때 인덱스는 0 ~ 6
) - mark :
reset() 메서드 실행시 돌아오는 위치
를 지정하는 인덱스, mark() 메서드로 지정 가능하나반드시 position 이하의 값
이어야 한다.
그래서 위 4개의 위치 속성은 다음과 같은 규칙을 가집니다.
0 <= mark <= position <= limit <= capacity
위치속성 변경 메소드
- flip() :
limit
를 현재position
의 위치로 설정하고position
은0번
으로 갑니다. - mark() : 현재
position
의 위치에mark
를 놓습니다. - rewind() :
limit은 유지
,position
을0번
으로 옮깁니다.mark
는 자동적으로사라집니다
. - reset() :
position
을mark 위치
로 가져다 놓습니다. - clear() :
limit은 capacity
로position은 0
으로mark는 사라짐
- compact() :
position ~ limit 데이터가 0
으로 복사되고position은 복사된 데이터 다음
으로 이동,그 외에 데이터는 유지
버퍼에 데이터를 읽고 쓰는 메소드들
- get() : 데이터를 읽는 메소드
- put() : 데이터를 쓰는 메소드
- 상대적 : position으로부터 데이터를 읽고 쓰는 경우
- 절대적 : position에 상관없이 주어진 인덱스에서 데이터를 읽고 쓰는 경우
File Channel
FileChannel
은 동기화 처리가 되기 때문에 멀티스레드 환경에서도 안전하게 사용이 가능합니다.
파일 채널 얻는 방법
FileChannel
은 open(), FileInputStream(), FileOutputStream(), getChannel() 메서드를 호출해서 얻을 수 있습니다.
아래는 /Users/jungwoon/
를 읽고 쓸 수 있도록 채널을 열었습니다.
// "/Users/jungwoon/'를 읽고 쓸 수 있도록 채널을 생성
FileChannel fileChannel = FileChannel.open(
Paths.get("/Users/jungwoon/"),
StandardOpenOption.READ,
StandardOpenOption.WRITE);
다 쓴 채널은 close()
를 이용하여 닫아줍니다.
fileChannel.close();
파일의 읽고 쓰기
파일에 바이트를 읽고 쓰려면 다음과 같이 FileChannel()의 read()메서드와 write()메서드를 호출하면 됩니다. 사용하는 방법은 다음과 같습니다.
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.util.List;
public class test {
public static void main(String[] args) throws IOException {
FileChannel writeFileChannel
= FileChannel.open(Paths.get("/Users/jungwoon/test.txt"), StandardOpenOption.WRITE);
// 쓰기
ByteBuffer byteBuffer = Charset.defaultCharset().encode("Let's go NIO coding");
int byteLength = writeFileChannel.write(byteBuffer); // 쓴 바이트의 길이
System.out.println("byte length is : " + byteLength);
writeFileChannel.close();
// ----------------------------------------------------------------------------------------------------
FileChannel readFileChannel
= FileChannel.open(Paths.get("/Users/jungwoon/test.txt"), StandardOpenOption.READ);
// 읽기
ByteBuffer buffer = ByteBuffer.allocate(100); // capacity를 100으로
StringBuilder readData = new StringBuilder();
int byteCount;
while (true) {
byteCount = readFileChannel.read(buffer); // 100바이트를 읽습니다.
// 더 이상 읽을 바이트가 없으면 -1을 리턴
if (byteCount == -1)
break;
buffer.flip();
readData.append(Charset.defaultCharset().decode(buffer).toString());
buffer.clear();
}
readFileChannel.close();
System.out.println("readData is : " + readData);
}
}
결과
byte length is : 19
readData is : Let's go NIO coding
파일 비동기 채널
FileChannel에서 read()와 write()가 호출되는 동안 스레드는 블로킹(=멈춤)
이 됩니다. 그래서 보통 이런 경우는 별도의 스레드를 생성해서 처리
하는데,
이러한 I/O가 동시에 많이 처리되야 한다면 스레드 수도 함께 증가하기 때문에 문제가 발생할 수 있습니다.
그래서 NIO
는 비동기 파일 채널(AsynchronousFileChannel)을 제공
하고 있습니다,
비동기 파일 채널은 데이터의 read()와 write() 호출시에 스레드 풀에게 작업 요청
을 하고,
해당 작업이 끝나면 콜백 메서드가 호출
됩니다.
사용 방법
이 방법은 기본 스레드풀의 스레드 개수를 지정할 수 없습니다.
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
Paths.get("/Users/jungwoon/test.txt"),
StandardOpenOption.READ,
StandardOpenOption.WRITE);
스레드풀의 개수를 지정하기 위해서는 아래와 같이 만들어야 합니다.
// ExecutorService는 스레드풀을 생성
// Runtime.getRuntime().availableProcessors() : CPU 코어 수
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
Paths.get("/Users/jungwoon/test.txt"),
EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE),
executorService);
read()와 write()에는
read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer, A> handler);
write(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer, A> handler);
- dst : 읽고 쓰기 위한 ByteBuffer
- position : 파일을 읽거나 쓰기 위한 위치
- attachment : 콜백 메소드로 전달한 객체
- handler : 작업 완료했을때, 처리할 콜백 함수
- Integer : read()와 write()가 읽거나 쓴 바이트 수
- A : 첨부 객체 타입으로 개발자가 CompletionHandler 구현 객체 작성시 지정 가능하며, 생략도 가능합니다.
아래는 read()에 대한 사용예제로, 주의할점은 작업이 정상적으로 완료되거나, 실패할 경우에만 채널을 닫아야 합니다.
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.util.EnumSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class test {
public static void main(String[] args) throws IOException {
// ExecutorService는 스레드풀을 생성
// Runtime.getRuntime().availableProcessors() : CPU 코어 수
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
for (int i = 0; i < 10; i++) {
// test0.txt, test1.txt ... 파일을 만들기위한 path 객체
Path path = Paths.get("/Users/jungwoon/test" + i + ".txt");
// 상위 디렉토리가 없으면 미리 생성
Files.createDirectories(path.getParent());
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
path,
EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE),
executorService);
// ByteBuffer 생성
ByteBuffer byteBuffer = Charset.defaultCharset().encode("Hello World");
// Callback Method에 제공할 첨부 객체로, 결과값 이외에 추가 정보를 얻고자 할 때 사용
class Attachment {
private Path path;
private AsynchronousFileChannel asynchronousFileChannel;
Path getPath() {
return path;
}
private void setPath(Path path) {
this.path = path;
}
AsynchronousFileChannel getAsynchronousFileChannel() {
return asynchronousFileChannel;
}
private void setAsynchronousFileChannel(AsynchronousFileChannel asynchronousFileChannel) {
this.asynchronousFileChannel = asynchronousFileChannel;
}
}
Attachment attachment = new Attachment();
attachment.setPath(path);
attachment.setAsynchronousFileChannel(fileChannel);
CompletionHandler<Integer, Attachment> completionHandler = new CompletionHandler<Integer, Attachment>() {
// 작업이 정상적으로 완료될었을 경우
@Override
public void completed(Integer result, Attachment attachment) {
System.out.println(
attachment.getPath().getFileName() + " : "
+ result + " bytes written : "
+ Thread.currentThread().getName());
try {
attachment.getAsynchronousFileChannel().close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 작업이 뭔가에 의해서 실패한 경우
@Override
public void failed(Throwable exc, Attachment attachment) {
exc.printStackTrace();
try {
attachment.getAsynchronousFileChannel().close();
} catch (IOException e) {
e.printStackTrace();
}
}
};
fileChannel.write(byteBuffer, 0, attachment, completionHandler);
}
executorService.shutdown();
}
}
결과
test1.txt : 11 bytes written : pool-1-thread-2
test0.txt : 11 bytes written : pool-1-thread-1
test3.txt : 11 bytes written : pool-1-thread-4
test2.txt : 11 bytes written : pool-1-thread-3
test4.txt : 11 bytes written : pool-1-thread-4
test5.txt : 11 bytes written : pool-1-thread-4
test7.txt : 11 bytes written : pool-1-thread-1
test6.txt : 11 bytes written : pool-1-thread-4
test8.txt : 11 bytes written : pool-1-thread-4
test9.txt : 11 bytes written : pool-1-thread-1
아래는 위에서 만든 파일에 대해서 쓰기 예제 입니다.
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.util.EnumSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class test {
public static void main(String[] args) throws IOException {
// ExecutorService는 스레드풀을 생성
// Runtime.getRuntime().availableProcessors() : CPU 코어 수
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
for (int i = 0; i < 10; i++) {
// test0.txt, test1.txt ... 파일을 만들기위한 path 객체
Path path = Paths.get("/Users/jungwoon/test" + i + ".txt");
// 상위 디렉토리가 없으면 미리 생성
Files.createDirectories(path.getParent());
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
path,
EnumSet.of(StandardOpenOption.READ),
executorService);
// ByteBuffer 생성
ByteBuffer byteBuffer = ByteBuffer.allocate((int) fileChannel.size());
// Callback Method에 제공할 첨부 객체로, 결과값 이외에 추가 정보를 얻고자 할 때 사용
class Attachment {
private Path path;
private AsynchronousFileChannel asynchronousFileChannel;
private ByteBuffer byteBuffer;
public ByteBuffer getByteBuffer() {
return byteBuffer;
}
public void setByteBuffer(ByteBuffer byteBuffer) {
this.byteBuffer = byteBuffer;
}
Path getPath() {
return path;
}
private void setPath(Path path) {
this.path = path;
}
AsynchronousFileChannel getAsynchronousFileChannel() {
return asynchronousFileChannel;
}
private void setAsynchronousFileChannel(AsynchronousFileChannel asynchronousFileChannel) {
this.asynchronousFileChannel = asynchronousFileChannel;
}
}
Attachment attachment = new Attachment();
attachment.setPath(path);
attachment.setAsynchronousFileChannel(fileChannel);
attachment.setByteBuffer(byteBuffer);
CompletionHandler<Integer, Attachment> completionHandler = new CompletionHandler<Integer, Attachment>() {
// 작업이 정상적으로 완료될었을 경우
@Override
public void completed(Integer result, Attachment attachment) {
attachment.getByteBuffer().flip();
String data = Charset.defaultCharset().decode(byteBuffer).toString();
System.out.println(
attachment.getPath().getFileName() + " : "
+ data + " : "
+ Thread.currentThread().getName());
try {
attachment.getAsynchronousFileChannel().close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 작업이 뭔가에 의해서 실패한 경우
@Override
public void failed(Throwable exc, Attachment attachment) {
exc.printStackTrace();
try {
attachment.getAsynchronousFileChannel().close();
} catch (IOException e) {
e.printStackTrace();
}
}
};
fileChannel.read(byteBuffer, 0, attachment, completionHandler);
}
executorService.shutdown();
}
}
결과
test0.txt : Hello World : pool-1-thread-1
test3.txt : Hello World : pool-1-thread-4
test2.txt : Hello World : pool-1-thread-3
test1.txt : Hello World : pool-1-thread-2
test4.txt : Hello World : pool-1-thread-3
test5.txt : Hello World : pool-1-thread-3
test6.txt : Hello World : pool-1-thread-3
test8.txt : Hello World : pool-1-thread-3
test7.txt : Hello World : pool-1-thread-4
test9.txt : Hello World : pool-1-thread-3