
들어가며
자바를 처음 배울 때 Scanner와 BufferedReader 둘 중에 하나 사용하면 된다고 배웠습니다. I/O 성능이 중요하면 더 빠른 BufferedReader를 쓰면 된다고 해서 BufferedReader를 습관처럼 사용해왔습니다.
BufferedReader가 버퍼 단위로 끊어서 입력을 받는다는 건 알고 있었는데 인자로 전달되는 InputStreamReader나 System.in에 대해선 무지했습니다. 기본부터 충실하자는 제 공부 철학을 지키기 위해 다시 새롭게 공부한 BufferedReader에 대해 글을 써보려고 합니다.
저처럼 BufferedReader의 사용법은 알지만 BufferedReader의 자료구조에 대해 공부하고 싶었던 분들에게 도움이 될 것 같습니다.
Java의 표준 입력
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));위 코드는 자바에서 키보드 입력을 효율적으로 읽기 위한 코드입니다.
- BufferedReader : 문자 스트림을 버퍼 단위로 끊어서 입력을 받는 클래스
- InputStreamReader : 바이트 스트림을 문자 스트림으로 변환해주는 클래스
- Sytem.in : 자바의 표준 입력 스트림으로 주로 키보드 입력에 사용
코드의 흐름은 다음과 같습니다.
키보드 입력 → InputStreamReader에 바이트 스트림으로 전달 → InputStreamReader에서 문자 스트림으로 변환 → BufferedReader에 전달 → BufferedReader에서 버퍼 단위로 끊어서 입력을 받음
System.in
먼저 System 클래스는 자바에서 제공하는 유틸리티 클래스로 입출력 스트림이나 시스템 명령어 등의 기능을 제공합니다. 이 System 클래스에 in 필드는 InputStream 타입의 객체라고 정의되어있습니다.

위의 사진처럼 in 필드는 InputStream 타입의 정적 필드입니다. 사용자가 키보드로 입력한 데이터는 바이트 단위로 읽혀 InputStreamReader 클래스에게 전달됩니다.
InputStreamReader
우선 InputStreamReader 클래스와 InputStream, Reader 추상클래스에 대한 정리가 필요할 것 같습니다.
InputStreamReader는 Reader 추상 클래스를 상속 받고 있습니다. 그리고 위에서 System.in이라는 InputStream 타입의 필드를 인자로 전달 받는다고 했습니다. 아래는 이 관계를 나타낸 모습입니다.

InputStreamReader는 바이트 스트림을 문자 스트림으로 변환해주는 클래스라고 언급했었습니다. InputStream을 통해 읽어 들인 바이트 코드를 문자 인코딩(charset)을 이용해 변환합니다. (문자 인코딩 방식은 UTF-8, ISO-8859-1 등이 있는데 이번 주제와 멀어질 것 같아 다음에 다뤄보도록 하겠습니다.)
참고로 뒤에서 자세히 설명하겠지만 BufferedReader는 Reader 클래스를 인자로 받습니다. 따라서 InputStreamReader처럼 Reader 클래스를 상속 받는 서브클래스들도 전부 BufferedReader 인자로 사용 가능합니다.

아래는 자주 사용되는 Reader 클래스의 서브클래스들입니다.
- FileReader: 파일에서 문자를 읽기 위한 클래스
- BufferedReader: 문자 입력 스트림을 버퍼링하여 성능을 향상시키기 위한 클래스
- StringReader: 메모리에 있는 문자열을 입력 스트림으로 처리하기 위한 클래스
이렇게 변환된 문자 스트림은 BufferedReader에게 인자로 전달됩니다.
BufferedReader
1. BufferedReader란?
BufferedReader는 문자 스트림을 버퍼 단위로 끊어서 입력을 받는 클래스입니다. 쉽게 풀어서 설명하면 정해진 크기의 버퍼에 사용자의 입력을 모아두었다가 버퍼의 크기에 도달할 때마다 입력을 처리합니다. BufferedReader는 이렇듯 필요에 따라 버퍼의 크기를 조절하며 한 번에 처리하기 때문에 처리 성능이 뛰어납니다.
아래는 자바의 표준 라이브러리입니다.

