
이 글은 이 코드 한 줄에서 시작됐다.
val br = BufferedReader(InputStreamReader(System.`in`))
val input = br.readLine().toInt()
요즘 부스트캠프에서 동료들 코드를 자주 보게 되는데,
피어 피드백 중에 문득 저 코드가 눈에 걸렸다.
아니 도대체 BufferedReader랑 InputStreamReader, System.'in'은 무슨 관계야?
그렇게 또 한 번, 나의 집요한 호기심이 발동해버렸고...
오늘 하루는 스트림에 대한 의문을 끝까지 파헤쳐보기로 했다.
하...피곤하다...응원 댓글 좀 주세요...
BufferedReader는 뭘까?
처음엔 그냥 "클래스겠지?" 했는데...
내 절친 G씨에게 물어봤더니 다음과 같이 이야기하더라.
"BufferedReader는 문자 스트림을 효율적으로 읽기 위해 버퍼를 사용하는 클래스입니다."
역시나 클래스.
근데 중요한 건 "문자 스트림"과 "버퍼"라는 말이다.
무슨 말일까?
스트림이라고 하면 "흐름"이라는데,
컴퓨터가 물에 떠다니는 것도 아니고 무슨 흐름?
난 이 개념이 너무 추상적으로 느껴졌다.
반면에 버퍼는 왠지 감이 온다.
"기다렸다가 처리하는 임시 저장 공간"이라는 느낌.
뭔가 감이 딱 온다.
G씨에 따르면 BufferdReader 내부에서는 이런 일이 벌어진다.
- 내부에 문자 배열 버퍼를 갖고 있음
- 데이터를 읽을 때마다 OS에 요청하는 것이 아니라, 한 번에 여러 문자를 읽어서 버퍼에 저장함
- 이후엔 버퍼에서 꺼내 쓰니까 빠르고 효율적임
즉, BufferedReader는 IO 요청을 최소화하고,
메모리에서 빠르게 읽기 위한 성능 최적화 도구인 거다.
- read() : 문자 하나 읽기
- readLine() : 한 줄 통째로 읽기 (\n, \r\n 포함 안 함)
- close() : 스트림 닫기
- ready() : 읽을 준비 됐는지 확인
이 질문이 또 생긴다.
"왜 BufferedReader가 InputStreamReader를 인자로 받고,
그건 또 왜 System.in을 받는 건데?"
G씨가 핵심을 콕 짚어줬다.
G씨에 따르면 BufferedReader는 문자 스트림(Reader)을 인자로 받는데,
System.in은 바이트 스트림(InputStream)이라서
중간에 InputStreamReader가 바이트 → 문자로 변환해준다고 한다.
정확히 말하면 아래와 같은 구조다.
키보드 입력
↓ (바이트 단위)
System.in → InputStream (바이트 스트림)
↓
InputStreamReader → Reader (문자 스트림)
↓
BufferedReader → 문자 버퍼 + readLine()
↓
사용자가 호출하면 문자열 반환
진짜 물리적인 이야기로 가보자.
우리가 키보드를 딱 누르면 내부적으로 다음과 같은 일들이 벌어진다.
- 기계적으로 스위치가 눌림
- 전기 신호가 발생 → 메인보드로 전달됨
- 키보드 안의 컨트롤러가 이걸 스캔 코드(숫자)로 바꿔줌
- OS는 이 스캔 코드를 받아서 유니코드 문자로 바꾸고
- 그 문자를 키보드 입력 버퍼(메모리 공간)에 저장함
자바는 JVM 위에서 동작하고, OS와는 독립적이어야 하는데
어떻게 OS의 키보드 입력 버퍼에 접근하지?
바로 여기에 답이 있다:
"네이티브 메서드"
System.in의 정체는 JVM에서 제공하는 InputStream 객체인데,
그 메서드 중 일부는 실제로 C나 C++로 구현된 OS 레벨의 네이티브 코드를 호출한다.
그래서 자바 코드가 OS의 입력 버퍼에 접근할 수 있는 거다.
- InputStream을 상속한 자바 클래스
- 내부적으로는 네이티브 메서드로 OS 자원을 다룸
- 실제론 키보드 입력 버퍼에서 바이트 단위로 데이터를 가져옴
val br = BufferedReader(InputStreamReader(System.`in`))
val input = br.readLine().toInt()
이 코드에서 가장 중요한 건 System.in
바로 OS와 JVM의 경계를 넘는 다리 역할을 한다.
- read() : 바이트 하나를 읽음. 읽은 바이트(0~255) 반환, 끝이면 -1 반환
- read(byte[] b) : 바이트 배열 크기만큼 데이터를 읽어서 배열에 저장, 읽은 바이트 수 반환
- read(byte[] b, int off, int len) : 배열 b의 off 위치부터 최대 len 바이트를 읽어 저장, 실제 읽은 바이트 수 반환
- skip(long n) : 입력 스트림에서 n 바이트를 건너뜀
- available() : 즉시 읽을 수 있는 바이트 수 반환
- close() : 스트림을 닫아서 리소스를 해제함
- mark(int readlimit) : 현재 위치를 표시해둠 (나중에 reset()으로 돌아오기 위함)
- reset() : mark()한 위치로 돌아감
- markSupported() : 마크 기능을 지원하는지 여부 반환
이 둘은 결국 흔히 말하는 "래퍼 클래스(wrapper class)"다.
즉, 다른 스트림을 감싸서 기능을 확장하거나 변환해주는 역할이다.
- InputStreamReader는 바이트 → 문자 변환기 (어댑터)
- BufferedReader는 성능 향상을 위한 버퍼 + 줄 단위 읽기 제공 (데코레이터)
그런데 재밌는 건
"BufferedReader의 '버퍼'는 키보드 입력 버퍼가 아니라, 클래스 내부 버퍼라는 점!"
이게 헷갈리기 쉬운 포인트다.
- InputStream : 바이트 기반 입력 스트림을 처리하는 추상 클래스
- OutputStream : 바이트 기반 출력 스트림을 처리하는 추상 클래스
- Reader : 문자 기반 입력 스트림을 처리하는 추상 클래스
- Writer : 문자 기반 출력 스트림을 처리하는 추상 클래스
- FileInputStream : 파일을 바이트 단위로 읽는 입력 스트림 클래스
- InputStreamReader : 바이트 스트림을 문자 스트림으로 변환해주는 클래스
- BufferedReader : 문자 스트림에 버퍼링과 readLine() 기능을 추가한 클래스
스트림은 결국 그냥 추상화된 개념일 뿐이다.
실제로 벌어지는 일은 다음과 같다.
- OS에 있는 키보드 입력 버퍼에 키가 들어오고
- JVM은 네이티브 메서드로 그걸 읽어오고
- System.in이 바이트 스트림으로 데이터를 전달하고
- InputStreamReader가 바이트를 문자로 바꾸고
- BufferedReader가 그걸 버퍼에 모아서 효율적으로 읽을 수 있게 해준다.
스트림이라는 건 결국 이런 입출력 동작을 추상화한 개념이고,
실제론 이런 역할을 하는 클래스들의 조합이다.
컴퓨터의 모든 입력은 물리에서 시작해,
OS를 거쳐, JVM을 지나, 우리가 호출하는 readLine()에 도달한다.
그 과정이 이렇게 정리되고 이해될 때,
비로소 나는 저 한 줄의 코드가 단순하지 않음을 느낀다.
하나에 꼳히면 끝까지 파고드는 이 성격...피곤하지만,
그래도 이렇게 정리해두면 다음부터는 좀 덜 피곤하겠지...
이 글이 누군가에게 도움이 되었으면 좋겠다!
필요하면 아래에 댓글로 질문 남겨주세요.
조언도 너무 감사합니다. 지적에 목마릅니다.
칭찬도 감사하지만 조언이나 지적은 배로 감사합니다.
안드로이드 개발자 화이팅!
Peace, Love.