CodeGym /행동 /JAVA 25 SELF /글로빙/PathMatcher, DirectoryStream.Filter

글로빙/PathMatcher, DirectoryStream.Filter

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

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의 모든 기능
*.java
.*\.java
재귀 지원
**/*.java
내장 지원 없음
언제 사용 파일 필터링 복잡한 검사

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을 닫으세요!

중요!

  • DirectoryStreamFiles.find/Files.walk가 반환하는 스트림은 반드시 닫아야 하는 자원입니다.
  • try-with-resources를 사용하세요:
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.java")) {
    for (Path entry : stream) {
        // ...
    }
}
  • 스트림을 닫지 않으면 리소스 누수가 발생할 수 있습니다(예: '열린 파일이 너무 많음').
  • Files.findFiles.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")를 제외하지 않음 — 검색이 느려지고 불필요한 파일로 '오염'됩니다.

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