CodeGym /행동 /JAVA 25 SELF /NIO2: Files, Paths, Files.walk: 파일 시스템 순회

NIO2: Files, Paths, Files.walk: 파일 시스템 순회

JAVA 25 SELF
레벨 39 , 레슨 0
사용 가능

1. NIO2: 자세히 살펴보기

이미 NIO2를 접해 보았지만, 이제 이 파일/디렉터리 작업용 유용한 라이브러리를 복습하고 더 깊이 이해해 봅시다.

예전에는 Java에 File 클래스만 있었습니다. 파일 존재 여부를 확인하고, 파일과 폴더를 생성/삭제하고, 디렉터리의 파일 목록을 가져올 수 있었죠. 하지만 제약이 많았습니다:

  • 경로 처리가 불편했습니다. 특히 서로 다른 운영 체제(C:\Users\user\file.txt의 Windows와 /home/user/file.txt의 Linux)를 고려해야 할 때가 문제였습니다);
  • 심볼릭 링크, 접근 권한, 파일 속성에 대한 제대로 된 지원이 없었습니다;
  • 디렉터리 트리 순회 기능이 제한적이었습니다;
  • 오류 처리도 만족스럽지 않았습니다.

Java 7에서 NIO2(New Input/Output, 버전 2)가 도입되면서 개발자의 삶이 훨씬 수월해졌습니다. 이제 다음을 사용할 수 있습니다:

  • 파일과 폴더 경로를 편리하게 다루는 Path 클래스;
  • 파일 읽기/쓰기, 복사, 삭제, 파일 정보 조회 등 핵심 작업을 제공하는 Files 클래스;
  • FileVisitor 인터페이스와 Files.walk 같은 메서드로 파일 시스템을 쉽고 유연하게 순회.

왜 중요한가요?

  • 크로스플랫폼: 동일한 코드가 Windows, Linux, macOS에서 경로 구분자(/ 또는 \)를 신경 쓰지 않고 동작합니다.
  • 안전성과 편의성: 오류 정보가 더 풍부하고, 불명확한 ‘마법’과 예기치 않은 놀라움이 줄어듭니다.
  • 강력함: 거대한 디렉터리도 처리할 수 있고, 필터링과 병렬 처리까지 곁들인 재귀적 순회가 가능합니다.

2. 핵심 클래스: Path와 Files

Path 클래스

Path는 파일이나 폴더 경로를 현대적으로 표현하는 타입입니다. 실제로 존재하는 파일을 가리킬 필요는 없으며, 단지 다루기 편한 경로 표현일 뿐입니다.

Path 생성

import java.nio.file.Path;
import java.nio.file.Paths;

Path path1 = Paths.get("file.txt"); // 상대 경로
Path path2 = Paths.get("/home/user/file.txt"); // 절대 경로
Path path3 = Path.of("mydir", "subdir", "file.txt"); // Java 11+

사실: Path는 운영 체제에 의존하지 않습니다. / 또는 \로 문자열을 직접 이어 붙이는 일을 잊으세요!

문자열로 변환

System.out.println(path1.toString());

부모 디렉터리와 파일 이름 가져오기

Path parent = path1.getParent(); // 상대 경로에서는 null일 수 있음
Path fileName = path1.getFileName(); // 파일 이름만

Files 클래스

Files는 파일과 디렉터리 작업을 위한 정적 메서드 모음입니다:

  • 존재 여부 확인: Files.exists(path)
  • 파일 읽기/쓰기: Files.readAllBytes(path), Files.write(path, bytes)
  • 정보 조회: Files.size(path), Files.getLastModifiedTime(path)
  • 복사, 삭제, 이동: Files.copy, Files.delete, Files.move

예시:

import java.nio.file.Files;
import java.nio.file.Path;

Path path = Path.of("file.txt");
if (Files.exists(path)) {
    System.out.println("파일이 존재합니다!");
    System.out.println("크기: " + Files.size(path) + " 바이트");
    System.out.println("마지막 수정: " + Files.getLastModifiedTime(path));
} else {
    System.out.println("파일을 찾을 수 없습니다.");
}

