1. Java가 파일을 다루게 된 과정
Java와 파일: 시작은 어디였나
한때, 1996년에 언어 개발자들은 파일 작업의 첫 접근 — 패키지 java.io를 제안했습니다. 그 안에 File, FileInputStream, FileOutputStream, Reader, Writer 등의 클래스가 등장했죠. 이 클래스들은 파일이 존재하는지, 파일 크기와 수정 날짜를 확인하고, 디렉터리를 다루는 등 다양한 작업을 가능하게 했습니다.
하지만 초창기 버전이 늘 그렇듯 완벽하진 않았습니다 — 더 편하고 안전하게 만들 수 있는 부분이 많았습니다.
오래된 API (java.io)의 한계
- File — 실제 파일이 아니라 “바로가기(핸들)”에 가깝다. File 클래스는 내용 읽기나 쓰기를 할 수 없습니다. 경로와 메타데이터를 설명할 뿐이죠. 읽기/쓰기를 하려면 별도의 스트림인 FileInputStream/FileOutputStream 또는 Reader/Writer가 필요합니다.
- 경로 처리 — 고통. "C:\\Users\\" + user + "\\Documents" 같이 수동으로 경로를 이어 붙이면 Windows와 Linux 간에 옮길 때 자주 깨졌습니다.
- 심볼릭 링크 미지원. 오래된 API는 심링크를 이해하지 못해 올바르지 않게 동작할 수 있었습니다.
- 속성 지원이 약함. 접근 권한, 소유자, “숨김” 플래그 등을 얻기가 어려웠습니다.
- 대용량 파일에 비효율적. 클래식 스트림은 대량 데이터에 대한 편리하고 빠른 시나리오를 제공하지 않았고, 비동기 I/O도 없었습니다.
java.nio.file의 등장 (NIO.2, Java 7)
Java 7에서 새로운 패키지인 java.nio.file(흔히 NIO.2)이 등장했습니다. 이것이 가져온 것은:
- Path — 현대적인 경로 추상화( ZIP, 클라우드, 네트워크 파일 시스템 내부 경로까지 포함 ).
- Files — 읽기, 쓰기, 복사, 삭제를 위한 정적 유틸리티.
- FileSystem — 로컬 디스크뿐 아니라 다른 파일 시스템까지 다룸.
- 심볼릭 링크, 확장 속성, 접근 권한 지원.
- 비동기 I/O 및 대용량 데이터에서의 개선된 성능.
- 디렉터리 처리와 스트림 API와의 편리한 연계.
NIO는 “New Input/Output”의 약자이며, 등장한 지 10년도 넘었지만 Java에서는 여전히 현대 표준입니다.
2. 접근 방식 비교: java.io vs java.nio.file
File 클래스 (java.io)
- 파일 또는 디렉터리의 경로와 그 메타데이터를 나타냅니다.
- 파일의 내용을 읽거나 쓸 줄 모릅니다.
- 존재 여부, 크기, 유형(파일/디렉터리), 절대 경로 등을 조회할 수 있습니다.
import java.io.File;
public class ExampleIO {
public static void main(String[] args) {
File file = new File("example.txt");
if (file.exists()) {
System.out.println("파일이 존재합니다!");
System.out.println("크기: " + file.length() + " 바이트");
}
}
}
Path 클래스 (java.nio.file)
- 현대적인 경로 추상화로, 자연스럽게 결합하고 정규화할 수 있습니다.
- 로컬, 아카이브, 네트워크 경로를 모두 표현할 수 있습니다.
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class ExampleNIOPath {
public static void main(String[] args) throws Exception {
Path path = Paths.get("example.txt");
if (Files.exists(path)) {
System.out.println("파일을 찾았습니다!");
System.out.println("크기: " + Files.size(path) + " 바이트");
}
}
}
Files 클래스 (java.nio.file)
- 전체/부분 읽기·쓰기를 위한 정적 메서드 모음.
- 파일 복사, 삭제, 이동.
- 디렉터리 생성/삭제.
- 확장 속성 접근.
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class ExampleNIOFiles {
public static void main(String[] args) throws Exception {
Path path = Paths.get("example.txt");
List<String> lines = Files.readAllLines(path);
for (String line : lines) {
System.out.println(line);
}
}
}
비교 표
| 기능 | |
|
|---|---|---|
| 경로 표현 | 예 | 예 |
| 내용 읽기/쓰기 | 아니오 | 예 (Files 사용) |
| 경로 작업 | 불편함 | 편리함 (resolve, normalize) |
| 심볼릭 링크 | 아니오 | 예 |
| 파일 속성 | 제한적 | 확장됨 |
| 비동기 I/O | 아니오 | 예 (NIO.2) |
| 크로스 플랫폼 | 부분적 | 탁월함 |
3. 언제 무엇을 사용할까?
오래된 API (java.io): 언제 필요할까?
- 레거시 코드 지원용. 프로젝트가 Java 7 이전에 시작되어 곳곳에서 File, FileInputStream, FileOutputStream을 사용한다면 호환성을 유지해야 할 수도 있습니다.
- 새 프로젝트에서는 — 권장하지 않음. 오늘날 ‘노장’ File을 쓰는 것은 VHS 테이프로 영상을 보는 것과 같습니다.
새 API (java.nio.file): 현대 표준
- 새 프로젝트에서는 항상 Path와 Files를 우선하세요.
- 더 간단하고, 안전하며, 스트림·람다·try-with-resources와의 통합이 뛰어납니다.
간단한 요약
- 읽기, 쓰기, 복사, 삭제? — Files로.
- 경로 작업(결합, 정규화)? — Path로.
- 크기, 날짜, 권한? — Files.getAttribute() 및 관련 메서드.
- 디렉터리 순회, 재귀? — Files.walk, Files.list.
4. 예제: 오래된 API와 새 API의 코드 비교
예제 1: 파일 존재 여부 확인
예전 방식:
import java.io.File;
File file = new File("data.txt");
if (file.exists()) {
System.out.println("파일을 찾았습니다!");
}
새로운 방식:
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Path path = Paths.get("data.txt");
if (Files.exists(path)) {
System.out.println("파일을 찾았습니다!");
}
예제 2: 파일의 모든 줄 읽기
예전 방식:
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
File file = new File("data.txt");
BufferedReader reader = new BufferedReader(new FileReader(file));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
새로운 방식:
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
Path path = Paths.get("data.txt");
List<String> lines = Files.readAllLines(path);
for (String line : lines) {
System.out.println(line);
}
설명: 새로운 방식이 더 짧고 안전합니다. try-with-resources를 사용하면 자원 관리가 더욱 쉬워집니다.
예제 3: 문자열을 파일에 쓰기
예전 방식:
import java.io.FileWriter;
FileWriter writer = new FileWriter("output.txt");
writer.write("안녕, 파일!");
writer.close();
새로운 방식:
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Path path = Paths.get("output.txt");
Files.write(path, "안녕, 파일!".getBytes());
5. 유용한 포인트
왜 Java는 전면 개편을 했을까?
- 안전성과 신뢰성. 자원 닫기와 경로 처리에서 오류가 줄어듭니다.
- 크로스 플랫폼. Windows, Linux, 심지어 ZIP 아카이브까지 동일한 클래스로 다룹니다.
- 확장 용이성. 클라우드와 네트워크 파일 시스템 지원을 추가하기 쉬워집니다.
- 성능. 대용량 파일에서 더 좋은 동작과 비동기 작업.
- 현대 Java 스택과의 호환성. 람다, 스트림, try-with-resources.
오래된 API에서 새 API로 전환하려면?
대부분의 경우 File을 Path로, 그 반대로도 변환할 수 있습니다:
File file = new File("data.txt");
Path path = file.toPath();
File file2 = path.toFile();
오래된 코드를 마주치면 무엇을 할까?
- 겁먹지 마세요: 더 간단하고 현대적으로 만들 수 있습니다.
- 가능하다면 java.nio.file로 다시 작성하세요.
- 어렵다면 브리지(toPath(), toFile())를 사용해 점진적으로 마이그레이션하세요.
6. 마지막 예: 미니 애플리케이션
예: 파일이 존재하는지 확인하고 그 크기를 출력합니다.
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileInfo {
public static void main(String[] args) {
Path path = Paths.get("test.txt");
if (Files.exists(path)) {
try {
long size = Files.size(path);
System.out.println("파일을 찾았습니다. 크기: " + size + " 바이트");
} catch (Exception e) {
System.out.println("파일 크기를 가져오는 중 오류: " + e.getMessage());
}
} else {
System.out.println("파일을 찾을 수 없습니다!");
}
}
}
여기서 사용한 것:
- Path — 경로의 표현.
- Files.exists() — 존재 여부 확인.
- Files.size() — 크기 얻기.
7. 파일 API 작업 시 흔한 실수
오류 №1: File이 내용 읽기나 쓰기를 할 수 있다고 기대함. 실제로 File은 “바로가기”일 뿐입니다. 읽기/쓰기를 위해서는 FileInputStream/FileOutputStream(오래된 API) 또는 Files(새 API) 유틸리티를 사용하세요.
오류 №2: + 또는 슬래시로 경로를 수동으로 이어 붙임. 이는 운영체제마다 오류를 일으킵니다. Path.resolve() 또는 여러 인자를 받는 Paths.get(...)을 사용하세요.
오류 №3: 스트림을 닫지 않음. 오래된 API에서는 자원 누수의 흔한 원인입니다. 새 API에서 많은 작업은 Files를 통해 스트림을 만들지 않으며, 스트림이 필요한 경우엔 try-with-resources를 사용하세요.
오류 №4: 새 프로젝트에서 오래된 API를 사용함. 새 프로젝트를 시작한다면 java.nio.file을 선택하세요. java.io는 레거시 코드와의 호환이 필요한 경우에만 의미가 있습니다.
GO TO FULL VERSION