개발/java

Input 찬찬히 뜯어보기. (BufferedReader와 InputStreamReader?)

ebang 2023. 12. 11. 17:21
반응형

Java 에서 Input을 받기 위한 방법

1. BufferedReader Class

서론

저는 C, C++ 로 코드를 많이 짭니다.
C, C++은 몇 단계만 거치면 바로 시스템 콜로 read, write 호출하는게 보여서 어떻게 동작하는지 아는게 쉬웠는데 java 는 왜 버퍼를 만들까… 하는 답답함이 있었습니다.

그래서 시간을 내어 원리를 파악하는 시간을 가졌습니다.

1. BufferedReader Class

BufferedReader class 는 문자들의 연속을 입력받을 때 사용하는 간단한 클래스입니다. 코딩 테스트에서 속도를 빠르게 한다고 하여 많이 사용하는 클래스입니다.
보통 이런식으로 사용됩니다.

BufferedReader 클래스는 안에 InputStreamReader를 반드시 사용해야할까요?
InputStreamReader 안에는 반드시 System.in이 들어가야할까요?

BufferedReader 클래스를 살펴보면 다음과 같습니다.

  • BufferedReader 객체는 생성자의 인자로 Reader 객체를 받습니다.
    • Reader 객체 : 문자 입력 스트림을 처리하는 추상 클래스입니다.
      • read(char[], int, int) and close() 를 구현해야하는 추상 클래스입니다.
      • Readable, Closesable 객체를 상속하네요.
      • ensureOpen() 함수를 이용해서 read 하기 전 예외처리를 해줍니다.
      • 살펴보니 read() 함수도 실제로 제대로 구현되어있지 않습니다.
      • 아마 InputStreamReader 가 상속한 뒤 overwrite 했겠죠? 이따가 살펴봅시다.

다시 BufferedReader 객체로 가봅시다.
Reader 멤버 변수 in, char[] cb가 핵심입니다.

Reader 추상 클래스의 구현체입니다.


먼저 생성자를 호출하네요.

super 를 이용해 인자로 받은 Reader 객체를 생성한 후,
멤버 변수 in 에 대입해줍니다.
예시대로라면, new InputStreamReader(System.in) 객체가 생성되어 in 에 저장되겠네요.
그리고 읽을 버퍼인 ch를 새롭게 할당해줍니다.
sz는 DEFAULT_CHAR_BUFFER_SIZE로 8192로 정의되어 있습니다.

이제 구현된 read() 함수를 봅시다. 자주 쓰는 readLine() 함수는 잠시 뒤에 보죠.

이 형태의 read() 메소드는 입력 스트림에서 하나의 문자를 읽고 그 문자를 반환합니다. 반환 값은 읽은 문자의 정수 값이며, 스트림의 끝에 도달하면 -1을 반환합니다.

  • InternalLock
  • locker.lock()
    • 두 개 모두 동기화를 위해 사용됩니다. 나중에 기회가 되면 알아보도록 하겠습니다.
  • implRead 함수가 핵심인 것으로 보이네요.
    • for 문처럼 보이지만, skipLF 가 true 일 때 continue 하기 위해서만 존재하는 for 문입니다.
    • 이 외에는 fill() 함수를 거친다음, cb[nextChar++] 를 수행하네요.
    • fill 함수
    • 먼저 위쪽 if 문을 통과한다고 치고, 아래 코드만 봅시다.
      • markedChar 값이 UNMARKED 라고 합시다.
    • do-while 문을 통해 mark 된 게 없다면 dst = 0 인 채로 in.read()를 수행합니다. Reader 객체를 저장해두었던 게 여기서 쓰이네요.
    • InputStreamReader(System.in) 객체를 넣어서 만든 BufferedReader라면,
    • InputStreamReader.read(cb, dst, cb.length - dst) 를 실행하는 것과 같습니다.
      • InputStreamReader의 read 정의를 봅시다.
      • 끝에 도달해서 아무것도 읽을 수 없을 때는 -1이 반환된다.
      • 그렇지 않으면 하나 이상의 문자가 읽힌 후 cbuf 에 저장된다.
      • 기본적으로 off 부터 len개의 문자를 입력한다.
    • 라고 설명에 적혀있네요.
    • sd 라고 불리는 인자는
      StreamDecoder 라는 자료형이고, 아래 과정을 통해 대입됩니다.
      InputStreamReader를 위한 객체 생성되는 것 같습니다. cs msㄴ 번역을 위해서 사용할 문자셋입니다.
    • lockFor는 동기화를 위한 락 객체를 반환합니다.
    다음과 같은 StreamDecoder가 생성되고, 반환됩니다.
    StreamDecoder 는 바이트 데이터를 문자 기반의 데이터로 변환하는데 사용됩니다.

    이 생성자는 다음과 같은 함수입니다.
    • decoder, characterset, lock 을 관리하고,
    • 특별히 bb 라는 게 보입니다.
      • this.bb = ByteBuffer.allocate(...): 조건에 따라 ByteBuffer의 크기를 할당합니다. 이 버퍼는 읽은 바이트를 저장하는 데 사용됩니다. mbc 값에 따라 기본 크기, 최소 크기, 또는 지정된 크기로 버퍼를 할당합니다.
    • bb.flip();: 버퍼를 읽기 모드로 설정합니다. flip 메소드는 버퍼의 위치를 0으로 설정하고, 한계를 현재 위치로 설정합니다.
    암튼 그래서 sd.read(cbuff, off, len) 의 의미를 살펴봅시다.

