본문으로 건너뛰기
Advertisement

15.5 NIO와 Files API (Java 7~11+)

Java 7에서 도입된 NIO.2(New I/O 2)java.nio.file 패키지를 통해 파일과 디렉터리 작업을 훨씬 쉽고 강력하게 처리할 수 있게 해줍니다. 기존 java.io.File 클래스의 단점을 대부분 해결했습니다.

1. Path - 경로 표현

java.io.File 대신 java.nio.file.Path를 사용합니다.

import java.nio.file.*;

// 경로 생성
Path path1 = Path.of("data/config.txt"); // Java 11+
Path path2 = Paths.get("data", "config.txt"); // Java 7+
Path absolute = Path.of("/home/user/data.txt"); // 절대 경로

// 경로 조작
Path parent = path1.getParent(); // data
Path fileName= path1.getFileName(); // config.txt
String ext = path1.toString(); // data/config.txt
Path resolved= Path.of("data").resolve("config.txt"); // data/config.txt

// 경로 정규화 (../ 등 제거)
Path messy = Path.of("data/../logs/./app.log");
Path cleaned = messy.normalize(); // logs/app.log
Path absolute2= messy.toAbsolutePath(); // 절대 경로로

// 두 경로의 상대 경로
Path base = Path.of("/home/user");
Path target = Path.of("/home/user/docs/report.pdf");
Path rel = base.relativize(target); // docs/report.pdf

// 경로 비교
System.out.println(Path.of("a/b").startsWith("a")); // true
System.out.println(Path.of("a/b/c").endsWith("b/c")); // true

2. Files 유틸리티 클래스

java.nio.file.Files는 파일 작업의 모든 것을 정적 메서드로 제공합니다.

파일 읽기

Path path = Path.of("hello.txt");

// 전체 내용을 String으로 (Java 11+, 소파일에 적합)
String content = Files.readString(path);
System.out.println(content);

// UTF-8 외 인코딩
String eucKr = Files.readString(path, java.nio.charset.Charset.forName("EUC-KR"));

// 모든 줄을 List<String>으로 (Java 7+)
List<String> lines = Files.readAllLines(path);
lines.forEach(System.out::println);

// 모든 줄을 Stream으로 (지연 읽기, 대용량 파일에 적합)
try (Stream<String> lineStream = Files.lines(path)) {
lineStream
.filter(line -> !line.isBlank())
.map(String::trim)
.forEach(System.out::println);
}

// 바이트 배열로
byte[] bytes = Files.readAllBytes(path);

파일 쓰기

Path output = Path.of("output.txt");

// 문자열 쓰기 (Java 11+, 덮어쓰기)
Files.writeString(output, "Hello, NIO!\n두 번째 줄");

// 줄 리스트 쓰기 (Java 7+)
List<String> lines = List.of("첫 번째 줄", "두 번째 줄", "세 번째 줄");
Files.write(output, lines);

// 추가 모드 (APPEND)
Files.writeString(output, "\n추가 내용", StandardOpenOption.APPEND);

// 바이트 쓰기
byte[] data = "바이너리 데이터".getBytes();
Files.write(output, data);

파일/디렉터리 관리

Path source = Path.of("original.txt");
Path dest = Path.of("copy.txt");
Path dir = Path.of("new_directory");

// 복사 (덮어쓰기: REPLACE_EXISTING 옵션)
Files.copy(source, dest, StandardCopyOption.REPLACE_EXISTING);

// 이동 (=이름 변경도 가능)
Files.move(source, Path.of("renamed.txt"), StandardCopyOption.REPLACE_EXISTING);

// 디렉터리 생성
Files.createDirectory(dir); // 상위 디렉터리 없으면 에러
Files.createDirectories(Path.of("a/b/c")); // 중간 디렉터리도 모두 생성

// 삭제
Files.delete(dest); // 없으면 예외
Files.deleteIfExists(Path.of("maybe.txt")); // 없으면 무시

