본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로프로그래밍 › Java › 입출력과 네트워크

비동기 파일채널

목차

AsynchronousFileChannel로 파일 읽고 쓰기

FileChannel.read()·write()는 호출한 스레드가 작업이 끝날 때까지 기다린다. UI나 이벤트 루프에서 큰 파일을 직접 처리하면 다른 일을 진행하기 어렵다. AsynchronousFileChannel은 파일 I/O를 시작한 뒤 Future<Integer> 또는 CompletionHandler로 완료를 받는 API다. 채널 자체는 공유 현재 위치를 가지지 않으며, 매번 읽거나 쓸 파일 위치를 전달한다.

flowchart LR
    A[호출자: 채널에 작업 제출] --> B[채널의 실행 스레드]
    B --> C[파일 읽기 또는 쓰기]
    C --> D[Future 완료 또는 CompletionHandler 호출]
    D --> E[결과 바이트 수 확인·다음 작업]

그림에서 호출자는 제출 직후 다른 작업을 할 수 있다. 하지만 Future.get()을 바로 호출하면 그 스레드는 다시 기다린다. 비동기 API를 선택하는 것과 호출 스레드를 비워 두는 것은 별개의 설계다. 완료 처리도 오래 걸리면 채널의 완료 스레드를 차지할 수 있으므로 무거운 처리는 다른 실행기로 넘긴다.

Future로 끝까지 쓰고 읽기

다음 예제는 흐름을 확인하기 위해 콘솔 main에서 get()으로 기다린다. UI 스레드에서는 같은 방식으로 대기하지 않는다. 파일 내용은 UTF-8로 명시한다.

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.ExecutionException;

public class AsyncFileExample {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("sample.txt");
        ByteBuffer source = StandardCharsets.UTF_8.encode("hello Java\n");

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(
                path, StandardOpenOption.CREATE,
                StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
            long position = 0;
            while (source.hasRemaining()) {
                int written = channel.write(source, position).get();
                if (written <= 0) throw new IllegalStateException("쓰기 진행 없음");
                position += written;
            }
        }

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(
                path, StandardOpenOption.READ)) {
            ByteBuffer target = ByteBuffer.allocate(32);
            int count = channel.read(target, 0).get();
            if (count >= 0) {
                target.flip();
                System.out.print(StandardCharsets.UTF_8.decode(target));
            }
        }
    }
}

write의 반환값은 이번 작업에서 쓴 바이트 수다. 한 번에 버퍼 전체가 기록된다고 가정하면 파일이 잘릴 수 있으므로 남은 바이트를 반복해 쓴다. 읽기의 -1은 파일 끝을 뜻하고, 예제의 읽기 버퍼는 32바이트뿐이라 큰 파일에는 반복 읽기와 위치 증가가 필요하다. ByteBuffer.flip()은 쓰기 모드에서 채운 버퍼를 읽기 모드로 전환한다. ExecutionException이 발생하면 그 원인(cause)을 확인해 실제 I/O 실패를 진단한다.

콜백으로 기다리지 않기

read(dst, position, attachment, handler)와 write(src, position, attachment, handler)는 완료 시 completed(Integer count, A attachment) 또는 failed(Throwable error, A attachment)를 호출한다. 첨부 객체에는 파일 위치, 버퍼, 채널 같은 해당 작업의 상태를 넣는다. completed에서 count가 버퍼 전체 길이보다 작다면 다음 작업을 제출할 수 있다. 실패 경로와 취소 경로에서도 채널을 닫아야 한다. 원래 메모의 읽기 예제는 읽은 바이트를 written으로 표시했고, 버퍼를 파일 크기의 4배로 할당했으며 실패를 비워 둔 catch로 삼켰다. 이런 방식은 큰 파일에서 과도한 메모리를 쓰고 오류 원인을 잃는다.

기본 open(path, options...)은 시스템 기본 스레드 풀과 연결된다. open(path, optionsSet, executor) 오버로드는 실행기를 지정한다. 전용 실행기를 만들었다면 채널을 닫고 실행기도 종료하는 책임이 호출자에게 있다. 파일 I/O 구현이 실제로 운영체제의 커널 비동기 기능을 사용하는지는 플랫폼에 따라 다를 수 있으므로 AsynchronousFileChannel이라는 이름만으로 처리량 향상을 단정하지 않는다. 작업이 적고 순차적인 도구라면 Files.readString이나 FileChannel이 더 단순할 수 있다.

참고: Oracle AsynchronousFileChannel API, ByteBuffer API

같은 카테고리의 글