CodeGym /행동 /JAVA 25 SELF /BufferedReader, BufferedWriter: 버퍼링과 장점

BufferedReader, BufferedWriter: 버퍼링과 장점

JAVA 25 SELF
레벨 36 , 레슨 0
사용 가능

1. 소개

FileReaderFileWriter가 어떻게 동작하는지 떠올려 봅시다. 이들에서 read()write()를 호출할 때마다 파일 시스템에 접근합니다 — 즉, 컴퓨터가 실제로 디스크에 가서 문자를 하나 읽거나 씁니다. 파일이 큰데 한 번에 한 문자씩 읽거나 쓰면, 디스크에 대한 접근이 수천, 수백만 번으로 늘어나 장거리 마라톤이 됩니다. 그리고 디스크는, 잘 알다시피, 프로그래머의 가장 빠른 친구가 아닙니다.

이렇게 비유할 수 있습니다: 양동이의 물을 병으로 옮겨야 하는데, 티스푼으로 뜨는 것과 같습니다. 형식적으로는 됩니다. 하지만 엄청 오래 걸리죠. 합리적으로는 국자나 컵을 쓰는 게 좋습니다. 파일도 마찬가지입니다. 한 문자씩 읽고 쓰는 건 물을 숟가락으로 나르는 것과 같습니다.

구체적으로는 다음과 같습니다:

// 파일을 문자 단위로 읽기(일명 "티스푼 방식"!)
try (FileReader reader = new FileReader("big.txt")) {
    int c;
    while ((c = reader.read()) != -1) {
        // 문자 처리(예: 개수만 세기)
    }
}

파일이 크면 프로그램이 매우 느리게 동작함을 금방 느끼게 됩니다.

버퍼링: 무엇이며 왜 필요한가

버퍼는 (보통 배열인) 메모리의 특별한 영역으로, 데이터를 한 문자씩이 아니라 한 번에 큰 덩어리(예: 8 KB 이상)로 불러오거나 기록합니다. 쉽게 말해, 버퍼는 일종의 중간 “양동이” 같은 메모리 공간으로, 데이터를 한 문자씩이 아니라 덩어리로 모아 읽고 씁니다.

작동 방식은 이렇습니다. 읽을 때는 프로그램이 디스크에 한 번 접근해서 버퍼에 데이터 블록을 통째로 담아 오고, 이후에는 메모리에서 한 문자씩 꺼내 제공합니다. 블록이 끝나면 다음 블록을 가져옵니다. 쓸 때도 마찬가지로, 먼저 데이터를 버퍼에 쌓아 두었다가 버퍼가 가득 차거나 스트림을 닫을 때(또는 flush()를 호출하면 즉시) 한 번에 디스크로 보냅니다.

왜 더 빠를까요? 디스크 자체가 느린데, 자잘하게 자주 건드리면 특히 더 느립니다. 반대로, 접근 횟수는 줄이고 한 번에 많이 가져오면 훨씬 빨라집니다. 한마디로, 버퍼링은 디스크 접근 횟수를 줄여 프로그램을 가속합니다.

2. BufferedReader와 BufferedWriter: 문법과 예시

Java에서 텍스트 파일의 읽기/쓰기를 버퍼링하려면 두 클래스를 사용합니다:

  • BufferedReader — 텍스트 파일 읽기용.
  • BufferedWriter — 텍스트 파일 쓰기용.

이들은 일반 Reader/Writer(예: FileReader/FileWriter) 위에서 동작하며, 버퍼링을 추가합니다.

BufferedReader로 파일을 한 줄씩 읽기

가장 흔한 시나리오는 파일을 줄 단위로 읽는 것입니다. readLine() 메서드는 새 줄 문자("\n" 또는 "\r\n") 전까지의 문자열을 반환합니다.

import java.io.*;

public class BufferedReaderExample {
    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line); // 한 줄을 화면에 출력
            }
        } catch (IOException e) {
            System.out.println("파일 읽기 오류: " + e.getMessage());
        }
    }
}

여기서 무슨 일이 일어날까요?

