관리 메뉴

나구리의 개발공부기록

File, Files, 경로 표시, Files로 문자 파일 읽기, 파일 복사 최적화 본문

인프런 - 실전 자바 로드맵/실전 자바 - 고급 2편, 입출력, 네트워크, 리플렉션

File, Files, 경로 표시, Files로 문자 파일 읽기, 파일 복사 최적화

소소한나구리 2025. 2. 24. 17:36
728x90

출처 : 인프런 - 김영한의 실전 자바 - 고급2편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


File

자바에서 파일 또는 디렉토리를 다룰 때는 File 혹은 Files, Path 클래스를 사용하면 파일이나 폴더를 생성, 삭제하고 정보를 확인할 수 있음

 

OldFileMain

파일과 디렉토리를 다양하게 생성, 수정, 삭제, 조회하는 기능들을 제공함

해당 메서드를 실행해보면 example.txt 파일과 exampleDir 디렉토리가 만들어지고, example.txt 파일은 renameTo 메서드를 통해 newExample.txt로 변경되는 것을 확인할 수 있음

 

File 객체를 생성했다고 파일이나 디렉토리가 바로 만들어지는 것은 아니며 메서드를 통해 생성해야 함

package io.file;

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. exists(): 파일이나 디렉토리의 존재 여부를 확인
        System.out.println("file.exists() = " + file.exists());

        // 2. createNewFile(): 새 파일 생성
        boolean created = file.createNewFile();
        System.out.println("File created = " + created);

        // 3. mkdir(): 새 디렉토리 생성
        boolean dirCreated = directory.mkdir();
        System.out.println("Directory created = " + dirCreated);

        // 4. delete(): 파일이나 디렉토리를 삭제
//        boolean deleted = file.delete();
//        System.out.println("File deleted = " + deleted);

        // 5. isFile(): 파일인지 확인
        System.out.println("Is file = " + file.isFile());

        // 6. isDirectory(): 디렉토리인지 확인
        System.out.println("Is directory = " + directory.isDirectory());

        // 7. getName(): 파일이나 디렉토리의 이름을 반환
        System.out.println("File Name = " + file.getName());

        // 8. length(): 파일의 크기를 바이트 단위로 반환
        System.out.println("File size " + file.length() + " bytes");

        // 9. renameTo(File dest): 파일의 이름을 변경하거나 이동
        File newFile = new File("temp/newExample.txt");
        boolean renamed = file.renameTo(newFile);
        System.out.println("File renamed = " + renamed);

        // 10. lastModified(): 마지막으로 수정된 시간을 반환
        long lastModified = newFile.lastModified();
        System.out.println("Last modified = " + new Date(lastModified));

    }
}
/* 실행 결과
file.exists() = false
File created = true
Directory created = false
Is file = true
Is directory = true
File Name = example.txt
File size 0 bytes
File renamed = true
Last modified = Sat Feb 22 16:56:45 KST 2025
*/

File과 같은 클래스들은 학습해야할 중요한 원리가 있는것이 아니라 다양한 기능의 모음을 제공함

이런 클래스의 기능들은 외우기 보다는 이런 것들이 있다 정도만 간단히 알아두고 필요할 때 찾아서 사용하면 됨


Files

File, Files의 역사

자바 1.0에서 File 클래스가 등장했고 이후에 자바 1.7에서 File 클래스를 대체할 Files와 Path가 등장했음

 

Files의 특징

  • 성능과 편의성이 모두 개선되었음
  • File은 과거의 호환을 유지하기 위해 남겨둔 기능이기 때문에 이제는 Files 사용을 먼저 고려해야 함
  • Files는 수 많은 유틸리티 기능이 있으며 성능은 물론 사용하기에도 더 편리하므로 File 클래스는 물론 File과 관련된 스트림(FileInputStream, FileWriter)의 사용을 고민하기 전에 Files에 있는 기능을 먼저 찾아보는 것이 좋음
  • 이러한 기능 위주의 클래스는 외우는 것이 아니라 이런 것이 있다 정도로 주요 기능만 알아두고 나머지는 필요할 때 검색하면 됨

