디렉터리 순회와 파일 속성
파일 시스템을 탐색할 때 한 디렉터리의 바로 아래와 모든 하위 트리를 걷는 작업은 비용과 실패 범위가 다릅니다.
Files.list는 한 단계, Files.walk는 깊이 우선 스트림을 제공합니다.
둘 다 열린 디렉터리 자원을 가질 수 있어 스트림을 닫아야 합니다.
심볼릭 링크와 접근 권한, 순회 중 삭제 같은 변화를 고려해야 합니다.
이 절에서는 필요한 깊이만 탐색하고 속성을 한 번에 읽는 방법을 다룹니다.
트리 순회는 시작할 때 전체 파일 목록을 고정하지 않습니다. 탐색 중 파일이 사라지거나 권한이 바뀔 수 있고, 한 경로의 실패가 전체 Stream 소비를 중단시킬 수 있습니다. 배치 도구라면 즉시 중단할지, 개별 오류를 기록하고 계속할지 먼저 정해야 합니다.
list와 walk의 탐색 범위를 구분한다
바로 아래 항목만 필요하면 list, 재귀 하위 경로가 필요하면 walk를 사용합니다.
깊이 제한으로 불필요한 탐색을 줄일 수 있습니다.
디렉터리 스트림도 닫을 자원이다
반환된 Stream을 try-with-resources에 두고 최종 연산까지 그 범위 안에서 수행합니다.
예제는 재귀 탐색 중 일반 파일만 골라 속성을 읽고, 정리 단계에서는 같은 트리를 자식부터 부모 순서로 삭제합니다. 탐색 목적에 따라 같은 경로 집합의 처리 순서가 달라집니다.
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Comparator;
public class DirectoryWalk {
public static void main(String[] args) throws Exception {
Path root = Files.createTempDirectory("walk-");
Files.createDirectories(root.resolve("a/b"));
Files.writeString(root.resolve("a/one.txt"), "1");
Files.writeString(root.resolve("a/b/two.txt"), "22");
try (var paths = Files.walk(root)) {
paths.filter(Files::isRegularFile).sorted().forEach(path -> {
try {
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
System.out.println(path.getFileName() + ":" + attrs.size());
} catch (java.io.IOException error) {
throw new java.io.UncheckedIOException(error);
}
});
}
try (var paths = Files.walk(root)) {
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) Files.delete(path);
}
}
}각 줄의 숫자는 문자 수가 아니라 파일에 저장된 바이트 크기입니다. 출력 뒤 삭제 순서를 역전하지 않으면 내용이 남은 디렉터리를 먼저 지우려다 DirectoryNotEmptyException을 만나게 됩니다.
two.txt:2
one.txt:1walk 스트림을 닫는 범위 안에서 속성을 읽고 삭제할 때는 자식부터 지우도록 역순으로 정렬합니다.
BasicFileAttributes로 관련 속성을 함께 읽는다
크기, 수정 시각, 파일 종류를 여러 Files 호출로 나누기보다 한 번의 속성 읽기로 같은 관찰 시점을 얻습니다.
아래 세 조각은 한 단계 탐색, 깊이 제한 재귀 탐색, Stream 수명이라는 서로 다른 선택을 나눕니다.
try (var direct = Files.list(Path.of("data"))) {
direct.forEach(System.out::println); // 바로 아래 한 단계
}try (var recursive = Files.walk(Path.of("data"), 2)) {
recursive.forEach(System.out::println); // 최대 깊이 2
}// 반환 Stream을 try 밖으로 넘기면 이미 닫혔거나 디렉터리 자원이 남을 수 있다.Files.walk는 기본적으로 심볼릭 링크가 가리키는 디렉터리를 따라가지 않습니다. FOLLOW_LINKS를 켜면 실제 디렉터리 트리가 아닌 링크 그래프를 걷게 되므로 순환과 작업 영역 이탈을 고려해야 합니다. 삭제나 권한 변경 도구에서는 기본 동작을 유지하는 편이 안전한 경우가 많습니다.
연습으로 Files.walk(root, 1)과 Files.list(root)의 결과를 각각 출력해 보세요. 시작 경로 포함 여부와 탐색 깊이 차이를 확인한 뒤, 실제 요구가 “바로 아래 파일”뿐인데 재귀 walk를 썼을 때 비용과 실패 범위가 어떻게 넓어지는지 설명해 보세요.
디렉터리 탐색은 경로 수가 커질 수 있는 I/O이므로 범위, 자원 수명, 개별 실패 정책을 먼저 제한해야 합니다.