CodeGym /행동 /JAVA 25 SELF /파일 대량 작업

파일 대량 작업

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

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의 차이

메서드 반환 내용
Files.list
현재 폴더의 내용만
Files.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());
            }
        });
}

대량 작업을 위한 주요 메서드

작업 메서드/접근 특징
모든 파일 가져오기
Files.list, Files.walk
walk — 재귀, list — 상위 레벨만
확장자로 필터링
filter(path -> ...)
다른 필터와 조합 가능
파일 그룹 복사
forEach + Files.copy
대상 폴더 존재 여부 확인
파일 그룹 삭제
forEach + Files.delete
각 작업마다 try-catch 사용 권장
일괄 이름 변경
forEach + Files.move
이름 변경은 새 이름으로 move 하는 것

6. 대량 작업에서 자주 하는 실수

오류 №1: forEach 내부에서 예외를 처리하지 않음.
아주 흔한 문제입니다. 각 작업을 try-catch로 감싸지 않으면 첫 번째 문제 파일에서 프로그램이 “쓰러지고”, 나머지는 처리되지 않습니다.

오류 №2: 폴더를 파일처럼 삭제/복사하려는 시도(또는 그 반대).
Files.deleteFiles.copy는 파일과 폴더를 다루는 방식이 다릅니다. 혼동하지 마세요! 예를 들어, 비어 있지 않은 폴더를 표준 메서드로 삭제하려 하면 오류가 발생합니다.

오류 №3: 복사 시 대상 경로를 잘못 구성.
구조를 고려하지 않고 대상 경로를 만들면(예: relativize를 사용하지 않음) 파일이 덮어써지거나 아카이브 구조가 엉킬 수 있습니다.

오류 №4: 동시에 너무 많은 파일을 열기.
루프 안에서 읽기/쓰기 스트림을 연다면 반드시 닫으세요! try-with-resources를 사용하는 것이 좋습니다.

오류 №5: 깊이 제한 없는 재귀 순회.
아주 크고 깊은 폴더에서 Files.walk를 사용하면 성능과 메모리 문제가 생길 수 있습니다. 깊이를 제한해야 한다면 깊이 매개변수가 있는 오버로드를 사용하세요: Files.walk(dir, depth).

오류 №6: 숨김/시스템 파일을 암묵적으로 무시.
사용자 파일을 다루는 애플리케이션에서는 실수로 숨김 또는 시스템 파일을 건드릴 수 있습니다. 이를 필터링하려면 Files.isHidden(path) 메서드를 사용하세요.

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