newFilesMain

  • Files는 직접 생성할 수 없고 static 메서드를 통해 기능을 제공함
  • Files를 사용할 때 파일이나 디렉토리의 경로는 Path 인터페이스를 사용해야 함
  • 파일이나 디렉토리를 생성할 때 이미 동일이름의 파일이나 디렉토리가 있으면 FileAlreadyExistsException을 던짐
  • Files.readAttributes(newFile, BasicFileAttributes.class)로 파일의 기본 속성들을 한번에 확인할 수 있음
  • Paths.get()은 옛날 방식이므로 Path.of()를 사용하여 파일이나 디렉토리의 경로를 지정하는 것을 권장함
package io.file;

public class NewFilesMain {
    public static void main(String[] args) throws IOException {
        Path file = Path.of("temp/example.txt");
        Path directory = Path.of("temp/exampleDir");

        // 1. exists(): 파일이나 디렉토리의 존재 여부를 확인
        System.out.println("file.exists = " + Files.exists(file));

        // 2. createNewFile(): 새 파일 생성
        try {
            Files.createFile(file);
            System.out.println("File created");
        } catch (FileAlreadyExistsException e) {
            System.out.println(file + "File already exists");
        }

        // 3. mkdir(): 새 디렉토리 생성
        try {
            Files.createDirectory(directory);
            System.out.println("Directory created");
        } catch (FileAlreadyExistsException e) {
            System.out.println(directory + "Directory already exists");
        }

        // 4. delete(): 파일이나 디렉토리를 삭제
//        Files.delete(file);
//        System.out.println("File deleted");

        // 5. isRegularFile(): 일반 파일인지 확인
        System.out.println("Is regular file = " + Files.isRegularFile(file));

        // 6. isDirectory(): 디렉토리인지 확인
        System.out.println("Is directory = " + Files.isDirectory(directory));

        // 7. getFileName(): 파일이나 디렉토리의 이름을 반환
        System.out.println("File name = " + file.getFileName());

        // 8. size(): 파일의 크기를 바이트 단위로 반환
        System.out.println("File size = " + Files.size(file) + " bytes");

        // 9. move(): 파일의 이름을 변경하거나 이동
//        Path newFile = Paths.get("temp/newExample2.txt"); // 옛날 방식
        Path newFile = Path.of("temp/newExample2.txt");    // 추천
        try {
            Files.move(file, newFile);
        } catch (FileAlreadyExistsException e) {
            System.out.println(newFile + "File already exists");
        }
        System.out.println("File moved/renamed");

        // 10. getLastModifiedTime(): 마지막으로 수정된 시간을 반환
        System.out.println("Last modified = " + Files.getLastModifiedTime(newFile));

        // 추가: readAttributes(): 파일의 기본 속성들을 한 번에 읽기
        BasicFileAttributes attrs = Files.readAttributes(newFile, BasicFileAttributes.class);
        System.out.println("==== Attributes ====");
        System.out.println("attrs.creationTime() = " + attrs.creationTime());
        System.out.println("attrs.isDirectory() = " + attrs.isDirectory());
        System.out.println("attrs.isRegularFile() = " + attrs.isRegularFile());
        System.out.println("attrs.isSymbolicLink() = " + attrs.isSymbolicLink());
        System.out.println("attrs.size() = " + attrs.size());
    }
}
/* 실행 결과
file.exists = true
temp/example.txtFile already exists
temp/exampleDirDirectory already exists
Is regular file = true
Is directory = true
File name = example.txt
File size = 0 bytes
temp/newExample2.txtFile already exists
File moved/renamed
Last modified = 2025-02-22T10:29:15.595704868Z
==== Attributes ====
attrs.creationTime() = 2025-02-22T10:29:15Z
attrs.isDirectory() = false
attrs.isRegularFile() = true
attrs.isSymbolicLink() = false
attrs.size() = 0
*/

 


