파일을 읽고 쓰는 건 기본이지만, 수 GB 파일을 효율적으로 처리하거나, 파일이 변경되면 자동으로 감지하는 건 어떻게 할까요?

try-with-resources 제대로 쓰기

기본 패턴

JAVA
// 리소스가 자동으로 닫힘 (AutoCloseable 구현 필수)
try (var reader = new BufferedReader(new FileReader("data.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        process(line);
    }
}  // reader.close() 자동 호출

다중 리소스

JAVA
try (var input = new FileInputStream("source.bin");
     var output = new FileOutputStream("target.bin")) {
    input.transferTo(output); // Java 9+: 한 줄로 복사
}
// output.close() → input.close() (선언 역순으로 닫힘)

Suppressed Exception

JAVA
// close()에서도 예외가 발생하면 suppressed로 기록
try (var resource = new MyResource()) {
    throw new RuntimeException("본래 예외");
} // close()에서 예외 발생 → suppressed exception으로 추가
// catch에서 getSuppressed()로 확인 가능

Path와 Files API (NIO.2)

java.io.File 대신 java.nio.file.PathFiles를 사용하세요.

JAVA
Path path = Path.of("data", "users.txt"); // 경로 조합

// 파일 읽기
String content = Files.readString(path);                    // 전체 읽기
List<String> lines = Files.readAllLines(path);              // 줄 단위
Stream<String> stream = Files.lines(path);                  // 스트림 (대용량)

// 파일 쓰기
Files.writeString(path, "hello", StandardOpenOption.APPEND);
Files.write(path, lines);

// 파일 정보
long size = Files.size(path);
boolean exists = Files.exists(path);
FileTime modified = Files.getLastModifiedTime(path);

디렉토리 순회

JAVA
// 1단계 깊이
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.txt")) {
    for (Path entry : stream) {
        System.out.println(entry.getFileName());
    }
}

// 재귀 순회
try (Stream<Path> paths = Files.walk(dir)) {
    paths.filter(Files::isRegularFile)
         .filter(p -> p.toString().endsWith(".java"))
         .forEach(System.out::println);
}

// find: 조건으로 검색
try (Stream<Path> found = Files.find(dir, 10,  // 최대 깊이 10
        (path, attrs) -> attrs.size() > 1_000_000)) { // 1MB 이상
    found.forEach(System.out::println);
}

WatchService — 파일 변경 감지

디렉토리의 파일 생성, 수정, 삭제를 실시간으로 감지합니다.

JAVA
public class FileWatcher {

    public void watch(Path dir) throws Exception {
        WatchService watcher = FileSystems.getDefault().newWatchService();

        dir.register(watcher,
            StandardWatchEventKinds.ENTRY_CREATE,  // 파일 생성
            StandardWatchEventKinds.ENTRY_MODIFY,  // 파일 수정
            StandardWatchEventKinds.ENTRY_DELETE); // 파일 삭제

        while (true) {
            WatchKey key = watcher.take(); // 이벤트 대기 (블로킹)

            for (WatchEvent<?> event : key.pollEvents()) {
                WatchEvent.Kind<?> kind = event.kind();
                Path fileName = (Path) event.context();

                log.info("{}: {}", kind.name(), fileName);
                // ENTRY_CREATE: config.yml
                // ENTRY_MODIFY: config.yml
            }

            boolean valid = key.reset(); // 다음 이벤트를 받기 위해 리셋
            if (!valid) break; // 디렉토리가 삭제되면 종료
        }
    }
}

활용 예시: 설정 파일 변경 시 자동 리로드, 로그 디렉토리 모니터링.

메모리 매핑 (Memory-Mapped File)

수 GB 파일을 메모리에 전부 올리지 않고, 가상 메모리에 매핑하여 처리합니다.

JAVA
public long countLines(Path file) throws IOException {
    try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {
        // 파일을 가상 메모리에 매핑
        MappedByteBuffer buffer = channel.map(
            FileChannel.MapMode.READ_ONLY, 0, channel.size());

        long count = 0;
        while (buffer.hasRemaining()) {
            if (buffer.get() == '\n') count++;
        }
        return count;
    }
}

메모리 매핑 vs 일반 I/O

항목일반 I/O메모리 매핑
방식read()로 커널 → 사용자 메모리 복사가상 메모리에 직접 매핑
버퍼사용자 메모리에 할당OS가 페이지 캐시로 관리
대용량청크 단위로 읽어야 함파일 전체를 배열처럼 접근
적합순차 읽기, 작은 파일랜덤 접근, 대용량 파일

비동기 파일 I/O (AsynchronousFileChannel)

JAVA
AsynchronousFileChannel channel =
    AsynchronousFileChannel.open(path, StandardOpenOption.READ);

ByteBuffer buffer = ByteBuffer.allocate(1024);

// 비동기 읽기 — 콜백 방식
channel.read(buffer, 0, null, new CompletionHandler<>() {
    @Override
    public void completed(Integer bytesRead, Object attachment) {
        buffer.flip();
        System.out.println("읽은 바이트: " + bytesRead);
    }

    @Override
    public void failed(Throwable exc, Object attachment) {
        log.error("읽기 실패", exc);
    }
});

임시 파일과 디렉토리

JAVA
// 임시 파일 생성
Path tempFile = Files.createTempFile("prefix-", ".tmp");
// /tmp/prefix-1234567890.tmp

// 임시 디렉토리 생성
Path tempDir = Files.createTempDirectory("upload-");

// JVM 종료 시 자동 삭제
tempFile.toFile().deleteOnExit();

자주 헷갈리는 포인트

  • Files.lines()는 닫아야 한다: Files.lines()는 내부적으로 스트림을 열므로 try-with-resources로 감싸야 합니다. Files.readAllLines()는 즉시 닫히므로 괜찮습니다.
  • MappedByteBuffer 해제: 메모리 매핑된 버퍼는 GC에 의존하므로 즉시 해제가 어렵습니다. Java 19+의 MemorySegment API가 더 안전합니다.
  • WatchService 중복 이벤트: 파일을 저장하면 MODIFY 이벤트가 여러 번 발생할 수 있습니다. 디바운싱 로직을 추가하세요.
  • 인코딩: Files.readString()의 기본 인코딩은 UTF-8입니다. 다른 인코딩은 Charset을 명시하세요.

정리

항목설명
try-with-resourcesAutoCloseable 리소스 자동 닫기
Path + FilesNIO.2 파일 API (java.io.File 대체)
WatchService파일 변경 실시간 감지
MappedByteBuffer대용량 파일 메모리 매핑
AsynchronousFileChannel비동기 파일 I/O

References

댓글 로딩 중...