본문 바로가기
Language/Java

[JAVA] 버퍼(Buffered) 스트림을 사용하는 이유 : 입출력 성능의 비약적 향상

by Papa Martino V 2026. 1. 20.
728x90

버퍼(Buffered) 스트림
버퍼(Buffered) 스트림

 

자바 프로그래밍에서 파일 시스템이나 네트워크를 통해 데이터를 주고받을 때, 성능 최적화의 핵심으로 꼽히는 것이 바로 버퍼(Buffered) 스트림입니다. 단순히 FileInputStream이나 FileReader를 사용하는 것보다 왜 BufferedInputStream이나 BufferedReader를 권장하는지, 그 내부 메커니즘과 실무적인 가치를 심도 있게 분석해 보겠습니다.

1. 버퍼(Buffer)란 무엇인가?

버퍼는 데이터를 한 곳에서 다른 곳으로 전송하는 동안 일시적으로 그 데이터를 보관하는 메모리 영역을 의미합니다. 자바의 입출력 작업에서 버퍼는 '바구니'와 같은 역할을 합니다. 물 한 방울씩을 옮기기 위해 매번 수도가와 거실을 왕복하는 대신, 커다란 양동이(버퍼)에 물을 가득 채워 한 번에 옮기는 것이 훨씬 효율적인 것과 같은 이치입니다.

2. 버퍼 스트림을 사용하는 결정적인 이유: '시스템 콜'의 최소화

기본 스트림(예: FileInputStream)은 read() 메서드가 호출될 때마다 운영체제의 OS API를 호출합니다. 이를 시스템 콜(System Call)이라고 합니다. 하드디스크나 SSD 같은 물리적 장치에 접근하는 작업은 CPU 연산에 비해 압도적으로 느린 'I/O 바운드' 작업입니다.

  • 기본 스트림: 1바이트를 읽을 때마다 하드디스크에 접근하여 데이터를 가져옵니다. 만약 1MB 파일을 읽는다면 100만 번의 시스템 콜이 발생합니다.
  • 버퍼 스트림: 내부적으로 커다란 메모리 버퍼(기본 8KB)를 유지합니다. 첫 read() 호출 시 디스크에서 8KB를 통째로 읽어 메모리에 저장한 뒤, 이후 요청은 디스크 접근 없이 메모리에서 즉시 반환합니다. 결과적으로 시스템 콜 횟수를 수천 분의 일로 줄여줍니다.

3. 성능 비교 및 데이터 처리 단위

버퍼 스트림은 단순히 속도만 빠른 것이 아니라, 데이터를 처리하는 '편의성'도 제공합니다. 대표적인 문자 기반 버퍼 스트림인 BufferedReader는 줄(Line) 단위로 읽을 수 있는 readLine() 메서드를 제공하여 텍스트 데이터 가공을 매우 용이하게 만듭니다.

구분 기본 스트림 (Non-Buffered) 버퍼 스트림 (Buffered)
입출력 방식 직접 하드웨어/네트워크 접근 메모리 버퍼를 경유
시스템 콜 횟수 매우 많음 (데이터 단위당 1회) 적음 (버퍼가 빌 때만 호출)
처리 속도 느림 (물리 장치 속도에 의존) 매우 빠름 (메모리 접근 속도)
CPU 점유율 I/O 대기 시간으로 인한 비효율 효율적인 리소스 활용 가능

4. 실전 코드 예제 (Sample Example)

다음은 대용량 파일을 읽을 때 권장되는 BufferedReader 사용법입니다. 현대적인 자바에서는 try-with-resources 구문과 함께 사용하는 것이 표준입니다.


import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class FastFileReader {
    public static void main(String[] args) {
        String filePath = "large_data_log.txt";

        // BufferedReader를 사용하여 성능 최적화
        try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
            String line;
            while ((line = br.readLine()) != null) {
                // 한 줄 단위로 처리 (버퍼 덕분에 매우 빠름)
                processLog(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void processLog(String log) {
        // 비즈니스 로직 처리
    }
}

5. 주의사항: 버퍼 비우기 (Flushing)

출력 스트림인 BufferedOutputStream이나 BufferedWriter를 사용할 때는 주의가 필요합니다. 데이터가 버퍼에 가득 차지 않으면 목적지로 전송되지 않고 버퍼에 머물러 있을 수 있습니다. 따라서 작업이 끝나면 반드시 close()를 호출하거나 명시적으로 flush()를 호출하여 버퍼에 남은 데이터를 강제로 밀어내야 합니다.

6. 결론

Java의 버퍼 스트림은 선택이 아닌 필수입니다. 특히 파일 업로드/다운로드, 대용량 로그 분석, 네트워크 소켓 통신과 같은 작업에서 버퍼 스트림을 적용하지 않는 것은 고성능 서버를 포기하는 것과 같습니다. 메모리라는 가용 자원을 영리하게 활용하여 물리적 한계를 극복하는 입출력 설계의 기본을 항상 기억하시기 바랍니다.


콘텐츠 출처 및 참고 문헌

  • Oracle Java Documentation: Buffered Streams (https://docs.oracle.com/javase/tutorial/essential/io/buffers.html)
  • Computer Architecture: A Quantitative Approach (Hennessy and Patterson) - I/O Performance Chapter
  • Effective Java 3rd Edition (Joshua Bloch)
728x90