경로 표시

File 경로 표시

OldFilePath

  • file.listFiles(): 현재 경로에 있는 모든 파일 또는 디렉토리를 반환함, 각 요소를 isFile()로 비교하여 파일이면 F, 파일이 아니면 D로 표현하도록 하여 현재 경로의 파일과 디렉토리를 출력함
package io.file;

public class OldFilePath {
    public static void main(String[] args) throws IOException {
        File file = new File("temp/..");
        System.out.println("path = " + file.getPath());

        // 절대 경로
        System.out.println("Absolute path = " + file.getAbsolutePath());

        // 정규 경로
        System.out.println("Canonical path = " + file.getCanonicalPath());

        File[] files = file.listFiles();
        for (File f : files) {
            System.out.println((f.isFile() ? "F" : "D") + " | " + f.getName());
        }
    }
}
/* 실행 결과
path = temp/..
Absolute path = /Users/jinagyeomi/Desktop/dev/study/clonecoding-study/java/6.java-adv2/temp/..
Canonical path = /Users/jinagyeomi/Desktop/dev/study/clonecoding-study/java/6.java-adv2
D | temp
F | 6.java-adv2.iml
D | out
F | .gitignore
D | .idea
D | src
*/

절대 경로(Absolute path): 경로의 처음부터 내가 입력한 모든 경로를 다 표현함

정규 경로(Canonical path): 경로의 계산이 모두 끝난 경로이며 정규 경로는 하나만 존재함

  • 예제에서 ..은 바로 위의 상위 디렉토리를 뜻하는데 이런 경로의 계산을 모두 처리하면 하나의 경로만 남음
  • 절대 경로는 다음 2가지 경로가 모두 가능함
    • /Users/jinagyeomi/Desktop/dev/study/clonecoding-study/java/6.java-adv2/temp/..
    • /Users/jinagyeomi/Desktop/dev/study/clonecoding-study/java/6.java-adv2
  • 정규 경로는 아래의 하나만 가능함
    • /Users/jinagyeomi/Desktop/dev/study/clonecoding-study/java/6.java-adv2

Files 경로 표시

NewFilesPath

  • Files.list(path): 현재 경로에 있는 모든 파일 또는 디렉토리를 Stream으로 반환
  • Stream은 람다와 스트림에서 따로 학습하며 여기서는 toList()를 통해 List컬렉션으로 변경하여 사용하였음
  • 코드의 설명은 File을 사용했던것과 동일함
package io.file;

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<Path> pathStream = Files.list(path);
        List<Path> list = pathStream.toList();
        pathStream.close();
        for (Path p : list) {
            System.out.println((Files.isRegularFile(p) ? "F" : "D") + " | " + p.getFileName());
        }
    }
}

/* 실행 결과
path = temp/..
Absolute path = /Users/jinagyeomi/Desktop/dev/study/clonecoding-study/java/6.java-adv2/temp/..
Canonical path = /Users/jinagyeomi/Desktop/dev/study/clonecoding-study/java/6.java-adv2
D | temp
F | 6.java-adv2.iml
D | out
F | .gitignore
D | .idea
D | src
*/

Files로 문자 파일 읽기

문자로된 파일을 읽고 쓸 때 과거에는 FileReader, FileWriter 같은 복잡한 스트림 클래스를 사용해야 했음

거기에 모든 문자를 읽으려면 반복문을 사용해서 파일의 끝까지 읽어야 하는 과정을 추가해야했고 한 줄 단위로 파일을 읽으려면 BufferedReader와 같은 스트림 클래스를 추가해야 했음

 

Files는 이런 문제를 코드 한줄로 깔끔하게 해결해줌

Files - 모든 문자 읽기

ReadTextFileV1

Files를 사용하면 매우 쉽게 파일에 문자를 쓰고 읽을 수 있는 것을 확인할 수 있으며, 파일도 인코딩 정보로 잘 생성되는 것을 알 수 있음

  • Files.writeString(): 파일에 쓰기
  • Files.readString(): 파일에서 모든 문자 읽기
