Quick Flow
Path path = Path.of("data", "users.txt");
if (Files.exists(path)) {
List<String> lines = Files.readAllLines(path);
}Path는 경로 조합과 정규화를, Files는 실제 I/O를 담당합니다. exists() 뒤에 다시 실행하는 방식은 TOCTOU 경쟁을 막지 못하므로 실제 read/write 결과를 처리하고, 외부 입력으로 만든 경로는 normalize() 뒤 base directory 안에 남는지 확인합니다.
기본 흐름
어떤 파일 시스템 API를 먼저 떠올리면 되나
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 경로를 값 객체처럼 다루기 | Path |
| 파일 읽기/쓰기/이동 | Files |
| 디렉터리 생성 | Files.createDirectories(...) |
| 디렉터리 순회 | Files.list(...) 또는 DirectoryStream |
| 문자열 경로를 안전하게 조합 | Path.of(...) / resolve(...) |
Path: 경로를 값 객체로 다루기
예전 java.io.File과 달리, java.nio.file.Path는 경로를 값처럼 다루는 객체입니다. OS별 구분자를 신경 쓰지 않아도 되고, resolve로 경로를 조합하고, getParent, getFileName, normalize로 경로 구성요소를 다룰 수 있습니다.
Path base = Path.of("/app/data");
Path file = base.resolve("users.txt"); // /app/data/users.txt
Path relative = base.relativize(file); // users.txt
Path parent = file.getParent(); // /app/data
Path name = file.getFileName(); // users.txt
// 경로 조합 — 문자열 덧셈 없이
Path config = Path.of("config", "app", "settings.json");Files: 경로에 대해 실제 작업 수행
Files는 경로를 받아 실제 파일 I/O 작업을 수행하는 유틸리티 클래스입니다. 읽기, 쓰기, 복사, 이동, 삭제, 디렉터리 생성, 존재 확인 등을 제공합니다. Path가 "어디"를 표현하고, Files가 "무엇을 할지"를 실행하는 구조입니다.
Path path = Path.of("output", "report.txt");
// 파일 읽기
String content = Files.readString(path);
List<String> lines = Files.readAllLines(path);
// 파일 쓰기
Files.writeString(path, "결과 데이터\n", StandardOpenOption.APPEND);
// 복사 / 이동
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.move(oldPath, newPath);
// 파일 삭제
Files.deleteIfExists(path);디렉터리 생성 및 탐색
디렉터리를 만들거나 디렉터리 안의 파일을 탐색할 때도 Files를 씁니다. createDirectories는 중간 경로까지 한 번에 생성해 줍니다.
// 디렉터리 생성 (중간 경로 포함)
Files.createDirectories(Path.of("output", "2026", "march"));
// 디렉터리 내 파일 목록
try (Stream<Path> entries = Files.list(Path.of("logs"))) {
entries.filter(p -> p.toString().endsWith(".log"))
.forEach(System.out::println);
}
// 재귀 탐색
try (Stream<Path> all = Files.walk(Path.of("src"))) {
all.filter(Files::isRegularFile)
.forEach(System.out::println);
}Path vs Files
Path는 경로 자체를 표현하고, Files는 그 경로에 대해 실제 작업을 수행합니다. 문자열을 계속 넘겨 다니기보다 이 역할을 분리해서 보면 코드가 훨씬 덜 꼬입니다.
Path report = Path.of("output").resolve("report.txt");
// 경로 계산은 Path
Path parent = report.getParent();
// 실제 읽기/쓰기/존재 확인은 Files
if (Files.notExists(parent)) {
Files.createDirectories(parent);
}
Files.writeString(report, "done");Files.exists(path)를 확인한 뒤 별도 read/write를 하는 것은 확인과 사용 사이에 파일이 바뀔 수 있는 TOCTOU 패턴입니다. 필요한 operation을 직접 실행하고 IOException을 처리합니다. 사용자 입력을 root 아래로 제한할 때는 resolve(...).normalize() 뒤에 startsWith(root)를 확인하고, symlink를 허용할지까지 정책으로 정합니다.
readString과 readAllLines는 작은 설정 파일에는 편하지만 파일 전체를 메모리에 올립니다. 크기를 모르는 로그나 업로드는 newBufferedReader 또는 stream API를 try-with-resources로 닫으며 처리합니다.
선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 경로 생성 및 조합 | Path.of(...), path.resolve(...) |
| 파일 읽기/쓰기 | Files.readString, Files.writeString |
| 파일 존재 확인 | Files.exists(path) |
| 디렉터리 생성 | Files.createDirectories(path) |
| 디렉터리 탐색 | Files.list(dir), Files.walk(dir) |
주의할 점
경로를 문자열 덧셈으로 직접 만들면 OS별 구분자 문제와 상대 경로 오류가 발생합니다. 경로는 항상 Path로 조합하세요.
// ❌ 문자열 덧셈으로 경로 조합 — OS마다 구분자(\, /) 다름
String filePath = baseDir + "/" + subDir + "/" + fileName;
File file = new File(filePath); // Windows에서 오류 가능
// ✅ Path.resolve()로 OS 독립적인 경로 조합
Path filePath = Path.of(baseDir)
.resolve(subDir)
.resolve(fileName);Files.list(...)와 Files.walk(...)가 반환하는 Stream은 닫아야 합니다. try-with-resources 없이 열어 두면 파일 핸들이 남을 수 있습니다.
// ❌ 닫지 않음
Stream<Path> paths = Files.list(Path.of("logs"));
// ✅ try-with-resources
try (Stream<Path> paths = Files.list(Path.of("logs"))) {
paths.forEach(System.out::println);
}참고 링크
3 sources