알고리즘 문제를 풀다 보면 "시간 초과"를 만나는 순간이 옵니다. 분명 로직은 맞는 것 같은데... 이럴 때 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가 느린 이유는:
- 정규표현식으로 토큰 파싱
- 다양한 타입 처리를 위한 오버헤드
- 내부적으로 작은 버퍼 사용
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를 쓰는 이유를 정리하면:
- 버퍼링으로 I/O 횟수를 줄여 성능 향상
- readLine()으로 편리한 줄 단위 입력
- Scanner 대비 5~10배 빠른 속도
알고리즘 문제에서 시간 초과가 나면, 일단 입출력부터 BufferedReader/BufferedWriter로 바꿔보세요!
다음 글에서는 BufferedReader의 내부 동작 원리(버퍼 구조, fill() 메서드)를 깊이 파헤쳐보겠습니다.
'Language > Java' 카테고리의 다른 글
| [Java][BufferedReader] 동기화 메커니즘 - 데코레이터 패턴의 적용 (0) | 2025.12.31 |
|---|---|
| [Java][BufferedReader] 내부 동작 원리 - fill()과 버퍼의 비밀 (1) | 2025.12.31 |
| [Java][Collections Framework 완전 정복] Stack 클래스를 사용하지 않는 이유 및 대안 (0) | 2025.01.16 |
| [Java][Collections Framework 완전 정복] Map 인터페이스, Map.Entry 인터페이스 (0) | 2025.01.07 |
| [Java][Collections Framework 완전 정복] Set 인터페이스(HashSet) (0) | 2025.01.07 |