package io.file.text;

public class ReadTextFileV1 {
    private static final String PATH = "temp/hello2.txt";

    public static void main(String[] args) throws IOException {
        String writeString = "abc\n가나다";
        System.out.println("== Write String ==");
        System.out.println(writeString);

        Path path = Path.of(PATH);

        // 파일에 쓰기
        Files.writeString(path, writeString, UTF_8);

        // 파일에서 읽기
        String readString = Files.readString(path, UTF_8);

        System.out.println("== Read String ==");
        System.out.println(readString);
    }
}
/* 실행 결과
== Write String ==
abc
가나다
== Read String ==
abc
가나다
*/

Files - 라인 단위로 읽기

ReadTextFileV2

  • Files.readAllLines(path): 파일을 한 번에 다 읽고 라인 단위로 List에 나누어 저장하고 반환함
  • 편리하게 파일의 내용을 한 줄 단위로 읽을 수 있지만 파일을 한번에 통째로 불러오기 때문에 용량이 매우 큰 파일은 이 방법이 아닌 lines(path)를 활용하는 것이 좋음
package io.file.text;

public class ReadTextFileV2 {
    private static final String PATH = "temp/hello3.txt";

    public static void main(String[] args) throws IOException {
        String writeString = "line\n한 줄씩 읽기\n한 줄 더 읽어라";
        System.out.println("== Write String ==");
        System.out.println(writeString);

        Path path = Path.of(PATH);

        // 파일에 쓰기
        Files.writeString(path, writeString, UTF_8);

        // 파일에서 읽기
        System.out.println("== Read String ==");
        List<String> lines = Files.readAllLines(path, UTF_8);
        for (int i = 0; i < lines.size(); i++) {
            System.out.println((i + 1) + ": " + lines.get(i));
        }
    }
}
/* 실행 결과
== Write String ==
line
한 줄씩 읽기
한 줄 더 읽어라
== Read String ==
1: line
2: 한 줄씩 읽기
3: 한 줄 더 읽어라
*/

 

Files.lines(path)

try (Stream<String> lineStream = Files.lines(path, UTF_8)) {
    lineStream.forEach(line -> System.out.println(line));
}
  • 파일을 한 줄 단위로 나누고 읽고, 메모리 사용량을 줄이고 싶다면 이 기능을 사용하면 되는데 이 기능을 제대로 이해하려면 람다와 스트림을 알아야 하므로 람다와 스트림을 배우지 않은 시점에서는 이런것이 있다 정도만 알아두면 됨
  • 파일을 스트림(I/O스트림이 아니라 람다와 스트림에서 사용하는 스트림임) 단위로 나누어 조회하기 때문에 파일의 용량이 매우 클 때 사용함
  • 이 기능을 사용하면 파일을 한 줄 단위로 메모리에 올릴 수 있어 만약 한 줄당 1MB의 용량을 사용한다면 자바는 파일에서 한 번에 1MB의 데이터만 메모리에 올려서 처리하고, 처리가 끝나면 다음 줄을 호출한 후 기존에 사용한 1M 데이터는 GC함
  • 용량이 너무 커서 자바 메모리에 한 번에 불러오는 것이 불가능할 수 있기 때문에 용량이 아주 큰 파일을 처리해야 한다면 이런 방식으로 처리하는 것이 효과 적임
  • 물론 BufferedReader를 통해서도 한 줄 단위로 이렇게 나누어 처리하는 것이 가능하긴 하지만 여기서의 핵심은 매우 편리하게 문자를 나누어 처리하는 것이 가능하다는 점임

파일 복사 최적화

예제 파일 생성

createCopyFile

파일을 복사하는 효율적인 방법을 알아보기 위해 200MB 임시 파일을 생성

package io.file.copy;

public class CreateCopyFile {

    private static final int FILE_SIZE = 200 * 1024 * 1024; // 200MB