// 임시 파일/디렉터리
Path temp = Files.createTempFile("prefix_", "_suffix.txt");
Path tempDir = Files.createTempDirectory("myapp_");

파일 메타데이터 확인

Path p = Path.of("data.txt");

System.out.println(Files.exists(p)); // 존재 여부
System.out.println(Files.isFile(p)); // 파일인지
System.out.println(Files.isDirectory(p)); // 디렉터리인지
System.out.println(Files.isReadable(p)); // 읽기 가능 여부
System.out.println(Files.size(p)); // 파일 크기 (바이트)
System.out.println(Files.getLastModifiedTime(p)); // 수정 시각
System.out.println(Files.isHidden(p)); // 숨김 여부

3. 디렉터리 탐색

Files.list - 디렉터리 직속 항목 나열

Path dir = Path.of(".");

// 현재 디렉터리의 파일 목록
try (Stream<Path> entries = Files.list(dir)) {
entries
.filter(Files::isRegularFile) // 파일만
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}

Files.walk - 재귀 탐색

Path root = Path.of("src");

// 모든 .java 파일 탐색 (깊이 제한 없음)
try (Stream<Path> walk = Files.walk(root)) {
walk
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}

// 깊이 제한
try (Stream<Path> walk = Files.walk(root, 2)) { // 최대 2 단계 깊이
walk.forEach(System.out::println);
}

Files.find - 조건으로 탐색

try (Stream<Path> found = Files.find(
Path.of("src"),
Integer.MAX_VALUE, // 최대 깊이
(path, attrs) -> attrs.isRegularFile() && path.toString().endsWith(".java")
)) {
found.forEach(System.out::println);
}

4. 실전 예제: 파일 처리 유틸리티

import java.nio.file.*;
import java.io.IOException;
import java.util.*;
import java.util.stream.*;

public class FileUtils {

// 디렉터리의 모든 파일 크기 합계 계산
public static long totalSize(Path dir) throws IOException {
try (Stream<Path> walk = Files.walk(dir)) {
return walk
.filter(Files::isRegularFile)
.mapToLong(p -> {
try { return Files.size(p); }
catch (IOException e) { return 0L; }
})
.sum();
}
}

// 확장자별 파일 개수
public static Map<String, Long> countByExtension(Path dir) throws IOException {
try (Stream<Path> walk = Files.walk(dir)) {
return walk
.filter(Files::isRegularFile)
.collect(Collectors.groupingBy(
p -> {
String name = p.getFileName().toString();
int dot = name.lastIndexOf('.');
return dot == -1 ? "(없음)" : name.substring(dot);
},
Collectors.counting()
));
}
}

// 특정 단어를 포함하는 파일 찾기
public static List<Path> findContaining(Path dir, String keyword) throws IOException {
try (Stream<Path> walk = Files.walk(dir)) {
return walk
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".txt"))
.filter(p -> {
try {
return Files.readString(p).contains(keyword);
} catch (IOException e) { return false; }
})
.collect(Collectors.toList());
}
}
}

고수 팁

try-with-resources로 스트림 닫기: Files.list(), Files.walk(), Files.lines()는 모두 Stream을 반환하고, 이 스트림들은 파일 핸들을 보유합니다. 반드시 try-with-resources로 닫아야 파일 핸들 누수를 방지할 수 있습니다.

// ❌ 위험 (스트림이 닫히지 않을 수 있음)
Files.walk(dir).forEach(System.out::println);

// ✅ 안전 (자동으로 닫힘)
try (Stream<Path> s = Files.walk(dir)) {
s.forEach(System.out::println);
}

Path.of() vs Paths.get(): Java 11+에서는 Path.of()를 사용하세요. 더 간결하고 Path 인터페이스의 정적 팩토리 메서드입니다. 두 기능은 동일합니다.

Advertisement