JAVA

Java 파일 I/O 현대적으로 하기: Files/Path, try-with-resources, 인코딩까지 깔끔하게

IT Lab 2026. 2. 24. 10:00

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이 “현대적”인 이유

Path와 Files 기반 파일 읽기/쓰기 흐름도(인코딩 명시와 자동 자원 해제 강조)

예전 File, FileInputStream, BufferedReader 조합도 여전히 동작하지만, 실무에서는 다음 이유로 PathFiles 중심으로 바꾸는 편이 이득입니다.

  1. Path는 “경로”를 더 정확히 모델링합니다
    File은 이름과 달리 “파일” 객체처럼 보이지만, 실제로는 경로 문자열에 가까워요. 반면 Path는 운영체제별 경로 처리, 정규화(normalize), 결합(resolve) 같은 작업이 명확합니다.
  2. Files는 자주 쓰는 작업을 고수준 API로 제공합니다
    읽기/쓰기, 복사/이동, 디렉터리 순회 같은 작업을 Files.readString, Files.writeString, Files.walk 등으로 간결하게 처리할 수 있어요. 코드가 짧아질수록 예외/리소스 처리 실수도 줄어듭니다.
  3. 인코딩은 “기본값에 기대면” 언젠가 터집니다
    가장 흔한 장애 포인트가 여기입니다. 개발 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 스레드 기초와 동기화]