우리는 문자를 하나씩 읽을 수 있는 FileReader를 만들고, 이를 BufferedReader로 감쌉니다. 버퍼링된 리더는 데이터를 한 문자씩이 아니라 큰 덩어리로 가져와 메모리에 쌓아 두고, readLine()으로 한 줄씩 꺼내줍니다. 결국 단순히 while 루프를 돌며 줄을 하나씩 받기만 하면 되고, 파일 크기에 크게 신경 쓰지 않아도 됩니다. 읽기는 빠르고 자원도 절약됩니다.

BufferedWriter로 파일 쓰기

문자열을 파일에 효율적으로 기록할 수도 있습니다:

import java.io.*;

public class BufferedWriterExample {
    public static void main(String[] args) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
            writer.write("Hello, world!");
            writer.newLine(); // 줄바꿈(운영체제에 따라 다름)
            writer.write("두 번째 줄입니다.");
        } catch (IOException e) {
            System.out.println("파일 쓰기 오류: " + e.getMessage());
        }
    }
}

여기서 무슨 일이 일어날까요?

먼저 FileWriter를 만든 다음, 이를 BufferedWriter로 감쌉니다. write()newLine()을 호출해도 데이터가 곧바로 디스크로 가지는 않습니다. 버퍼라는 중간 메모리에 먼저 쌓입니다. 버퍼가 가득 차거나 스트림을 닫을 때(또는 flush()를 명시적으로 호출할 때) 누적된 텍스트가 한 번에 파일로 기록됩니다. 이런 방식은 기록 속도를 크게 높이고 디스크 접근을 줄입니다.

하나의 애플리케이션에서 어떻게 보일까?

파일에 기록을 저장하고 화면에 출력하는 간단한 “일기장” 프로그램을 작성한다고 가정해 보겠습니다.

import java.io.*;
import java.util.Scanner;

public class DiaryApp {
    public static void main(String[] args) {
        String fileName = "diary.txt";
        Scanner scanner = new Scanner(System.in);

        // 새 항목 저장
        System.out.print("일기 새 항목을 입력하세요: ");
        String entry = scanner.nextLine();

        try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, true))) {
            writer.write(entry);
            writer.newLine();
            System.out.println("항목이 저장되었습니다!");
        } catch (IOException e) {
            System.out.println("쓰기 오류: " + e.getMessage());
        }

        // 모든 항목 읽기
        System.out.println("\n일기:");
        try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.out.println("읽기 오류: " + e.getMessage());
        }
    }
}

우리는 파일 이름 "diary.txt"를 사용해, 사용자에게 새 기록을 입력받습니다. 저장에는 FileWriterappend 모드(true 전달)를 사용하므로 기존 내용은 지워지지 않고, 새 줄이 파일 끝에 차곡차곡 추가됩니다. BufferedWriter로 감싸면 기록이 빠르고 효율적입니다. 데이터는 먼저 메모리에 모였다가 한 번에 디스크로 기록됩니다.

그 다음 같은 파일을 BufferedReader로 열어 읽습니다. 내부적으로는 큰 블록으로 읽어 오되, 우리에게는 줄 단위로 제공합니다. 루프에서 화면에 그대로 출력하면, 처음부터 끝까지 “일기장” 전체를 확인할 수 있습니다.

3. BufferedReader와 BufferedWriter의 장점

눈에 띄는 성능 향상

버퍼링된 스트림으로 파일을 읽고 쓰면, 디스크 접근 횟수가 수십 배, 때로는 수백 배까지 줄어듭니다. 큰 파일일수록 효과가 뚜렷합니다.

편리한 메서드

  • BufferedReader.readLine() — 파일을 줄 단위로 읽을 수 있어 텍스트 파일(예: 로그, CSV, 구성 파일) 처리에 매우 편리합니다.
  • BufferedWriter.newLine() — 운영체제에 맞는 줄바꿈 문자를 알맞게 삽입합니다.

사용이 간단함

  • 다른 스트림과 쉽게 조합할 수 있습니다(예: 다양한 인코딩을 읽기 위해 InputStreamReaderBufferedReader 안에 감싸기).
  • try-with-resources를 사용하면 모든 리소스가 자동으로 닫힙니다.