lockedRead
2부분으로 나누어서 봅시다.

남은 문자가 있었다면, 문자 하나를 추가해서 담습니다.
off++, len–를 통해 상황을 업데이트해줍니다.
그 결과로 len이 0이 되었거나 implReady(남이있는게 있다.)가 아니라면, 1을 반환합니다.
하나만 버퍼에 옮겼기 때문에 1을 반환하나봅니다.


남은 문자가 1개가 남아있다면 read0() 함수를 호출한 뒤, n + 1을 리턴합니다.

아직도 남아있다면 implRead 함수를 호출합니다.
주어진 바이트 버퍼(bb)로부터 데이터를 읽고, 이를 문자 버퍼(cb)에 디코딩합니다.


로직의 흐름

  • CharBuffer cb = CharBuffer.wrap(cbuf, off, end - off);: 입력 배열 cbuf를 기반으로 하는 CharBuffer를 생성합니다. 이 버퍼는 디코딩된 문자를 저장하는 데 사용됩니다.
  • cb.position() != 0 조건과 cb = cb.slice();: 버퍼의 현재 위치를 조정하여 올바른 시작점에서 읽기를 시작합니다.
  • 무한 루프(for(;;)) 내에서:
    • decoder.decode(bb, cb, eof): bb에서 데이터를 읽고, 이를 cb에 디코딩합니다. eof는 입력 스트림의 끝을 나타냅니다.
    • CoderResult를 체크하여 디코딩 상태를 확인하고, 필요에 따라 추가 조치를 취합니다.
      • isUnderflow(): 읽을 데이터가 충분하지 않은 경우 추가 데이터를 읽거나 루프를 종료합니다.
      • isOverflow(): 버퍼 공간이 부족한 경우 루프를 종료합니다.
      • throwException(): 디코딩 중 발생한 에러를 처리합니다.
  • eoftrue인 경우 decoder.reset(): 디코더를 리셋합니다.
  • 최종적으로 cb.position()을 반환하여 읽은 문자의 수를 나타냅니다.

정리해보면, BufferedReader 의 read() 함수는 먼저 자기 버퍼에 저장해둔 후, (이때는 바이트 데이터)InputStreamReader 에서 System.in 에 맞는 StreamDecoder 에서 read 함수를 호출해서 버퍼에 옮겨(이때는 문자열 데이터) 저장하도록 하네요.

더 자주 사용하는 readLine() 함수는 어떨까요? 이제 한번 살펴봅시다.

비슷한 구조인데, String을 반환하는 함수네요. 그리고 implReadLine (ignoreLF, term) 함수를 사용합니다.

implReadLine의 구조를 나눠서 살펴봅시다.


