File과 Files – 파일 관리 성능 최적화

Legacy File vs Modern Files & Path

과거의 방식 – java.io.File

File 클래스는 자바의 탄생과 함께했지만, 몇 가지 치명적인 단점이 있다

  • 에러 핸들링 불분명: 메서드가 실패했을 때 예외를 던지는 대신 false를 반환하는 경우가 많아 원인 파악이 어렵다
  • 제한된 기능: 심볼릭 링크 처리 불가, 파일 속성 변경의 어려움 등이 있다
  • 블로킹 방식: 현대적인 고성능 I/O 처리에 한계가 있다
public class OldFileMain {
    public static void main(String[] args) throws IOException {
        File file = new File("temp/example.txt");
        File directory = new File("temp/exampleDir");

        // 1. 존재 여부 확인
        System.out.println("File exists: " + file.exists());

        // 2. 실제 파일 및 디렉토리 생성
        boolean created = file.createNewFile();
        boolean dirCreated = directory.mkdir();

        // 3. 파일 정보 확인
        System.out.println("Is file: " + file.isFile());
        System.out.println("File name: " + file.getName());
        System.out.println("File size: " + file.length() + " bytes");

        // 4. 이름 변경 및 이동
        File newFile = new File("temp/newExample.txt");
        file.renameTo(newFile);
    }
}

현대적인 방식 – java.nio.file.Files & Path

자바 1.7(NIO.2)부터 도입된 이 방식은 성능과 편의성을 획기적으로 개선했다

  • 객체지향적인 경로 관리: Path 클래스를 통해 경로를 추상화한다
  • 다양한 유틸리티: Files 클래스가 제공하는 static 메서드들을 통해 복잡한 작업을 코드 한 줄로 해결한다
  • 상세한 예외 처리: 실패 시 IOException의 하위 예외 (예: NoSuchFileException)를 던져 정확한 실패 원인을 알 수 있다
public class NewFilesMain {
    public static void main(String[] args) throws IOException {
        Path file = Path.of("temp/example.txt");
        Path directory = Path.of("temp/exampleDir");

        System.out.println("File exists: " + Files.exists(file));
        
        try {
            Files.createFile(file);
            System.out.println("File created");
        } catch (FileAlreadyExistsException e) {
            System.out.println(file + " already exists");
        }

        // 파일 속성 한 번에 읽기 (BasicFileAttributes 활용)
        BasicFileAttributes attrs = Files.readAttributes(file, BasicFileAttributes.class);
        System.out.println("Creation time: " + attrs.creationTime());
        System.out.println("Size: " + attrs.size() + " bytes");
    }
}

현대적인 Java 환경에서는 Files 사용을 권장한다. 특히 readAttributes()를 사용하면 여러 번의 시스템 호출 없이 파일의 생성 시간, 크기, 권한 등을 한 번에 효율적으로 가져올 수 있다

경로의 이해 – Absolute Path vs Canonical Path

파일 시스템에서 경로를 다룰 때 가장 헷갈리는 부분은 바로 절대 경로정규 경로이다 (작성자는…)

  • Path: 단순히 입력받은 경로 그 자체이다 (예: temp/..)
  • Absolute Path (절대 경로): 운영체제의 루트(Root) 시작하는 전체 경로이다. 하지만 ..(상위 디렉토리) 같은 상대적 기호가 포함될 수 있다
  • Canonical Path (정규 경로): 경로상의 모든 기호(., ..)를 계산하여 물리적으로 유일하게 존재하는 경로이다. 보안상 중요한 검증을 할 때는 정규 경로를 사용해야 한다
public class NewFilesPath {
    public static void main(String[] args) throws IOException {
        Path path = Path.of("temp/.."); // 현재 위치의 상위 디렉토리를 지칭

        System.out.println("path = " + path);
        // 절대 경로: 입력된 경로를 루트부터 단순히 표현
        System.out.println("Absolute path: " + path.toAbsolutePath());
        // 정규 경로: .. 등을 모두 계산한 실제 물리적 경로
        System.out.println("Canonical path: " + path.toRealPath());
        
        // 디렉토리 내 파일 목록 출력 (Stream API 활용)
        try (Stream<Path> pathStream = Files.list(path)) {
            pathStream.forEach(p -> System.out.println(
                (Files.isRegularFile(p) ? "F" : "D") + " | " + p.getFileName()));
        }
    }
}

