Java 17 기준으로 Files/Path API와 try-with-resources를 활용해 안전하고 읽기 쉬운 파일 I/O를 구현하고, UTF-8 인코딩 이슈를 확실히 해결하는 방법을 정리합니다.
도입 (문제 상황): Java 파일 읽기가 왜 자꾸 깨질까요?
파일을 읽었는데 한글이 ???로 보이거나, 운영 서버에서만 줄바꿈이 이상하게 처리되는 경험 해보셨을 거예요. 게다가 스트림을 닫는 걸 깜빡해서 파일 핸들이 누수되거나, 예외 처리 코드가 본문보다 길어지는 경우도 흔합니다.
Java 17에서는 이런 문제를 **Files/Path(NIO.2)**와 try-with-resources, 그리고 명시적 인코딩 지정으로 꽤 우아하게 정리할 수 있어요.
핵심 개념: Files/Path + 명시적 Charset이 “현대적”인 이유

예전 File, FileInputStream, BufferedReader 조합도 여전히 동작하지만, 실무에서는 다음 이유로 Path와 Files 중심으로 바꾸는 편이 이득입니다.
- Path는 “경로”를 더 정확히 모델링합니다
File은 이름과 달리 “파일” 객체처럼 보이지만, 실제로는 경로 문자열에 가까워요. 반면Path는 운영체제별 경로 처리, 정규화(normalize), 결합(resolve) 같은 작업이 명확합니다. - Files는 자주 쓰는 작업을 고수준 API로 제공합니다
읽기/쓰기, 복사/이동, 디렉터리 순회 같은 작업을Files.readString,Files.writeString,Files.walk등으로 간결하게 처리할 수 있어요. 코드가 짧아질수록 예외/리소스 처리 실수도 줄어듭니다. - 인코딩은 “기본값에 기대면” 언젠가 터집니다
가장 흔한 장애 포인트가 여기입니다. 개발 PC는 UTF-8인데 운영 환경 기본 charset이 다르면(또는 반대) 같은 코드가 다른 결과를 만들어요.
따라서 파일 I/O에서는 항상StandardCharsets.UTF_8같은 charset을 명시하는 습관이 안전합니다.
아래 표는 실무에서 자주 비교하는 포인트를 요약한 것입니다.
| 항목 | java.io(전통) |
java.nio.file(Files/Path) |
|---|---|---|
| 경로 모델 | File(경로 + 일부 파일 기능 혼재) |
Path(경로에 집중, 조합/정규화 명확) |
| 간결성 | 보일러플레이트 많음 | readString/writeString/copy/move 등 고수준 API |
| 인코딩 처리 | InputStreamReader로 가능하지만 누락 잦음 |
readString(path, charset)처럼 자연스럽게 명시 |
| 대용량 처리 | 버퍼 스트림 직접 구성 | 스트림/채널/버퍼 선택 폭 넓음 |
| 권장 | 레거시 유지보수 | 신규 코드 기본 선택지 |
flowchart TD
A["Path 생성"] --> B["Files.newBufferedReader(UTF-8)"]
B --> C["라인 단위 처리"]
C --> D["try-with-resources로 자동 close"]
위 흐름은 “경로 → UTF-8 리더 → 처리 → 자동 자원 해제”의 가장 안전한 기본 패턴입니다.
코드 예제: Java 17에서 바로 쓰는 “안전한 파일 I/O” 템플릿
아래 코드는 그대로 복붙해서 실행할 수 있는 예제입니다.
Files.readString/writeString로 간단한 텍스트 파일 처리newBufferedReader/newBufferedWriter로 라인 단위 스트리밍 처리try-with-resources로 자원 누수 방지- 모든 텍스트 I/O는 UTF-8 명시
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
public class ModernFileIoDemo {
public static void main(String[] args) throws IOException {
Path baseDir = Path.of("demo-io");
Files.createDirectories(baseDir);
Path input = baseDir.resolve("input.txt");
Path output = baseDir.resolve("output.txt");
Path backup = baseDir.resolve("input.backup.txt");
// 1) 간단한 텍스트 파일 쓰기/읽기: readString/writeString + UTF-8 명시
Files.writeString(
input,
"안녕하세요\n" +
"파일 I/O는 인코딩이 생명입니다.\n" +
"timestamp=" + LocalDateTime.now() + "\n",
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
);
String content = Files.readString(input, StandardCharsets.UTF_8);
System.out.println("== readString 결과 ==");
System.out.println(content);
// 2) 안전한 백업(복사): REPLACE_EXISTING으로 덮어쓰기 명시
Files.copy(input, backup, StandardCopyOption.REPLACE_EXISTING);
// 3) 라인 단위 처리: 대용량일 때 readAllLines/readString 대신 스트리밍
List<String> processed = new ArrayList<>();
try (BufferedReader reader = Files.newBufferedReader(input, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
// 핵심 포인트: 처리 로직은 여기 집중, close는 try-with-resources가 처리
processed.add(line.toUpperCase());
}
}
// 4) 라인 단위 쓰기: newBufferedWriter + UTF-8 명시
try (BufferedWriter writer = Files.newBufferedWriter(
output,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
)) {
for (String line : processed) {
writer.write(line);
writer.newLine(); // 플랫폼별 줄바꿈 처리
}
}
System.out.println("== output.txt 생성 완료 ==");
System.out.println("output path: " + output.toAbsolutePath());
}
}
실무 팁
💡 실무에서는: “기본 charset”에 기대지 말고, 팀 규칙으로 UTF-8을 고정해 보세요
- 텍스트 파일 I/O는
StandardCharsets.UTF_8을 항상 명시하는 게 가장 안전합니다. - “내 PC에서는 되는데 서버에서 깨짐”의 상당수가 기본 charset 차이에서 나옵니다.
- 외부에서 들어오는 파일이 UTF-8이 아닐 수 있다면(예: EUC-KR/CP949), 입력 인코딩을 설정으로 분리하고, 내부 표준은 UTF-8로 통일하는 전략이 유지보수에 유리합니다.
💡 실무에서는: 대용량 파일에 readString/readAllLines를 무심코 쓰지 마세요
Files.readString,Files.readAllLines는 편하지만 전체를 메모리에 올립니다.- 로그/CSV처럼 커질 수 있는 파일은
newBufferedReader(또는Files.lines)로 스트리밍 처리하고, 필요하면 배치 단위로 flush하는 방식이 안정적입니다. - 파일 쓰기는
TRUNCATE_EXISTING/APPEND를 명시해서 “덮어쓰기인지 이어쓰기인지”를 코드로 드러내는 편이 좋습니다.
핵심 요약: Files/Path로 파일 I/O를 단순화하고, try-with-resources로 누수를 막아보세요.
핵심 요약: 텍스트 I/O는 기본값을 믿지 말고 UTF-8 같은 Charset을 항상 명시하는 게 안전합니다.
핵심 요약: 대용량 파일은 한 번에 읽지 말고 라인/스트림 기반으로 처리하는 습관이 중요합니다.
다음 글: [#25 스레드 기초와 동기화]