1. 소개
파일과 폴더를 다룰 때, 특정 파일만 고르고 싶은 경우가 많습니다. 예를 들어 모든 ".java" 파일, 모든 이미지, 특정 날짜의 로그 등입니다. 이를 위해 Java(및 다른 환경)에서는 글로빙(glob)과 정규 표현식(regex)을 사용합니다.
- 글로빙 — 특수 기호(*, ?, [], {})로 파일 이름 패턴을 간단히 표현하는 방법으로, Linux나 Windows의 명령줄과 유사합니다.
- Regex — 복잡한 패턴을 위한 강력한 정규 표현식 언어입니다.
글로빙 패턴 예시
- *.java — 현재 폴더의 모든 ".java" 파일.
- **/*.java — 모든 하위 폴더의 ".java" 파일(별표 두 개 — 재귀 검색).
- *.{png,jpg} — 확장자가 ".png" 또는 ".jpg"인 모든 파일.
- file-??.log — "file-01.log", "file-AB.log"처럼 임의의 두 글자를 가진 파일.
- [A-Z]*.txt — 대문자로 시작하는 모든 ".txt" 파일.
글로빙은 regex보다 단순하며, 파일 필터링에는 대부분 이 정도면 충분합니다.
glob와 regex 비교
| 특징 | glob | regex |
|---|---|---|
| 단순성 | 매우 쉬움 | 더 복잡하지만 더 강력함 |
| 사용 기호 | |
regex의 모든 기능 |
| 예 | |
|
| 재귀 지원 | |
내장 지원 없음 |
| 언제 사용 | 파일 필터링 | 복잡한 검사 |
2. PathMatcher: 패턴으로 파일 필터링
Java NIO(java.nio.file)에서는 패턴으로 파일을 필터링하기 위해 PathMatcher 인터페이스를 사용합니다. 이는 FileSystems.getDefault().getPathMatcher(...)를 통해 얻을 수 있습니다.
구문
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.java");
- "glob:*.java" — 글로빙 패턴.
- "regex:.*\\.java" — 정규식 패턴.
예: 폴더에서 파일 필터링
import java.nio.file.*;
Path dir = Paths.get("src");
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.java");
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
for (Path entry : stream) {
if (matcher.matches(entry.getFileName())) {
System.out.println(entry);
}
}
}
중요:
- matcher.matches()는 보통 파일 이름만 검사하므로 전체 경로가 아니라 entry.getFileName()을 전달하세요.
- 재귀 검색에는 Files.walk() 또는 Files.find()를 사용하세요.
Files.walk()는 지정한 디렉터리의 모든 파일과 폴더를 하위 폴더까지 재귀적으로 담은 Stream<Path>를 반환하는 NIO2 메서드입니다. DirectoryStream이 한 폴더의 내용만 보여주는 것과 달리, Files.walk()는 Stream API로 디렉터리 트리를 다룰 수 있게 해줍니다.
사용 예:
import java.nio.file.*;
import java.util.stream.Stream;
Path start = Paths.get("src");
try (Stream<Path> stream = Files.walk(start)) { // 모든 하위 폴더를 재귀적으로
stream.filter(Files::isRegularFile)
.forEach(System.out::println);
}
regex 예
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("regex:.*\\.(png|jpg)");
3. Files.newDirectoryStream: 디렉터리 열람 시 필터링
메서드 Files.newDirectoryStream()을 사용하면 패턴이나 사용자 정의 필터로 바로 파일을 필터링할 수 있습니다.
glob 패턴으로 필터링
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.java")) {
for (Path entry : stream) {
System.out.println(entry);
}
}
두 번째 인자는 glob 패턴입니다(접두사 "glob:" 없이).
DirectoryStream.Filter를 사용한 필터링
크기, 날짜, 폴더 제외 등 더 복잡한 로직이 필요하면 DirectoryStream.Filter<Path>를 사용하세요:
DirectoryStream.Filter<Path> filter = path -> {
// 예: 폴더가 아닌 파일만, 그리고 .git, node_modules 제외
return Files.isRegularFile(path)
&& !path.getFileName().toString().equals(".git")
&& !path.getFileName().toString().equals("node_modules");
};
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, filter)) {
for (Path entry : stream) {
System.out.println(entry);
}
}
4. Files.find: BiPredicate로 강력한 검색
이름, 크기, 날짜 등 복잡한 조건(재귀 포함)으로 파일을 찾아야 한다면 Files.find를 사용하세요.
구문
Stream<Path> stream = Files.find(
startDir, // 검색 위치
maxDepth, // 깊이 (Integer.MAX_VALUE — 재귀)
(path, attrs) -> {
// path — 파일 경로
// attrs — 파일 속성(크기, 날짜 등)
return path.getFileName().toString().endsWith(".log")
&& attrs.size() > 1024; // 큰 로그만
}
);
stream.forEach(System.out::println);
- 두 번째 인자는 최대 검색 깊이입니다.
- 세 번째는 BiPredicate<Path, BasicFileAttributes>로, 파일이 조건에 맞으면 true를 반환합니다.
예: .git과 node_modules 폴더 제외
Stream<Path> stream = Files.find(
Paths.get("."),
Integer.MAX_VALUE,
(path, attrs) -> {
String name = path.getFileName().toString();
// .git과 node_modules 폴더는 제외
if (name.equals(".git") || name.equals("node_modules")) return false;
// 1 MB보다 큰 .log 파일만
return name.endsWith(".log") && attrs.size() > 1024 * 1024;
}
);
stream.forEach(System.out::println);
5. 실습
예 1: .git과 node_modules를 제외한 모든 .log 파일 찾기
Files.walk(Paths.get("."))
.filter(path -> {
String name = path.getFileName().toString();
// 폴더 제외
if (name.equals(".git") || name.equals("node_modules")) return false;
// .log 파일만
return name.endsWith(".log");
})
.forEach(System.out::println);
예 2: 특정 날짜 이후에 생성된 모든 이미지 찾기
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;
Instant after = Instant.parse("2024-06-01T00:00:00Z");
Files.find(
Paths.get("images"),
Integer.MAX_VALUE,
(path, attrs) -> {
String name = path.getFileName().toString();
return (name.endsWith(".png") || name.endsWith(".jpg"))
&& attrs.creationTime().toInstant().isAfter(after);
}
).forEach(System.out::println);
예 3: .git을 제외하고 큰 파일(10 MB 초과) 모두 찾기
Files.find(
Paths.get("."),
Integer.MAX_VALUE,
(path, attrs) -> {
String name = path.getFileName().toString();
return !name.equals(".git") && attrs.size() > 10 * 1024 * 1024;
}
).forEach(System.out::println);
6. 유용한 팁
glob 구문 치트시트
- * — 구분자 /를 제외한 임의의 문자 0개 이상
- ** — 임의의 하위 디렉터리 경로(Java에서 지원)
- ? — 임의의 문자 정확히 1개
- [abc] — a, b, c 중 하나
- [a-z] — 범위 내 임의의 문자
- {a,b,c} — 나열된 값 중 하나(예: *.{png,jpg})
예시:
- *.java — 현재 폴더의 모든 ".java" 파일
- **/*.java — 모든 하위 폴더의 ".java" 파일
- file-??.log — "file-01.log", "file-AB.log" 같은 파일
- [A-Z]*.txt — 대문자로 시작하는 모든 ".txt" 파일
성능과 누수: 반드시 DirectoryStream을 닫으세요!
중요!
- DirectoryStream과 Files.find/Files.walk가 반환하는 스트림은 반드시 닫아야 하는 자원입니다.
- try-with-resources를 사용하세요:
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.java")) {
for (Path entry : stream) {
// ...
}
}
- 스트림을 닫지 않으면 리소스 누수가 발생할 수 있습니다(예: '열린 파일이 너무 많음').
- Files.find와 Files.walk의 경우 반드시 close()를 호출하거나 try-with-resources를 사용하세요:
try (Stream<Path> stream = Files.find(...)) {
stream.forEach(System.out::println);
}
7. 정리와 흔한 실수
오류 1: *가 재귀적으로 검색하지 않는다는 점을 모른 채 glob 패턴을 사용하는 경우. 재귀가 필요하면 "**/*.java" 또는 Files.walk를 사용하세요.
오류 2: matcher.matches()에 전체 경로를 전달하는 경우 — 보통 파일 이름(getFileName())만 전달해야 합니다.
오류 3: DirectoryStream 또는 Stream<Path>를 닫지 않음 — 리소스 누수가 발생합니다.
오류 4: 단순한 필터링에 지나치게 복잡한 regex 패턴을 사용함 — 충분하다면 glob를 사용하세요.
오류 5: 서비스용 폴더(".git", "node_modules")를 제외하지 않음 — 검색이 느려지고 불필요한 파일로 '오염'됩니다.
GO TO FULL VERSION