ensureOpen() 을 이용해, close 된 스트림인 경우 에러를 내뱉습니다.
omitLF 는 무시 혹은 pass 하는 경우 true 가 되네요.
omitLF 는 개행문자를 무시할건지 여부를 의미하는 것 같습니다.
term[0]은 좀 더 봐야할 것 같네요.


가장 먼저 보이는 건 bufferLoop 이라고 label을 달아두었습니다.
안쪽에서는 nextChar >= nChars라면 fill 함수를 사용하네요.

아까 보았던대로 fill 함수에서 cb 버퍼에 있던 문자들을 Reader 객체를 이용해 내부 버퍼로 이동시킵니다.
- fill 함수의 일부-


-> InputStreamReader.read -> StringDecoder의 read 호출 -> lockedRead -> 문자가 아니라 문자열이 남아있다면 implRead -> decoder.decode -> 바이트 데이터를 문자열 데이터로 변환해서 저장.

그 다음은 개행 문자 혹은 ‘\r’ 문자일 경우 반복문을 돕니다.

nextChar 는 앞으로 읽을 버퍼의 인덱스를 의미하고, nChars는 현재 버퍼의 총 크기입니다.
그리고 EOF에 도달했을 때 경우에 따라 s.toString() 혹은 null 을 반환하네요.

그 다음에는 charLoop를 돌면서 cb, 즉 버퍼를 순회합니다.

c 에는 한 문자씩 업데이트하고, 이 문자가 개행 혹은 ‘\r’ 이라면 루프를 빠져나옵니다. 그리고 빠져나오기 전에 eol 값은 true, term[0] 도 true 로 세팅합니다.

nextChar 값에는 i가 들어갑니다. 아마도 ‘\n’ 혹은 ‘\r’ 문자를 가리키고 있을 때니, 해당 부분부터 다시 for 문이 돌겠네요.


문장의 끝에 도달했다면, 즉 ‘\n’ 혹은 ‘\r’ 에 도달했다면, eol = true일 것이고 if 문 안에 들어가서 String 객체를 만들든지, 존재하던 StringBuilder를 이용해서 추가된 다음 반환됩니다.

StringBuilder가 처음엔 null로 설정되어있었는데요, 여기서 대입이 되네요.

StringBuilder 클래스는 Java에서 가변적인 문자열을 효율적으로 처리하기 위해 사용되는 클래스입니다. 이 클래스는 java.lang 패키지에 속하며, 문자열을 생성하고 조작하는 메소드들을 제공합니다. StringBuilder는 변경 가능한 문자열을 다루기 때문에, 문자열을 자주 변경하거나 추가하는 경우 String 클래스보다 성능상의 이점이 있다고 하네요.

 

 

여기까지 2시간동안 걸려서 알아봤습니다. 즐거운 시간이었습니다!


요약

  • new BufferedReader(new InputStream(System.in));
  •  BufferedReader , InputStreamReader는 각각 추상화된 Reader, InputStream 객체를 생성자의 인자로 받아서 초기화됩니다. 
      • InputStream 의 구현체 = System.in , InputStreamReader 의 생성자로 전달된다.

  • Reader 의 구현체  = InputStreamReader , BufferedReader의 생성자로 전달된다. 

 

    • BufferedReader : 문자 입력 스트림에 버퍼링을 추가하여 읽기 성능을 향상시킨다. 
    • InputStreamReader :  바이트단위의 데이터를 문자 스트림으로 변환한다.
    • System.in : 표준 입력 스트림을 나타낸다..기본적으로 이 스트림은 키보드 입력을 받는다. 
  • InputStreamReader 가 생성자의 인자로 받는 InputStream 추상클래스 객체에 대해, StreamDecoder를 생성해서 바이트 단위의 데이터를 문자 단위로 변환합니다. 
  • BufferedReader가 생성자의 인자로 받는 Reader 추상 클래스 객체에 대해, 데이터를 읽어들일 것을 요청한 뒤 이 데이터를 버퍼에 저장합니다. 

 

  • 전달 순서 : System.in [nputStream 구현체](바이트), -> InputStreamReader[Reader 구현체] (문자열) -> BufferedReader(버퍼링된 문자열)

 

반응형