오버로딩된 2개의 생성자가 정의되어 있습니다. Reader는 필수 인자고, 버퍼의 사이즈는 선택 사항입니다. 버퍼 사이즈의 최소 단위는 1 Byte이고, 만약 버퍼 사이즈를 지정하지 않는다면 defaultCharBufferSize가 적용되는데 이 값은 8192 Byte로 정의되어 있습니다. 즉, 8192개의 문자를 입력 받을 수 있다는 의미이고 8192개의 char 타입이기 때문에 한 버퍼당 8192 * 2 Byte = 16384 Btye의 입력을 받게 됩니다.
2. 왜 8192가 기본값일까?
만약 버퍼의 크기가 4 Byte처럼 매우 작다면 어떻게 될까요? 입력의 크기가 매우 큰 경우 잦은 I/O으로 인해 성능이 저하되고 BufferedReader를 쓰는 의미가 퇴색될 것입니다. 반대로 버퍼의 크기가 매우 크다면 메모리 공간이 많이 필요하게 되고, 입력이 적을 경우 메모리의 낭비로도 이어질 수 있습니다. 그래서 I/O 성능과 메모리를 고려해 8192를 절충안으로 많이 사용하게 되었습니다.
3. BufferedReader 메서드
먼저 아래와 같이 BufferedReader의 객체 br을 생성했습니다.
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
read()
입력의 단위가 문자 하나입니다. 한글, 영어, 특수문자, 공백 모두 문자에 해당합니다. read의 반환 값은 유니코드 값인 정수이기 때문에 int 타입으로 변수를 선언해야 합니다.
int data = br.read();
여러 문자를 입력 받는 경우는 다음과 같이 작성할 수 있습니다.
int data;
while ((data = br.read()) != -1) { // 입력의 끝에 도달할 때까지 반복
char character = (char) data; // 읽은 정수 값을 문자로 변환
System.out.print(character);
}입력이 없는 경우 -1을 반환하기 때문에 반환값이 -1인 경우 입력을 즉시 종료합니다.
readLine()
readLine은 개행 문자(Enter) 단위로 입력을 받습니다. 그리고 입력된 값을 String 타입으로 반환합니다. Number도 우선 String 타입으로 받아야하기 때문에 Integer.parseInt 등의 메서드로 파싱하는 과정이 필요합니다.
String input = br.readLine()
그리고 readLine은 입력을 통으로 받기 때문에 입력을 가공하고 싶으면 StringTokenizer나 split 등을 사용하면 됩니다. 아래는 공백을 기준으로 입력을 구분하는 각각의 예시입니다.
// StringTokenizer
StringTokenizer st = new StringTokenizer(br.readLine(), " ");
// split
String []input = br.readLine().split(" ");
StringTokenizer를 사용하면 두번째 인자를 기준으로 입력을 각각의 토큰으로 분리합니다. 그리고 split 메서드를 사용하면 인자를 기준으로 입력이 나뉘어져 배열로 저장할 수 있습니다.
IOException
BufferedReader의 read와 readLine 메서드를 사용할 때 IOException을 처리하지 않으면 왼쪽 사진과 같은 메세지가 뜹니다.


오른쪽 사진처럼 throws 키워드로 예외 전가하는 방법을 선택해 예외 처리해줬습니다.
그렇다면 IOException 예외가 발생하는 이유가 뭘까요?
우선 IOException은 I/O 작업 중 생길 수 있는 모든 위험 요소에 대해 발생합니다. 이런 잠재적 오류를 가진 메서드를 사용할 때는 반드시 예외 처리가 필요합니다. 따라서 자바 표준 라이브러리에서 read와 readLine 메서드에 throws IOException을 지정해 메서드 호출자에게 IOException을 처리하라고 강제합니다.


알고리즘 문제 풀 땐 보통 throws 처리하지만 일반적인 경우 try ~ catch 문을 사용합니다. (다음에 예외 처리(Exception)에 대한 포스팅도 다뤄보겠습니다.)
.
.
.
마치며
BufferedReader 사용에 대한 정리만 했는데도 내용이 생각보다 많아서 놀랐습니다. 자료구조 공부할 때는 시간 가는 줄 모르고 하는 것 같네요. 이번 주제에 충실하고자 제외한 내용들이 꽤 있는데 다음에 자세하게 다뤄보도록 하겠습니다.
피드백은 언제나 환영합니다! 😊
'Java' 카테고리의 다른 글
| [Java] Pattern과 Matcher 클래스로 정규 표현식 제대로 사용하기 (1) | 2025.01.05 |
|---|---|
| [Java] Collections Framework간 변환 총정리 - List, Set, Map, Queue (1) | 2024.12.03 |
| [Java] compare 메서드에서 overflow가 발생하지 않는 이유 (0) | 2024.10.14 |
| [Java] toString과 String.valueOf 메서드 차이 (3) | 2024.10.14 |
| [Java] Comparable과 Comparator로 정렬 이해하기 (0) | 2024.10.13 |