Java NIO에서는 비동기적으로 파일 입출력을 실행할 수 있는 AsynchronousFileChannel을 제공한다. FileChannel은 동기적이다. read()/write() 요청을 하면 어찌되었던 메소드에서 리턴을 하고 다음 동작들이 실행된다. 이 동작을 비동기적으로 실행할 수 있도록 해주는 것이 AsynchronousFileChannel이다.
Path path = Paths.get("/tmp/test.txt");
AsynchronousFileChannel channel =
AsynchronousFileChannel.open(path, StandardOpenOption.READ);
Path 객체로 파일 경로를 만들고 정적메소드 AsynchronousFileChannel.open()을 호출해서 채널을 열어준다.
AsynchronousFileChannel을 이용해서 비동기적으로 파일을 읽는데에는 두가지 방법이 있다.
- Future 객체를 이용
- 콜백을 이용
비동기 읽기 - Future 객체
AsynchronousFileChannel에서 데이터를 읽기 위해 read() 메소드를 호출하면 Future 객체가 반환된다.
Future<Integer> operation = asyncChannel.read(buffer, 0);
read() 메소드의 첫 번째 파라미터는 데이터를 읽어서 저장할 버퍼이고, 두 번째 인자는 읽기 연산을 시작할 파일에서의 오프셋이다.
read() 메소드는 즉시 리턴한다. 읽기 연산은 이후 언젠가 실행된다. 대신 반환되는 Future 객체의 isDone() 메소드를 이용해서 읽기 연산이 끝났는지 여부를 확인할 수 있다. 비동기적으로 말이다.
Path path = Paths.get("/tmp/dir1");
AsynchronousFileChannel channel =
AsynchronousFileChannel.open(path, StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
Future<Integer> operation = channel.read(buffer, position);
while(!operation.isDone());
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println(new String(data));
buffer.clear();
이렇게 BusyWaiting으로 기다려도되고, 다른 작업을 처리하다가 중간중간 확인해서 필요한 로직을 실행해도 된다.
비동기 읽기 - Callback
주기적으로 확인하는 대신 콜백 객체를 만들어서 넘겨주는 방식을 사용해도 된다. 콜백은 CompletionHandler 인터페이스를 구현해서 만들 수 있다.
channel.read(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("result = " + result);
attachment.flip();
byte[] data = new byte[attachment.limit()];
attachment.get(data);
System.out.println(new String(data));
attachment.clear();
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
}
});
콜백을 등록해 놓으면 read() 동작이 끝났을 때 completed() 메소드에 구현한 내용이 실행된다.
completed() 메소드의 인자를 살펴보자. 첫 번째 인자는 얼마나 많은 데이터가 읽혔는지를 의미한다. 두 번째 인자는 read() 메소드의 세 번째 인자로 받은 객체가 넘겨진다. 위 코드에서는 read() 메소드에 넘겨준 ByteBuffer를 이용해서 읽은 데이터를 출력했다.
read() 메소드가 실패했으면 failed() 메소드가 호출된다.
비동기 쓰기 - Future 객체
AsynchronousFileChannel을 이용해서 비동기적으로 쓰기 연산을 수행할 수도 있다.
Path path = Paths.get("/tmp/dir1");
AsynchronousFileChannel channel =
AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Write some data");
buffer.flip();
long position = 0;
Future<Integer> operation = channel.write(buffer, position);
buffer.clear();
while(!operation.isDone());
buffer.flip();
우선 쓰기 모드로 채널을 열어야한다. StandardOpenOption.WRITE를 open() 메소드의 두 번째 파라미터로 넘겨준다. 이후 바이트버퍼를 만들고 데이터를 버퍼에 쓴다. 그리고 write() 메소드를 호출하면 즉시 Future 객체를 반환한다. 읽기와 마찬가지로 isDone() 메소드로 쓰기 연산이 종료되었는지 확인할 수 있다.
주의해야할 점은 이미 존재하는 파일에만 write() 연산을 할 수 있다는 점이다. 존재하지 않은 파일에 write() 메소드를 실행하면 NoSuchFileException이 발생한다.
if (!Files.exists(path)) {
Files.createFile(path);
}
이런식으로 파일이 없으면 만들어주고 시작할 수도 있다.
비동기 쓰기 - Callback
읽기와 마찬가지로 주기적으로 확인하는 대신 콜백 객체를 만들어서 넘겨주는 방식을 사용해도 된다. CompletionHandler 인터페이스를 구현해서 만들 수 있다.
channel.read(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("bytes written: " + result);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("Failed to write the data");
}
});
댓글