본문 바로가기

Language/Java

[Java][BufferedReader] 완벽 정리 - 왜 Scanner 대신 쓸까?

알고리즘 문제를 풀다 보면 "시간 초과"를 만나는 순간이 옵니다. 분명 로직은 맞는 것 같은데... 이럴 때 Scanner를 BufferedReader로 바꾸는 것만으로 해결되는 경우가 꽤 많습니다.

이번 글에서는 BufferedReader가 뭔지, 왜 빠른지, 어떻게 쓰는지 알아보겠습니다.


1. 기본 사용법

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        String line = br.readLine();  // 한 줄 입력
        int num = Integer.parseInt(br.readLine());  // 숫자 입력

        br.close();
    }
}

처음 보면 복잡해 보이지만, 구조를 이해하면 간단합니다.


2. 왜 InputStreamReader를 BufferedReader로 감쌀까?

System.in → InputStreamReader → BufferedReader

각 단계가 하는 일을 살펴보면:

클래스 역할
System.in 키보드에서 바이트(byte) 데이터를 받음
InputStreamReader 바이트 → 문자(char)로 변환
BufferedReader 버퍼링으로 성능 최적화 + readLine() 제공

바이트와 문자의 차이

키보드에서 "가" 입력

System.in이 받는 것: [0xEA, 0xB0, 0x80]  ← 바이트 3개 (UTF-8)
InputStreamReader 변환 후: '가'           ← 문자 1개

한글은 UTF-8 기준 3바이트로 표현됩니다. InputStreamReader가 이 바이트들을 조합해서 우리가 쓸 수 있는 문자로 만들어주는 겁니다.

💡 여기서 말하는 "바이트"는 JVM 바이트코드와 전혀 다른 개념입니다. 단순히 8비트 데이터 단위를 의미합니다.


3. BufferedReader가 빠른 이유

핵심은 버퍼링입니다.

버퍼 없이 읽으면?

// InputStreamReader만 사용
InputStreamReader isr = new InputStreamReader(System.in);
isr.read();  // 시스템 콜 1번
isr.read();  // 시스템 콜 1번
isr.read();  // 시스템 콜 1번
// 100번 읽으면 시스템 콜 100번

시스템 콜은 User Mode → Kernel Mode 전환이 필요해서 비용이 큽니다.

버퍼로 읽으면?

// BufferedReader 사용
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
br.readLine();  // 내부적으로 8KB를 한 번에 읽어서 버퍼에 저장
br.readLine();  // 버퍼에서 꺼냄 (시스템 콜 없음)
br.readLine();  // 버퍼에서 꺼냄 (시스템 콜 없음)
// 버퍼가 빌 때까지 시스템 콜 없이 읽기 가능

비유하자면

  • 버퍼 없이: 편의점 갈 때마다 물 한 병씩 사오기
  • 버퍼 사용: 한 번에 박스째 사다 놓고 꺼내 마시기

4. read() vs readLine()

BufferedReader는 두 가지 읽기 방식을 제공합니다.

read() - 문자 하나씩

int ch = br.read();   // 문자 하나를 int로 반환
char c = (char) ch;   // char로 변환해서 사용

// 스트림 끝이면 -1 반환

readLine() - 한 줄씩

String line = br.readLine();  // 개행 전까지 한 줄을 String으로 반환

// 스트림 끝이면 null 반환

입력이 "Hello\n"일 때 비교

// read()로 읽으면
br.read();  // 'H' (72)
br.read();  // 'e' (101)
br.read();  // 'l' (108)
br.read();  // 'l' (108)
br.read();  // 'o' (111)
br.read();  // '\n' (10)  ← 개행도 읽힘!

// readLine()으로 읽으면
br.readLine();  // "Hello"  ← 개행 제외, 한 번에

99% 상황에서 readLine()을 씁니다.


5. Scanner vs BufferedReader 성능 비교

10만 개의 정수를 입력받는 상황을 가정해봅시다.

// Scanner 방식
Scanner sc = new Scanner(System.in);
for (int i = 0; i < 100000; i++) {
    int num = sc.nextInt();
}

// BufferedReader 방식
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
for (int i = 0; i < 100000; i++) {
    int num = Integer.parseInt(br.readLine());
}
방식 대략적인 시간
Scanner 약 1500ms
BufferedReader 약 150ms

약 10배 차이가 납니다. Scanner가 느린 이유는:

  1. 정규표현식으로 토큰 파싱
  2. 다양한 타입 처리를 위한 오버헤드
  3. 내부적으로 작은 버퍼 사용

6. 알고리즘 문제 필수 패턴

정수 여러 개 한 줄에 입력

입력: 1 2 3 4 5
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());

int a = Integer.parseInt(st.nextToken());  // 1
int b = Integer.parseInt(st.nextToken());  // 2
int c = Integer.parseInt(st.nextToken());  // 3

출력도 빠르게 - BufferedWriter

BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

bw.write("Hello\n");
bw.write(String.valueOf(123));  // 숫자는 String 변환 필요
bw.newLine();  // 줄바꿈

bw.flush();  // 버퍼 비우기 (필수!)
bw.close();

전체 템플릿

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

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        int n = Integer.parseInt(br.readLine());
        StringTokenizer st = new StringTokenizer(br.readLine());

        int[] arr = new int[n];
        for (int i = 0; i < n; i++) {
            arr[i] = Integer.parseInt(st.nextToken());
        }

        // 로직 처리

        bw.write(result + "\n");
        bw.flush();
        bw.close();
        br.close();
    }
}

7. 주의사항

nextInt() 후 readLine() 문제 (Scanner)

Scanner sc = new Scanner(System.in);
int num = sc.nextInt();      // 숫자만 읽고 개행은 남김
String str = sc.nextLine();  // 남은 개행을 읽어버림 (빈 문자열)

BufferedReader는 이런 문제가 없습니다. readLine()이 항상 한 줄 전체를 읽으니까요.

IOException 처리

// 방법 1: throws
public static void main(String[] args) throws IOException {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
}

// 방법 2: try-catch
public static void main(String[] args) {
    try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
        // 사용
    } catch (IOException e) {
        e.printStackTrace();
    }
}

알고리즘 문제에서는 간단하게 throws를 씁니다.


마무리

BufferedReader를 쓰는 이유를 정리하면:

  1. 버퍼링으로 I/O 횟수를 줄여 성능 향상
  2. readLine()으로 편리한 줄 단위 입력
  3. Scanner 대비 5~10배 빠른 속도

알고리즘 문제에서 시간 초과가 나면, 일단 입출력부터 BufferedReader/BufferedWriter로 바꿔보세요!


다음 글에서는 BufferedReader의 내부 동작 원리(버퍼 구조, fill() 메서드)를 깊이 파헤쳐보겠습니다.