텍스트 파일 읽기의 진화 – 메모리 효율성

readString() (작은 파일용)

파일 전체 내용을 하나의 String으로 읽는다. 코드가 가장 단순하지만, 파일이 크면 OutOfMemoryException가 발생할 수 있다

// 파일 전체를 하나의 String으로 읽기
String readString = Files.readString(path, StandardCharsets.UTF_8);

readAllLines() (중간 크기 파일용)

파일을 라인(Line) 단위로 리스트에 담는다. 줄 단위 처리가 편하지만, 역시 전체 리스트를 메모리에 올린다는 점을 주의해야 한다

List<String> lines = Files.readAllLines(path, UTF_8);
for (int i = 0; i < lines.size(); i++) {
    System.out.println((i + 1) + ": " + lines.get(i));
}

lines() (대용량 파일용 – 권장)

JavaStream API를 활용한다. 파일을 한 줄씩 스트림으로 읽어 처리하기 때문에 수 GB의 대용량 파일도 메모리 점유율을 낮게 유지하며 처리할 수 있다

public class ReadTextFileV3 {
    public static void main(String[] args) throws IOException {
        Path path = Path.of("temp/hello2.txt");

        System.out.println("== Read String by Stream ==");
        // 한 줄씩 메모리에 올려 처리 (OOM 방지)
        try (Stream<String> lineStream = Files.lines(path)) {
            lineStream.forEach(System.out::println);
        }
    }
}

파일 복사 성능 최적화

200MB 파일을 복사할 때, 방식에 따라 성능 차이는 극명하게 갈린다

자바 메모리 경유 (V1)

FileInputStream으로 데이터를 읽어 자바 메모리(byte[])에 올린 뒤 다시 쓰는 방식이다. 파일 시스템 → 커널 영역 → 유저 영역(자바) → 커널 영역 → 파일 시스템이라는 복잡한 단계를 거친다(CPU 소비 높음)

public class FileCopyMainV1 {
    public static void main(String[] args) throws IOException {
        long startTime = System.currentTimeMillis();

        FileInputStream fis = new FileInputStream("temp/copy.dat");
        FileOutputStream fos = new FileOutputStream("temp/copy_new.dat");

        byte[] bytes = fis.readAllBytes();
        fos.write(bytes);

        fis.close();
        fos.close();

        long endTime = System.currentTimeMillis();
        System.out.println("Time taken: " + (endTime - startTime) + " ms");
    }
}

transferTo() 활용 (V2)

자바 9에서 도입된 InputStream.transferTo(OutputStream)는 내부적으로 버퍼 처리가 최적화되어 있어 코드가 훨씬 간결하고 빠르다

public class FileCopyMainV3 {
    public static void main(String[] args) throws IOException {
        long startTime = System.currentTimeMillis();

        FileInputStream fis = new FileInputStream("temp/copy.dat");
        FileOutputStream fos = new FileOutputStream("temp/copy_new.dat");
        
        fis.transferTo(fos); // 내부 버퍼를 이용한 효율적 전송

        long endTime = System.currentTimeMillis();
        System.out.println("Time taken: " + (endTime - startTime) + " ms");
    }
}

Files.copy() 운영체제 위임 (V3 – 최적)

자바 메모리에 데이터를 올리지 않고, 운영체제 레벨의 파일 복사 기능(Zero-Copy 기술)을 직접 호출한다. 자바 프로세스를 거치지 않고 OS 커널 단에서 복사가 일어나기 때문에 압도적인 성능을 보여준다

public class FileCopyMainV3 {
    public static void main(String[] args) throws IOException {
        long startTime = System.currentTimeMillis();

        Path source = Path.of("temp/copy.dat");
        Path target = Path.of("temp/copy_new.dat");
        
        // 운영체제 레벨의 복사 기능 사용
        Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

        long endTime = System.currentTimeMillis();
        System.out.println("Time taken: " + (endTime - startTime) + " ms");
    }
}

출처 – 김영한 님의 강의 중 김영한의 실전 자바 – 고급 2편, I/O, 네트워크, 리플렉션