본문 바로가기
카테고리 없음

Java NIO - 비동기 파일 채널 (AsynchronousFileChannel)

by A6K 2021. 1. 4.

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");
    }
});

 

댓글