    public static void main(String[] args) throws IOException {
        String fileName = "temp/copy.dat";
        long startTime = System.currentTimeMillis();

        FileOutputStream fos = new FileOutputStream(fileName);
        byte[] buffer = new byte[FILE_SIZE];
        fos.write(buffer);
        fos.close();

        long endTime = System.currentTimeMillis();
        System.out.println("File created : " + fileName);
        System.out.println("File size : " + FILE_SIZE / 1024 / 1024 + "MB");
        System.out.println("Time taken : " + (endTime - startTime) + " ms");
    }
}
/* 실행 결과
File created : temp/copy.dat
File size : 200MB
Time taken : 80 ms
*/

파일 복사 예제

FileCopyMainV1

  • FileInputStream에서 readAllBytes를 통해 한 번에 모든 데이터를 읽고 write(bytes)를 통해 한 번에 모든 데이터를 저장함
  • 파일(copy.dat) ➡ 자바(byte) ➡ 파일(copy_new.dat)의 과정을 거침
  • 자바가 copy.dat파일의 데이터를 자바 프로세스가 사용하는 메모리에 불러온 후 메모리에 있는 데이터를 copy_new.dat에 전달
package io.file.copy;

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.dat");

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

        long endTime = System.currentTimeMillis();
        System.out.println("Time taken: " + (endTime - startTime) + "ms");
    }
}
/* 실행 결과
Time taken: 121ms
*/

FileCopyMainV2

  • InputStream에는 자바9 부터 지원하는 transferTo()라는 특별한 메서드가 있는데 이 메서드는 InputStream에서 읽은 데이터를 바로 OutputStream으로 출력함
  • transferTo()는 내부에서 성능 최적화가 되어 있기 때문에 V1과 비슷하거나 조금 더 빠른데 상황에 따라 조금 느릴 수도 있음(디스크는 실행시 시간의 편차가 심함)
  • V1 버전과 동일한 과정을 거치지만 내부 최적화로 속도가 빨라졌고 2줄의 코드를 1줄로 줄이고 코드가 매우 직관적으로 보이는 효과가 있으며 스트림이기 때문에 파일의 정보로 네트워크에 전달하거다 다른 처리를 할 수 있음
package io.file.copy;

public class FileCopyMainV2 {
    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_new2.dat");

        fis.transferTo(fos);

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

        long endTime = System.currentTimeMillis();
        System.out.println("Time taken: " + (endTime - startTime) + "ms");
    }
}
/* 실행 결과
Time taken: 67ms
*/

FileCopyMainV3

Files.copy()

  • 앞의 예제들은 파일(copy.dat) ➡ 자바(byte) ➡ 파일(copy_new3.dat)의 과정으로 파일의 데이터를 자바로 불러오고 또 자바에서 읽은 데이터를 다시 파일에 전달해야 함
  • Files의 copy() 메서드는 자바에 파일 데이터를 불러오지 않고 운영체제의 파일 복사 기능을 사용하기 때문에 중간 과정이 생략됨
  • 즉 파일(copy.dat) ➡ 파일(copy_new3.dat)의 과정만 있으므로 가장 빠른 것을 실행 결과로 확인할 수 있음
  • 파일을 다루어야 할 일이 있다면 항상 Files의 기능을 먼저 찾아보는 것을 권장함
  • 물론 이 기능은 파일에서 파일을 복사할 때만 유용하므로 파일의 정보를 읽어서 처리해야 하거나, 스트림을 통해 네트워크에 전달해야 한다면 앞서 설명한 스트림을 직접 사용해야함
package io.file.copy;

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_new3.dat");
        
        // Copy 옵션을 줄 수 있음(REPLACE_EXISTING: 기존의 파일을 교체)
        Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

        long endTime = System.currentTimeMillis();
        System.out.println("Time taken: " + (endTime - startTime) + "ms");
    }
}
/* 실행 결과
Time taken: 47ms
*/

 

728x90