Java 입출력 | File, BufferedReader, NIO

Java 입출력 | File, BufferedReader, NIO

이 글의 핵심

BufferedReader·Files, NIO Channel·Buffer·Selector, 직렬화, 성능 팁, CSV·JSON·로그 실전까지 한 번에 정리합니다.

들어가며

java.io·java.nio 계열은 바이트·문자 스트림으로 파일과 네트워크를 다루는 출발점입니다. 읽기/쓰기 경로를 열고 닫는 책임을 try-with-resources로 묶는 패턴이 흔합니다.


1. 파일 읽기 (IO)

BufferedReader

텍스트 파일을 효율적으로 읽는 가장 일반적인 방법입니다:

import java.io.*;

public class FileReadExample {
    public static void main(String[] args) {
        // try-with-resources 구문: 자동으로 리소스를 닫아줌
        // () 안에 선언된 리소스는 try 블록이 끝나면 자동으로 close() 호출
        try (BufferedReader br = new BufferedReader(
                new FileReader("file.txt"))) {
            
            // BufferedReader: 내부 버퍼(보통 8KB)를 사용해 I/O 횟수 감소
            // FileReader를 감싸서 성능 향상
            
            String line;
            // readLine(): 한 줄씩 읽어옴 (줄바꿈 문자 제외)
            // 파일 끝에 도달하면 null 반환
            // (line = br.readLine()): 읽으면서 동시에 line 변수에 할당
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
            
        } catch (FileNotFoundException e) {
            // 파일이 존재하지 않을 때 발생
            System.out.println("파일을 찾을 수 없습니다");
        } catch (IOException e) {
            // 파일 읽기 중 발생하는 기타 입출력 오류
            System.out.println("파일 읽기 오류: " + e.getMessage());
        }
        // try 블록이 끝나면 br.close()가 자동 호출됨
    }
}

성능 비교:

  • FileReader 단독: 한 문자씩 읽음 → 느림
  • BufferedReader + FileReader: 버퍼 단위로 읽음 → 빠름 (10~100배)

FileReader vs BufferedReader

두 방식의 성능 차이를 명확히 이해해봅시다:

// FileReader: 한 문자씩 읽기 (느림)
try (FileReader fr = new FileReader("file.txt")) {
    int ch;
    // read(): 한 문자를 읽어서 int로 반환 (0~65535)
    // 파일 끝이면 -1 반환
    // 문제점: 파일에서 한 문자 읽을 때마다 디스크 I/O 발생
    while ((ch = fr.read()) != -1) {
        // int를 char로 캐스팅해서 출력
        System.out.print((char) ch);
    }
}
// 1000자 파일 → 1000번의 디스크 I/O 발생

// BufferedReader: 버퍼링 사용 (빠름)
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
    String line;
    // readLine(): 한 줄 전체를 읽어서 String으로 반환
    // 내부적으로 버퍼(보통 8KB)에 미리 읽어놓고 사용
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}
// 1000자 파일 → 약 1~2번의 디스크 I/O만 발생 (버퍼 크기에 따라)

성능 차이 예시:

  • 1MB 텍스트 파일 읽기
    • FileReader: 약 1,000,000번의 I/O → 수 초 소요
    • BufferedReader: 약 128번의 I/O (8KB 버퍼) → 0.1초 미만

결론: 파일 I/O는 항상 BufferedReader/BufferedWriter 사용 권장


2. 파일 쓰기 (IO)

BufferedWriter

import java.io.*;

public class FileWriteExample {
    public static void main(String[] args) {
        try (BufferedWriter bw = new BufferedWriter(
                new FileWriter("output.txt"))) {
            
            bw.write("첫 번째 줄");
            bw.newLine();
            bw.write("두 번째 줄");
            bw.newLine();
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

추가 모드

// 덮어쓰기 (기본)
try (BufferedWriter bw = new BufferedWriter(
        new FileWriter("output.txt"))) {
    bw.write("새 내용");
}

// 추가 모드
try (BufferedWriter bw = new BufferedWriter(
        new FileWriter("output.txt", true))) {
    bw.write("추가 내용");
}

3. NIO (Java 7+)

Files 클래스

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

public class NIOExample {
    public static void main(String[] args) throws IOException {
        Path path = Path.of("file.txt");
        
        // 파일 읽기 (전체)
        String content = Files.readString(path);
        System.out.println(content);
        
        // 파일 읽기 (줄 단위)
        List<String> lines = Files.readAllLines(path);
        for (String line : lines) {
            System.out.println(line);
        }
        
        // 파일 쓰기
        Files.writeString(Path.of("output.txt"), "Hello, NIO!");
        
        // 여러 줄 쓰기
        List<String> linesToWrite = List.of("라인 1", "라인 2", "라인 3");
        Files.write(Path.of("output.txt"), linesToWrite);
    }
}

파일 조작

import java.nio.file.*;

// 파일 존재 확인
boolean exists = Files.exists(Path.of("file.txt"));

// 파일 삭제
Files.deleteIfExists(Path.of("temp.txt"));

// 파일 복사
Files.copy(
    Path.of("source.txt"),
    Path.of("dest.txt"),
    StandardCopyOption.REPLACE_EXISTING
);

// 파일 이동
Files.move(
    Path.of("old.txt"),
    Path.of("new.txt"),
    StandardCopyOption.REPLACE_EXISTING
);

// 디렉토리 생성
Files.createDirectories(Path.of("dir/subdir"));

4. 실전 예제

예제: 로그 파일 처리

import java.io.*;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

public class LogProcessor {
    public static void processLog(String inputPath, String outputPath) {
        try {
            List<String> lines = Files.readAllLines(Path.of(inputPath));
            
            List<String> errors = lines.stream()
                .filter(line -> line.contains("ERROR"))
                .collect(Collectors.toList());
            
            Files.write(Path.of(outputPath), errors);
            
            System.out.println("에러 로그 " + errors.size() + "개 추출");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        processLog("app.log", "errors.log");
    }
}

정리

핵심 요약

  1. BufferedReader/Writer: 버퍼링으로 성능 향상
  2. try-with-resources: 자동 리소스 닫기
  3. NIO Files: 간결한 현대적 API
  4. Path: 파일 경로 표현
  5. 파일 조작: 복사, 이동, 삭제

다음 단계

  • Java 멀티스레딩
  • Java Spring Boot

관련 글

  • Java 시작하기 | JDK 설치부터 Hello World까지