유연성

  • 기본 버퍼 크기(보통 8 KB)가 맞지 않으면 명시적으로 버퍼 크기를 지정할 수 있습니다:
BufferedReader reader = new BufferedReader(new FileReader("big.txt"), 16384); // 16 KB 버퍼

4. BufferedReader와 BufferedWriter를 언제 사용할까

다음 상황에서 사용하세요:

  • 텍스트 파일(로그, CSV, 대용량 텍스트 데이터)을 다룰 때.
  • 파일을 줄 단위로 읽거나 써야 할 때.
  • 큰 파일에서 성능이 중요한 경우.
  • Reader/Writer를 지원하는 네트워크·기타 소스의 데이터 스트림을 처리할 때.

다음 상황에서는 사용하지 마세요:

  • 바이너리 파일(예: 이미지, 아카이브, 비디오)을 다룰 때 — 이 경우 InputStream/OutputStream을 사용하세요.
  • 파일이 아주 작고 한 번에 통째로 읽거나 쓸 때 — 버퍼링의 이점은 미미합니다(해가 되는 것도 아닙니다).

5. 유용한 팁

다양한 인코딩과의 조합

특정 인코딩(예: "UTF-8", "Windows-1251")으로 파일을 읽거나 써야 한다면, 버퍼링된 스트림과 함께 InputStreamReader/OutputStreamWriter를 사용하세요:

BufferedReader reader = new BufferedReader(
    new InputStreamReader(new FileInputStream("input.txt"), "UTF-8")
);

BufferedWriter writer = new BufferedWriter(
    new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")
);

버퍼를 명시적으로 비우기

데이터가 반드시 디스크에 기록되어야 하는 순간(예: 로그, 영수증 출력)이 있다면 writer.flush()를 호출하세요. 보통은 스트림을 닫을 때 자동으로 버퍼가 비워집니다.

버퍼 크기

기본 버퍼는 약 8 KB입니다. 특정 상황(예: 초대형 파일 처리)에서 성능 향상이 확실하다면 다른 크기를 지정할 수 있습니다.

비교: FileReader/FileWriter vs BufferedReader/BufferedWriter

클래스 대용량 파일에서의 속도 줄 단위 읽기 편의성 줄 단위 쓰기 편의성 인코딩 유연성
FileReader/FileWriter 느림 아니요(문자 단위만 가능) 아니요(문자 단위만 가능) 기본값만 지원
BufferedReader/Writer 빠름 예 (readLine()) 예 (newLine()) 예 (InputStreamReader/OutputStreamWriter 사용)

6. BufferedReader/BufferedWriter 사용 시 흔한 실수

오류 №1: 스트림을 닫지 않음. try-with-resources를 사용하지 않거나 close()를 호출하지 않으면, 파일이 잠긴 채로 남거나 데이터가 디스크에 기록되지 않을 수 있습니다. 항상 try-with-resources를 사용하세요!

오류 №2: 텍스트 파일과 바이너리 파일을 혼동. 바이너리 파일(".jpg", ".zip")을 BufferedReader로 열면 문자가 깨지고 오류가 발생할 수 있습니다. 바이너리 파일은 InputStream/OutputStream을 사용하세요.

오류 №3: 대용량 데이터에서 버퍼링을 사용하지 않음. 문자를 하나씩 읽거나 쓰면 프로그램이 느려집니다. 큰 파일에는 반드시 버퍼링을 사용하세요.

오류 №4: 필요한 경우 flush()를 호출하지 않음. 데이터가 즉시 디스크에 기록되어야 한다면(예: 로깅) writer.flush()를 호출하세요. 보통은 스트림을 닫으면 자동으로 처리됩니다.

오류 №5: 인코딩을 고려하지 않음. 잘못된 인코딩으로 파일을 열면 텍스트가 깨져 보일 수 있습니다(문자가 ‘?’로 보이거나 이상한 기호로 표시됨). 시스템 기본 인코딩이 아닌 경우에는 항상 필요한 인코딩을 명시하세요(예: "UTF-8").

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION