1. 디렉터리에서 파일 순회
파일 목록 얻기: Files.list와 Files.walk
Java에서 폴더 내용물을 다룰 때 매우 유용한 메서드가 두 가지 있습니다:
- Files.list(Path) — 지정한 디렉터리의 최상위 파일과 폴더에 대한 스트림(Stream<Path>)을 반환합니다.
- Files.walk(Path) — 디렉터리와 그 모든 하위 폴더(재귀)의 모든 파일과 폴더에 대한 스트림을 반환합니다.
예시: 디렉터리 data의 모든 파일과 폴더 출력
import java.nio.file.*;
import java.io.IOException;
public class ListFilesExample {
public static void main(String[] args) throws IOException {
Path dir = Paths.get("data");
// 최상위의 모든 파일과 폴더 스트림 가져오기
try (var stream = Files.list(dir)) {
stream.forEach(System.out::println);
}
}
}
모든 하위 폴더까지(재귀적으로) 순회하려면 Files.walk를 사용하세요:
try (var stream = Files.walk(dir)) {
stream.forEach(System.out::println);
}
주의:
Files.walk는 구조가 크면 매우 많은 파일을 반환할 수 있습니다 — 디스크 루트에서 실행하지 마세요. “우주의 모든 목록”을 보게 될 수도 있어요!
시각화: list와 walk의 차이
| 메서드 | 반환 내용 |
|---|---|
|
현재 폴더의 내용만 |
|
하위를 포함한 모든 파일과 폴더 |
2. 파일 필터링
Stream<Path>로 작업할 때의 큰 장점 중 하나는 확장자, 이름, 크기, 생성일 등 어떤 기준으로든 파일을 필터링할 수 있다는 점입니다.
확장자로 필터링
.txt 파일만 필요하다고 가정해 봅시다. 다음처럼 필터링할 수 있습니다:
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".txt"))
.forEach(System.out::println);
}
이름으로 필터링
이름이 "report"로 시작하는 모든 파일:
stream
.filter(path -> path.getFileName().toString().startsWith("report"))
.forEach(System.out::println);
크기로 필터링
“무거운” 파일만 가져오기(1 MB 초과):
stream
.filter(path -> {
try {
return Files.size(path) > 1_000_000;
} catch (IOException e) {
return false;
}
})
.forEach(System.out::println);
날짜로 필터링
예를 들어 최근 7일 내에 수정된 파일:
import java.time.*;
import java.nio.file.attribute.*;
stream
.filter(path -> {
try {
FileTime lastModified = Files.getLastModifiedTime(path);
return lastModified.toInstant().isAfter(Instant.now().minus(Duration.ofDays(7)));
} catch (IOException e) {
return false;
}
})
.forEach(System.out::println);
모두 합치기: 복합 필터링
레스토랑에서 메뉴 고르듯 필터를 결합할 수 있습니다. 예를 들어 한 달보다 오래되고 10 KB 초과인 .log 파일 전체:
stream
.filter(path -> path.toString().endsWith(".log"))
.filter(path -> {
try {
return Files.size(path) > 10_000;
} catch (IOException e) {
return false;
}
})
.filter(path -> {
try {
FileTime lastModified = Files.getLastModifiedTime(path);
return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(30)));
} catch (IOException e) {
return false;
}
})
.forEach(System.out::println);
3. 파일 대량 복사 및 삭제
대량 작업은 간단합니다: 파일 스트림을 얻은 뒤 각 요소에 필요한 함수를 적용하면 됩니다. 중요한 점은 잠겨 있거나 이미 존재하는 파일 등 잠재적인 오류를 잊지 않는 것입니다.
한 폴더의 모든 파일을 다른 폴더로 복사
import java.nio.file.StandardCopyOption;
Path sourceDir = Paths.get("data");
Path targetDir = Paths.get("backup");
try (var stream = Files.list(sourceDir)) {
stream.forEach(path -> {
Path targetPath = targetDir.resolve(path.getFileName());
try {
Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
System.out.println("복사됨: " + path.getFileName());
} catch (IOException e) {
System.out.println("복사 오류 " + path.getFileName() + ": " + e.getMessage());
}
});
}
모든 임시 파일(*.tmp) 삭제
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".tmp"))
.forEach(path -> {
try {
Files.delete(path);
System.out.println("삭제됨: " + path.getFileName());
} catch (IOException e) {
System.out.println("삭제 오류 " + path.getFileName() + ": " + e.getMessage());
}
});
}
경로 컬렉션 처리를 위한 Stream API 사용
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".txt"))
.sorted((p1, p2) -> {
try {
return Long.compare(Files.size(p2), Files.size(p1)); // 크기 기준, 내림차순
} catch (IOException e) {
return 0;
}
})
.limit(5) // 가장 큰 5개만
.forEach(System.out::println);
}
4. 실전 과제
특정 날짜보다 오래된 파일 찾기 및 삭제
1년 이상 수정되지 않은 모든 파일을 삭제합니다:
try (var stream = Files.list(dir)) {
stream
.filter(path -> {
try {
FileTime lastModified = Files.getLastModifiedTime(path);
return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(365)));
} catch (IOException e) {
return false;
}
})
.forEach(path -> {
try {
Files.delete(path);
System.out.println("삭제됨: " + path.getFileName());
} catch (IOException e) {
System.out.println("삭제 오류 " + path.getFileName() + ": " + e.getMessage());
}
});
}
패턴에 따라 파일 이름 일괄 변경
예를 들어 각 .txt 파일에 접두사 "old_"를 추가해야 하는 경우:
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".txt"))
.forEach(path -> {
Path newPath = path.resolveSibling("old_" + path.getFileName());
try {
Files.move(path, newPath);
System.out.println("이름 변경: " + path.getFileName() + " -> " + newPath.getFileName());
} catch (IOException e) {
System.out.println("이름 변경 오류 " + path.getFileName() + ": " + e.getMessage());
}
});
}
모든 하위 폴더의 모든 파일 복사(재귀)
Files.walk를 사용해 모든 하위 폴더의 파일을 순회합니다:
try (var stream = Files.walk(sourceDir)) {
stream
.filter(Files::isRegularFile)
.forEach(path -> {
Path relative = sourceDir.relativize(path);
Path targetPath = targetDir.resolve(relative);
try {
Files.createDirectories(targetPath.getParent());
Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
System.out.println("복사됨: " + path + " -> " + targetPath);
} catch (IOException e) {
System.out.println("복사 오류 " + path + ": " + e.getMessage());
}
});
}
5. 중요한 포인트와 유의사항
대량 작업에서의 오류 처리
대량 작업에서는 접근 불가, 파일 사용 중, 경로 과도하게 김, 이미 존재 등 “문제” 파일이 거의 항상 등장합니다. 루프 내부에서 예외를 처리하지 않으면 프로그램이 첫 실패에서 바로 종료될 수 있습니다. 따라서 항상 forEach 내부에서 try-catch를 사용해 한 파일의 오류는 “삼키고” 나머지는 계속 처리하세요.
재귀가 성능과 호출 스택에 미치는 영향
큰 디렉터리에 대해 Files.walk를 사용할 때, 재귀를 직접 구현하면 매우 긴 순회가 될 수 있습니다. 다행히 이 메서드는 효율적으로 구현되어 스택 오버플로를 일으키지 않습니다. 하지만 디렉터리 삭제/복사를 위한 자신의 재귀 함수를 작성한다면 주의하세요. 매우 거대한 중첩 구조에서는 스택 오버플로가 발생할 수 있습니다.
대용량 폴더 작업
- Files.walk는 주의해서 사용하세요. 폴더에 수천 개의 파일이 있다면 시간이 오래 걸리고 메모리를 많이 사용할 수 있습니다.
- 최상위만 처리하면 된다면 Files.list를 사용하세요.
- 파일 마스크로 찾을 때는 이름 필터링(endsWith, matches 등)을 사용하세요.
예시: “임시 파일로부터 폴더 청소”
Path tempDir = Paths.get("data/tmp");
try (var stream = Files.walk(tempDir)) {
stream
.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".tmp"))
.forEach(path -> {
try {
Files.delete(path);
System.out.println("삭제됨: " + path);
} catch (IOException e) {
System.out.println("삭제 오류 " + path + ": " + e.getMessage());
}
});
}
대량 작업을 위한 주요 메서드
| 작업 | 메서드/접근 | 특징 |
|---|---|---|
| 모든 파일 가져오기 | |
walk — 재귀, list — 상위 레벨만 |
| 확장자로 필터링 | |
다른 필터와 조합 가능 |
| 파일 그룹 복사 | |
대상 폴더 존재 여부 확인 |
| 파일 그룹 삭제 | |
각 작업마다 try-catch 사용 권장 |
| 일괄 이름 변경 | |
이름 변경은 새 이름으로 move 하는 것 |
6. 대량 작업에서 자주 하는 실수
오류 №1: forEach 내부에서 예외를 처리하지 않음.
아주 흔한 문제입니다. 각 작업을 try-catch로 감싸지 않으면 첫 번째 문제 파일에서 프로그램이 “쓰러지고”, 나머지는 처리되지 않습니다.
오류 №2: 폴더를 파일처럼 삭제/복사하려는 시도(또는 그 반대).
Files.delete와 Files.copy는 파일과 폴더를 다루는 방식이 다릅니다. 혼동하지 마세요! 예를 들어, 비어 있지 않은 폴더를 표준 메서드로 삭제하려 하면 오류가 발생합니다.
오류 №3: 복사 시 대상 경로를 잘못 구성.
구조를 고려하지 않고 대상 경로를 만들면(예: relativize를 사용하지 않음) 파일이 덮어써지거나 아카이브 구조가 엉킬 수 있습니다.
오류 №4: 동시에 너무 많은 파일을 열기.
루프 안에서 읽기/쓰기 스트림을 연다면 반드시 닫으세요! try-with-resources를 사용하는 것이 좋습니다.
오류 №5: 깊이 제한 없는 재귀 순회.
아주 크고 깊은 폴더에서 Files.walk를 사용하면 성능과 메모리 문제가 생길 수 있습니다. 깊이를 제한해야 한다면 깊이 매개변수가 있는 오버로드를 사용하세요: Files.walk(dir, depth).
오류 №6: 숨김/시스템 파일을 암묵적으로 무시.
사용자 파일을 다루는 애플리케이션에서는 실수로 숨김 또는 시스템 파일을 건드릴 수 있습니다. 이를 필터링하려면 Files.isHidden(path) 메서드를 사용하세요.
GO TO FULL VERSION