3. 파일 시스템 순회: Files.walk와 그 밖의 도구

예전 방식의 문제

예전 API에서는 폴더와 하위 폴더의 모든 파일을 순회하려면 재귀 함수를 작성하고 파일과 폴더를 일일이 구분하며, 무한 재귀에 빠지지 않도록 주의해야 했습니다. 번거로울 뿐 아니라 실수하기 쉬웠습니다.

현대적인 방법: Files.walk

Files.walk(Path start)는 지정한 경로부터 시작해 모든 하위 디렉터리를 포함한 파일과 폴더의 Stream<Path>를 반환합니다. 이제 파일 시스템 순회는 스트림을 다루는 일일 뿐입니다!

예: 모든 파일과 폴더 출력

import java.nio.file.*;

try (var paths = Files.walk(Path.of("mydir"))) {
    paths.forEach(System.out::println);
}

여기서는 mydir부터 시작해 파일과 폴더를 포함한 모든 경로가 출력됩니다.

예: 파일만(폴더 제외)

try (var paths = Files.walk(Path.of("mydir"))) {
    paths.filter(Files::isRegularFile)
         .forEach(System.out::println);
}

메서드 Files.isRegularFile(path)은 일반 파일에 대해서만 true를 반환합니다(폴더나 심볼릭 링크 제외).

예: 확장자로 파일 찾기

예를 들어, 디렉터리와 하위 디렉터리에서 모든 .txt 파일을 찾아야 한다고 합시다:

try (var paths = Files.walk(Path.of("mydir"))) {
    paths.filter(Files::isRegularFile)
         .filter(path -> path.toString().endsWith(".txt"))
         .forEach(System.out::println);
}

예: 모든 파일의 총 크기 합산

long totalSize = 0;
try (var paths = Files.walk(Path.of("mydir"))) {
    totalSize = paths.filter(Files::isRegularFile)
                     .mapToLong(path -> {
                         try {
                             return Files.size(path);
                         } catch (Exception e) {
                             System.err.println("크기 읽기 오류: " + path);
                             return 0L;
                         }
                     })
                     .sum();
}
System.out.println("파일의 총 크기: " + totalSize + " 바이트");

중요!

  • Files.walk반드시 닫아야 하는 스트림을 반환합니다(AutoCloseable을 구현). 따라서 try-with-resources를 사용하세요!
  • 기본적으로 순회 깊이는 제한이 없습니다(모든 하위 디렉터리). 깊이를 제한하려면 Files.walk(path, maxDepth)를 사용할 수 있습니다.

4. 실전 과제

과제 1: 디렉터리에서 모든 이미지 찾기

.jpg, .png, .gif 확장자의 파일을 images 폴더에서 모두 찾아 이름을 출력하세요.

import java.nio.file.*;
import java.util.Set;

Set<String> extensions = Set.of(".jpg", ".png", ".gif");

try (var paths = Files.walk(Path.of("images"))) {
    paths.filter(Files::isRegularFile)
         .filter(path -> {
             String name = path.getFileName().toString().toLowerCase();
             return extensions.stream().anyMatch(name::endsWith);
         })
         .forEach(System.out::println);
}

과제 2: 모든 .txt 파일을 다른 폴더로 복사

import java.nio.file.*;

Path sourceDir = Path.of("src");
Path destDir = Path.of("dest");

try (var paths = Files.walk(sourceDir)) {
    paths.filter(Files::isRegularFile)
         .filter(path -> path.toString().endsWith(".txt"))
         .forEach(path -> {
             try {
                 Path relative = sourceDir.relativize(path);
                 Path target = destDir.resolve(relative);
                 Files.createDirectories(target.getParent());
                 Files.copy(path, target, StandardCopyOption.REPLACE_EXISTING);
                 System.out.println("복사됨: " + path + " -> " + target);
             } catch (Exception e) {
                 System.err.println("복사 오류: " + path);
             }
         });
}

