참고자료
김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션 강의 | 김영한 - 인프런
김영한 | I/O, 네트워크, 리플렉션, 애노테이션을 기초부터 실무 레벨까지 깊이있게 학습합니다. 웹 애플리케이션 서버(WAS)를 자바로 직접 만들어봅니다., 국내 개발 분야 누적 수강생 1위, 제대로
www.inflearn.com
File
자바에서 파일 또는 디렉토리를 다룰 때는 File 또는 Files, Path 클래스를 사용하면 된다. 이 클래스들을 사용하면 파일이나 폴더를 생성하고, 삭제하고, 또 정보를 확인할 수 있다.
먼저 File 클래스를 사용해보자. 이 방식은 오래된 방식으로 이제는 새로운 방식인 Files를 더 많이 사용하긴 하지만, 여전히 알아두어야 한다.
OldFileMain
package cwchoiit.io.file;
import java.io.File;
import java.io.IOException;
import java.util.Date;
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());
System.out.println("directory exists: " + directory.exists());
// 2. 파일과 디렉토리를 생성
boolean created = file.createNewFile();
boolean dirCreated = directory.mkdir();
System.out.println("file created: " + created);
System.out.println("Directory created: " + dirCreated);
// 3. 파일과 디렉토리 삭제
boolean deleted = file.delete();
boolean deletedDir = directory.delete();
System.out.println("File deleted: " + deleted);
System.out.println("Directory deleted: " + deletedDir);
// 4. 파일인지 디렉토리인지
System.out.println("is file: " + file.isFile());
System.out.println("is directory: " + directory.isDirectory());
// 5. 파일 이름, 사이즈
System.out.println("File Name: " + file.getName());
System.out.println("File Size: " + file.length());
// 6. 파일 이름 바꾸기
File newFile = new File("temp/newExample.txt");
boolean renamed = file.renameTo(newFile);
System.out.println("File renamed: " + renamed);
// 7. 파일의 마지막 변경 시간
long lastModified = newFile.lastModified();
System.out.println("last modified: " + new Date(lastModified));
}
}
실행 결과
file exists: false
directory exists: true
file created: true
Directory created: false
File deleted: true
Directory deleted: true
is file: false
is directory: false
File Name: example.txt
File Size: 0
File renamed: false
last modified: Mon Oct 14 20:50:57 KST 2024
File은 파일과 디렉토리를 둘 다 다룬다. 참고로 File 객체를 생성했다고 파일이나 디렉토리가 바로 만들어지는 것은 아니다. 메서드를 통해 생성해야 한다. File과 같은 클래스들은 학습해야 할 중요한 원리가 있는 것이 아니라, 다양한 기능의 모음을 제공한다. 이런 클래스의 기능들은 외우기 보다는 이런 것들이 있다 정도만 간단히 알아두고, 필요할 때 찾아서 사용하면 된다.
Files
File, Files의 역사
자바 1.0에서 File 클래스가 등장했다. 이후에 자바 1.7에서 File 클래스를 대체할 Files와 Path가 등장했다.
Files의 특징
- 성능과 편의성이 모두 개선되었다.
- File은 과거에 호환을 유지하기 위해 남겨둔 기능이다. 이제는 Files 사용을 먼저 고려하자.
- 여기에는 수많은 유틸리티 기능이 있다. File 클래스는 물론이고, File과 관련된 스트림(FileInputStream, FileWriter)의 사용을 고민하기 전에 Files에 있는 기능을 먼저 찾아보자. 성능도 좋고, 사용하기도 더 편리하다.
- 기능이 너무 많기 때문에 주요 기능만 알아보고, 나머지는 필요할 때 검색하자.
- 이렇게 기능 위주의 클래스는 외우는 것이 아니다. 이런게 있다 정도의 주요 기능만 알아두고, 나머지는 필요할 때 검색하면 된다.
앞서 작성한 코드를 Files로 그대로 사용해보자. 참고로 Files를 사용할 때 파일이나, 디렉토리의 경로는 Path 클래스를 사용해야 한다.
package cwchoiit.io.file;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
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. createFile(): 새 파일을 생성
try {
Files.createFile(file);
System.out.println("File created");
} catch (FileAlreadyExistsException e) {
System.out.println(file + " already exists");
} catch (IOException e) {
throw new RuntimeException(e);
}
// 3. createDirectory(): 새 디렉토리를 생성
try {
Files.createDirectory(directory);
System.out.println("Directory created");
} catch (FileAlreadyExistsException e) {
System.out.println(directory + " already exists");
} catch (IOException e) {
throw new RuntimeException(e);
}
// 4. delete(): 파일이나 디렉토리를 삭제
/*try {
Files.delete(file);
System.out.println("File deleted");
} catch (IOException e) {
throw new RuntimeException(e);
}*/
// 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 = Path.of("temp/newExample.txt");
Files.move(file, newFile, StandardCopyOption.REPLACE_EXISTING);
System.out.println("File moved");
// 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("Creation time: " + attrs.creationTime());
System.out.println("Is directory: " + attrs.isDirectory());
System.out.println("Is regular file: " + attrs.isRegularFile());
System.out.println("Is symbolic link: " + attrs.isSymbolicLink());
System.out.println("Size: " + attrs.size());
}
}
- Files는 직접 생성할 수 없고, static 메서드를 통해 기능을 제공한다.
실행 결과
File exists: false
File created
temp/exampleDir Directory already exists
Is regular file: true
Is directory: true
File name: example.txt
File size: 0 bytes
File moved/renamed
Last modified: 2024-09-03T06:32:09.182412076Z
===== Attributes =====
Creation time: 2024-09-03T06:32:09Z
Is directory: false
Is regular io.file: true
Is symbolic link: false
Size: 0
경로 표시
파일이나 디렉토리가 있는 경로는 크게 절대 경로와 정규 경로로 나눌 수 있다.
File을 사용한 경로 표시
package cwchoiit.io.file;
import java.io.File;
import java.io.IOException;
public class OldFilePath {
public static void main(String[] args) throws IOException {
// 앞에 아무것도 없고 "temp/.." 이렇게 되면 자바가 시작되는 지점부터다. (상대 경로)
File file = new File("temp/..");
System.out.println("path: " + file.getPath());
// 절대 경로
System.out.println("Absolute path: " + file.getAbsolutePath());
// 정규 경로 ("temp/.."에서 ..을 다 계산한 결과를 정규 경로라 한다)
System.out.println("Canonical path: " + file.getCanonicalPath());
File[] files = file.listFiles();
for (File f : files) {
System.out.println((f.isDirectory() ? "D" : "F") + " | " + f.getName());
}
}
}
실행 결과
path = temp/..
Absolute path = /Users/yh/study/inflearn/java/java-adv2/temp/..
Canonical path = /Users/yh/study/inflearn/java/java-adv2
D | temp
F | java-adv2-v1.iml
D | out
F | .gitignore
D | .idea
D | src
- 절대 경로(Absolute Path): 절대 경로는 경로의 처음부터 내가 입력한 모든 경로를 다 표현한다.
- 정규 경로(Canonical Path): 경로의 계산이 모두 끝난 경로이다. 정규 경로는 하나만 존재한다. 예제에서 `..`은 바로 위의 상위 디렉토리를 뜻한다. 이런 경로의 계산을 모두 처리하면 하나의 경로만 남는다.
- 그래서 절대 경로는 다음 2가지 경로가 모두 가능하지만,
- /Users/yh/study/inflearn/java/java-adv2
- /Users/yh/study/inflearn/java/java-adv2/temp/..
- 정규 경로는 다음 하나만 가능하다.
- /Users/yh/study/inflearn/java/java-adv2
file.listFiles()
- 현재 경로에 있는 모든 파일 또는 디렉토리를 반환한다.
- 파일이면 F, 디렉토리면 D로 표현했다.
FIles를 사용한 경로 표시
package cwchoiit.io.file;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
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);
pathStream.forEach(p -> System.out.println((Files.isRegularFile(p) ? "F" : "D") + " | " + p.getFileName()));
pathStream.close();
}
}
실행 결과
path = temp/..
Absolute path = /Users/yh/study/inflearn/java/java-adv2/temp/..
Canonical path = /Users/yh/study/inflearn/java/java-adv2
D | temp
F | java-adv2-v1.iml
D | out
F | .gitignore
D | .idea
D | src
Files.list(path)
- 현재 경로에 있는 모든 파일 또는 디렉토리를 반환한다.
- 파일이면 F, 디렉토리면 D로 표현했다.
Files로 문자 파일 읽기
문자로된 파일을 읽고 쓸 때 과거에는 FileReader, FileWriter 같은 복잡한 스트림 클래스를 사용해야 했다. 거기에 모든 문자를 읽으려면 반복문을 사용해서 파일의 끝까지 읽어야 하는 과정을 추가해야 했다. 또 한 줄 단위로 파일을 읽으려면 BufferedReader와 같은 스트림 클래스를 추가해야 했다.
Files는 이런 문제를 코드 한 줄로 깔끔하게 해결해준다.
Files - 모든 문자 읽기
package cwchoiit.io.file.text;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
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, StandardCharsets.UTF_8);
// 파일에서 읽기
String readString = Files.readString(path, StandardCharsets.UTF_8);
System.out.println("== Read String ==");
System.out.println(readString);
}
}
실행 결과
== Write String ==
abc
가나다
== Read String ==
abc
가나다
- Files.writeString(): 파일에 쓰기
- Files.readString(): 파일에서 모든 문자 읽기
Files를 사용하면 아주 쉽게 파일에 문자를 쓰고 읽을 수 있다.
Files - 라인 단위로 읽기
package cwchoiit.io.file.text;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
public class ReadTextFileV2 {
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, StandardCharsets.UTF_8);
// 파일에서 읽기 (라인별)
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
System.out.println("== Read String ==");
for (int i = 0; i < lines.size(); i++) {
System.out.println((i + 1) + ": " + lines.get(i));
}
}
}
실행 결과
== Write String ==
abc
가나다
== Read String ==
1: abc
2: 가나다
- Files.readAllLines(path) → 파일을 한번에 다 읽고, 라인 단위로 List에 나누어 저장하고 반환한다.
근데 이런 경우, 모든 라인을 일단 다 읽어서 리스트에 저장하기 때문에 파일이 엄청 크면 메모리를 매우 많이 차지하거나, 아예 OOM이 터질수도 있다. 그래서 파일을 한 줄 단위로 나누어 읽고, 메모리 사용량을 줄이고 싶다면 다음 기능을 사용하면 된다. 다만, 이 기능을 제대로 이해하려면, 람다와 스트림을 알아야 한다.
// 파일에서 읽기 (라인별 - 스트림)
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
lines.forEach(System.out::println);
}
- 파일을 스트림 단위로 나누어 조회한다. (I/O 스트림이 아니라, 람다와 스트림에서 사용하는 스트림이다)
- 파일이 아주 크다면 한 번에 모든 파일을 다 메모리에 올리는 것보다, 파일을 부분 부분 나누어 메모리에 올리는 것이 더 나은 선택일 수 있다.
- 예를 들어, 파일의 크기가 1000MB라면 한번에 1000MB의 파일이 메모리에 불러진다. 앞서, Files.readAllLines의 경우, List에 1000MB의 파일이 모두 올라간다.
- 이 기능을 사용하면 파일을 한 줄 단위로 메모리에 올릴 수 있다. 한 줄 당 1MB의 용량을 사용한다면 자바는 파일에서 한번에 1MB의 데이터만 메모리에 올려 처리한다. 그리고 처리가 끝나면 다음 줄을 호출하고, 기존에 사용한 1MB의 데이터는 GC한다.
- 용량이 아주 큰 파일을 처리해야 한다면, 이런 방식으로 나누어 처리하는 것이 효과적이다.
- 참고로 용량이 너무 커서 자바 메모리에 한번에 불러오는 것이 불가능할 수 있다. 그때는 이런 방식으로 처리해야만 한다.
- 물론, BufferedReader를 통해서도 한 줄 단위로 이렇게 나누어 처리하는 것이 가능하다. 여기서 핵심은 매우 편리하게 문자를 나누어 처리하는 것이 가능하다는 점이다.
파일 복사 최적화
이번에는 파일을 복사하는 효율적인 방법에 대해 알아보자. 예제를 위해서 200MB 임시 파일을 하나 만들어보자.
package cwchoiit.io.file.copy;
import java.io.FileOutputStream;
import java.io.IOException;
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[] bytes = new byte[FILE_SIZE];
fos.write(bytes);
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: 69ms
이렇게 만들어진 파일을 다른 파일로 복사하는 여러 가지 방법을 알아보자. 그리고 가장 효율적인 방법을 알아보자.
파일 복사 예제1
package cwchoiit.io.file.copy;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
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);
fos.close();
fis.close();
long endTime = System.currentTimeMillis();
System.out.println("Time taken : " + (endTime - startTime) + " ms");
}
}
실행 결과
Time taken: 109ms
- FileInputStream에서 readAllBytes를 통해, 한번에 모든 데이터를 읽고 write(bytes)를 통해 한번에 모든 데이터를 저장한다.
- 파일(copy.dat) → 자바(byte) → 파일(copy_new.dat)의 과정을 거친다.
- 자바가 copy.dat 파일의 데이터를 자바 프로세스가 사용하는 메모리에 불러온다. 그리고 메모리에 있는 데이터를 copy_new.dat에 전달한다.
파일 복사 예제2
package cwchoiit.io.file.copy;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
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_new.dat");
fis.transferTo(fos);
fos.close();
fis.close();
long endTime = System.currentTimeMillis();
System.out.println("Time taken : " + (endTime - startTime) + " ms");
}
}
실행 결과
Time taken: 70ms
- InputStream에는 transferTo()라는 특별한 메서드가 있다 (자바 9부터 제공)
- 이 메서드는 InputStream에서 읽은 데이터를 바로 OutputStream으로 출력한다.
- transferTo()는 성능 최적화가 되어 있기 때문에, 앞의 예제와 비슷하거나 조금 더 빠르다.
- 파일(copy.dat) → 자바(byte) → 파일(copy_new.dat)의 과정을 거친다.
- transferTo() 덕분에 파일을 읽고 바이트로 변환한 값을 다시 파일에 쓰는 코드를 생략할 수 있다.
파일 복사 예제3
package cwchoiit.io.file.copy;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
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");
}
}
실행 결과
Time taken: 37ms
- Files.copy() → 앞의 예제들은 파일을 복사할 때 다음 과정을 거쳤다.
- 파일(copy.dat) → 자바(byte) → 파일(copy_new.dat)의 과정을 거친다.
- 이 과정들은 파일의 데이터를 자바로 불러오고, 또 자바에서 읽은 데이터를 다시 파일에 전달해야 한다.
- Files.copy()는 자바에서 파일 데이터를 불러오지 않고, 운영체제의 파일 복사 기능을 사용한다. 따라서 자바로 데이터를 불러오는 과정이 필요가 없다. 그러므로 가장 빠르다.
정리를 하자면
파일을 다루어야 할 일이 있다면 항상 Files의 기능을 먼저 찾아보자. 파일을 읽거나 쓸 때도 스트림 대신 매우 편리하게 읽고 쓸 수 있고, 생성이나 복사 등 여러 방면에서 좋은 점이 많다. File은 예전 방식이고 이제는 먼저 고려할건 Files다.
'JAVA의 가장 기본이 되는 내용' 카테고리의 다른 글
네트워크 2 (Socket) (4) | 2024.10.16 |
---|---|
네트워크 1 (Socket) (2) | 2024.10.15 |
IO 활용 (2) | 2024.10.13 |
IO 기본 2 (0) | 2024.10.11 |
IO 기본 1 (0) | 2024.10.11 |