여기서는 하위 디렉터리 구조를 그대로 유지합니다.

5. 유용한 팁

NIO2의 장점

크로스플랫폼

Path가 폴더 구분자를 알아서 처리합니다. 코드는 Windows, Linux, macOS에서 동일하게 동작합니다.

스트림 기반 처리

Files.walk 같은 메서드는 스트림(Stream<Path>)을 반환하며, 이를 필터링하고 변환하고 컬렉션으로 수집하는 등 Stream API가 제공하는 모든 작업을 활용할 수 있습니다.

대용량 디렉터리 처리

예전 API는 파일이 너무 많으면(예: 100 000장의 사진) ‘죽을’ 수도 있었습니다. NIO2는 모든 것을 한꺼번에 메모리에 올리지 않기 때문에 이런 경우도 수월하게 처리합니다.

심볼릭 링크, 속성, 권한 지원

경로가 심볼릭 링크인지 확인하고(Files.isSymbolicLink(path)), 접근 권한을 가져오고(Files.getPosixFilePermissions(path)), 파일 소유자를 조회하는 등 다양한 기능을 제공합니다.

이전 API와 새 API 비교

작업 이전 API (File) 새 API (Path, Files)
존재 여부 확인
file.exists()
Files.exists(path)
크기 가져오기
file.length()
Files.size(path)
폴더의 파일 목록
file.listFiles()
Files.list(path)
재귀 순회 수동 재귀
Files.walk(path)
파일 복사 file.renameTo() (문제 많음)
Files.copy(src, dest)
확장자 얻기 문자열 파싱
path.getFileName().toString()
부모 얻기
file.getParentFile()
path.getParent()
권한 처리 거의 불가
Files.getPosixFilePermissions(path)

중요한 특징

파일 유형 확인

  • Files.isRegularFile(path) — 일반 파일
  • Files.isDirectory(path) — 폴더
  • Files.isSymbolicLink(path) — 심볼릭 링크

대용량 디렉터리 처리

  • 모든 경로를 리스트에 모으지 마세요. Stream<Path>로 처리하면서 순차적으로 소비하세요.
  • 처리가 끝나면 스트림을 반드시 닫으세요(try-with-resources).

예외

  • 거의 모든 메서드가 IOException을 던질 수 있으니, 예외를 처리하거나 상위로 전달하는 것을 잊지 마세요.

순회 깊이 제한

try (var paths = Files.walk(Path.of("mydir"), 2)) { // 최대 2 레벨
    // ...
}

6. NIO2 사용 시 흔한 실수

오류 №1: walk 스트림을 닫지 않음. try-with-resources를 사용하지 않으면 자원 누수가 발생할 수 있습니다 — 파일 시스템 스트림이 열린 채로 남습니다. 항상 try (var paths = Files.walk(...)) { ... } 구문을 사용하세요.

오류 №2: 경로가 디렉터리인지 확인하지 않음. Files.walk에 폴더가 아닌 파일 경로를 전달하면 예상치 못한 동작이나 오류가 발생할 수 있습니다.

오류 №3: 예외를 처리하지 않음. NIO2의 거의 모든 메서드는 IOException을 던질 수 있습니다. 이런 오류를 무시하지 말고 — 최소한 사용자에게 메시지를 출력하거나 로그를 남기세요.

오류 №4: 경로 구분자를 혼용함. / 또는 \로 경로를 수동으로 이어 붙이고 있다면 이제 그만! Path.of(...) 또는 resolve(...)를 사용하면 플랫폼에 맞게 올바르게 처리됩니다.

오류 №5: 거대한 디렉터리를 한꺼번에 ‘메모리로’ 읽으려 함. 파일이 매우 많다면 모든 경로를 리스트로 모으지 말고 — Stream<Path>로 흘려보내며 처리하세요.

오류 №6: 크로스플랫폼을 잊음. Windows 또는 Unix 스타일의 절대 경로를 하드코딩하지 마세요. Pathresolve/relativize 같은 연산을 사용하면 어떤 OS에서도 올바르게 동작합니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION