본문 바로가기

Backend/Java

[JAVA] BufferedReader 사용 이유와 과정 (read, 스트림(Stream), 버퍼(Buffer))

 

 


단계별로 알아보는 Byte → Char → String 입력 받기

(System.in,read() , 스트림(Stream), 버퍼(Buffer))

 


 

예전에 백준을 풀면서 수행시간이 빠르다는 이유로 원리도 모르고 사용했던 기억이 있어 이번 기회에 다뤄보면 좋을 것 같아 작성하게 된 글이다.

 

 

1. 입출력(Inout-Output)


자바에서는 기본적으로 스트림 개념의 입출력을 사용한다.

모든 입출력 클래스는 java.io 패키지에 정의되어 있다.

 

입출력 타입 데이터 타입 예시 처리 방법
Byte(바이트) 이진 데이터 이미지
실행파일
엑셀파일 등
InputStream (입력)
OutputStream (출력)
문자 텍스트 데이터 txt 파일 등 Reader (입력)
Wrtier (출력)

 

 

기본적으로 표준입출력 형태인 System.in 부터 Scanner 클래스를 통해 입력 받기도 하고, 밑에서 다룰 InputStream 등등이 존재하지만.. 이 포스트에서는 가볍게 Byte형을 Char형으로, Char형을 String으로 입력 받는 과정에 초점을 두어 작성됐다.

 

한마디로..

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

이게 대체 무슨 뜻인데?

 

가 궁금한 사람들에게 해결책을 줄 글이다.

 

 

 

2. read() 메소드


 

자바에는 System.in 형태의 입력 형태가 있다. 갖가지 값이 들어오기 때문에 에러가 발생할 수 있어 예외처리가 필수이다.

 

그리고 이런 System.in 에 read() 가 붙게 되면 Byte 형태를 입력받게 된다.

System.in.read();

 

 

콘솔창에서 입력을 받아오는 read() 메소드 (매개변수X)가 있다. read()는 inputStream에서 1바이트를 입력 받는다.

public static void main(String[] args) throws IOException{
    System.out.print("정수를 입력하세요 : ");
    int num = System.in.read(); // 예외 처리 필요
    System.out.printf("입력한 수 : %d\n", num);
}

 

정수 123을 입력했더니 49가 출력됐다. 이게 무슨 뚱딴지같은 값인가? 싶겠지만..

 

우리는 입력한 123을 System.in.read()는 1바이트밖에 읽지 못하는 바이트형 입력 클래스이다. 그래서 실상 "1"만 읽었다. 그렇게 받은 Byte형의 1은 우리의 의도였던 int와 다르게 "1"이란 글자로 입력 받게 되었고, 해당 값을 int형의 num에 넣어서 '1'의 아스키코드 값인 49가 출력된 것이다.

 

또한 System.in.read(); 는 예외 처리가 필요한 입력 형식이라 main에 throws IOException을 명시했다.

 

 

System.out.print("키보드 입력을 실행하세요 -> ");
int num1 = System.in.read();
System.out.printf("\n입력된 값 : %c, %d\n", (char)num1, num1);

// 앞에서 입력한 엔터 또는 입력 끝 표시가 다음 입력값으로 입력되는 오류를 방지하기 위해
System.in.read();
System.in.read();

 

위의 예시에서 입력이 끝나거나 콘솔창에 값을 입력하고 엔터를 눌러 다음 명령을 실행한 순간, 다음 입력 명령에 값으로 입력 끝 표시나 엔터가 입력되는 오류가 생길 수 있다. 위와 같은 명령이 연달아 온다고 생각해보자. 다음 입력은 int num2; 로 받는다고 했을 때, 처음 num1 에는 1바이트의 값이 무사히 저장될테지만, 엔터를 입력함으로서 뒤이어 오는 num2 에는 엔터가 저장된다.

또한 입력의 끝을 알리는 표시가 저장되어 있어 입력을 원치않는 데이터가 총 2Byte가 존재하게 된다. 따라서 엔터 또는 입력의 끝을 마치는 표시가 다음 입력값으로 입력되는 오류를 방지하기 위해 System.in.read(); 을 두번 추가 작성했다.

 

 

입력한 문자로 출력하기 위해서는 입력 받는 변수에 (char) 캐스팅 연산을 해야한다.

 

 

byte 배열로 입력 받는 방법도 존재한다.

public static void main(String[] args) throws IOException {
    byte[] input = new byte[20]; // 입력된 글자 하나의 아스키코드 값을 저장할 공간 20칸

    System.out.print("키보드 입력을 실행하세요(1개 이상의 글자)");
    // 입력된 글자의 갯수를 리턴해서 size 변수에 저장
    int size = System.in.read(input); // input 변수가 갖고 있는 참조값(주소)부터 입력되는 내용이 차례로 저장

    System.out.printf("입력한 내용의 크기는 %d 입니다. \n", size);
    for(int i=0; i<input.length; i++) // 빈공간은 ㅁ(0)로 출력(폰트차이)
        System.out.printf("%c", (char)input[i]);
}

 

read(byte[]) 배열의 형식은 매개변수가 byte형 배열인 경우로, 입력 스트립의 가장 앞부터 매개변수로 전달된 배열의 크기까지의 바이트를 읽어온다. 여러개의 글자를 한번에 입력받고, 배열의 참조값부터 글자를 하나씩 저장한다. 위와 같은 형식으로 입력받은 값은 결과적으로 크기를 반환한다.

 

하나 유의할 점이, read()로 입력을 받고나면 문자가 끝나는 표시와 엔터가 함께 입력된다고 했다. 따라서 크기는 우리가 입력한 문자에 +2된 값이 나온다.

 

 

입력한 글자를 하나씩 출력해보면, 빈공간이 폰트에 따라 ㅁ로 출력되곤 한다. 이는 아스키코드 0에 해당하는 깨진 글자다.

 

 

public static void main(String[] args) throws IOException {
    // | 6 | 7 | 5 | 1 | 2 |   |   |   | ... - 입력된 글자
    // | 0 | 1 | 2 | 3 | 4 |   |   |   | ... - 배열의 인덱스
    // 6*10000 + 7*1000 + 5*100 + 1*10 + 2

    byte[] number = new byte[10];
    System.out.print("정수를 입력하세요 : ");
    int size = System.in.read(number);

    // 입력글자 : 5, size : 7, 변환시작인덱스 : 4
    int m = 1;
    int num1 = 0;
    for(int i=(size-3); i>=0; i--) {
        num1 += (number[i] - '0') * m; // 아스키코드 1 : 49 -> 48('0')을 빼면 됨
        m = m * 10;
    }
    System.out.printf("입력한 숫자(정수)는 %d입니다\n", num1);
}

 

이러한 사실을 이용해 위와 같이 read(배열) 메소드로 여러글자를 입력 받아 우리가 아는 문자로 출력할 수도 있다.

 

 

 

앞서 구구절절 얘기를 많이했지만

결론적으로, System.in.read(); 는 불편하다.

1. 입력한 값이 의도한 값으로 쓰이려면 별도의 변환 작업이 필요하고

2. 버퍼에 데이터가 있으면(엔터, 입력 끝) 의도하지 않는 입력이 실행되고

3. 명령을 한 번에 한글자씩만 입력 받을 수 있는

 

우리가 사용하기에는 비효율적인 입력 방식이다.

 

 

 

3. 스트림(Stream)


자바에서는 파일이나 콘솔의 입출력을 직접 다루지 않고, 스트림(stream)이라는 흐름을 통해 다룬다. 여기서 스트림이란, 실제의 입력이나 출력이 표현된 데이터의 이상화된 흐름을 의미한다. 스트림은 운영체제에 의해 생성되어 키보드, 모니터, 파일, 네트워크 등을 잇는 가상의 연결 고리이며 이들의 입출력의 중간 매개자 역할을 한다. ( 출처 : https://tcpschool.com/java/java_io_stream )

 

모든 데이터의 입력은 스트림 형태로 받는다.

 

스트림은 단방향 통신이다. 따라서 입력과 출력 기능을 모두 갖춘 클래스는 존재하지 않는다. 대신 입력과 출력에 관한 스트림이 각각 클래스로 제공된다.

 

 

앞서 작성했던 표를 다시 가져오겠다.

입출력 타입 데이터 타입 예시 처리 방법
Byte(바이트) 이진 데이터 이미지
실행파일
엑셀파일 등
InputStream (입력)
OutputStream (출력)
문자 텍스트 데이터 txt 파일 등 Reader (입력)
Wrtier (출력)

 

수업시간에는 이렇게 배웠는데 찾아보니까 Reader는 입력의 최상위 '추상' 클래스라 직접 객체를 생성하지는 못한다고 한다.. 자식 객체에 상속되거나 오버라이딩 되어 사용된다고... Reader 아래 InputStreamRader 와 InputStreamBuffered 관계라고... 그런데 또 다른 게시글은 바이트 입출력 클래스는 InputStream과 OutputStream 이라는 추상클래스를 상속 받아 만들어지고, 문자 입출력 클래스는 Reader와 Writer 이라는 추상클래스를 상속 받아 만들어진다고 한다. 이 부분은 더 자세히 공부해서 추후 수정하겠지만 우선 후자로 이해를 하고 설명하겠다.

 

 

앞서 Byte형으로 값들을 입력 받아봤으니, 이번에는 문자인 Char형으로 받아보자.

 

우선 Char 문자로 입력을 받기 위해서는 문자 스트림 클래스를 통해 기본 입력을 문자 스트림으로 처리해야 한다.

일반적으로 문자 스트림은 스트림 클래스의 객체를 생성하여 입력을 수행한다. 만약 생성된 바이트 스트림이 존재한다면, 바이트 스트림의 객체를 사용하여 문자 스트림으로 변환이 가능하다.

 

바이트 스트림 객체를 문자 스트림으로 변환하기 위해서는 InputStreamReader 클래스를 사용한다. ( InputStream : 바이트 처리, Reader : 문자 처리라고 생각하면 이해가 쉽다. )

InputStreamReader 클래스는 바이트 스트림의 객체를 생성자의 매개변수로 전달받아 해당 바이트 스트림을 문자 스트림으로 변환해 준다.

 

사용을 위해 java.io.InputStreamReader; 를 임포트 해주고...

import java.io.InputStreamReader;
// byte 형입력만 가능한 System.in을 InputStreamReader가 장착하고 char형 입력을 가능하게 함
InputStreamReader reader = new InputStreamReader(System.in);

 

read 메소드를 사용할 때 사용했던 byte형 입력 가능 System.in 을 매개변수로 전달받아 InputStreamReader의 매개변수로 넣어줬다. 이로 인해 InputStreamReader 객체인 reader는 char형 입력을 받을 수 있게 되었다.

 

 

InputStreamReader reader = new InputStreamReader(System.in);

char[] message = new char[30];
System.out.print("메시지를 입력하세요 : ");

int size = reader.read(message);
System.out.printf("입력된 메시지는 %d 글자입니다.\n", size);

System.out.println(message); // 반복실행문 없이 한번에 출력

 

Byte형에서 Char형으로 받았다 분이지 아직 우리가 활용할 수 있는 String 형태는 아니다.

 

 

 

4. 버퍼(Buffer)


Char형을 String으로 받기 위해서는 BufferedReader 클래스를 이용해야 한다.

BufferedReader 클래스 이용을 위해서는 java.io.BufferedReader; 를 임포트 해주어야 한다.

import java.io.BufferedReader;
InputStreamReader reader = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(reader);
// BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// System.in -> InputStreamReader -> BufferedReader

 

위와 같은 형태로 사용한다.

 

System.in 으로 입력 받은 Byte형태를

InputStreamReader에 전달해 Char형으로 변환하고

Char형으로 변환(문자로 변환)한 값을 BufferedReader가 String형으로 입력 받는 형태다.

 

더보기

참고로 위와 같은 형태로 작성하기 위해서는

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;

 

3가지를 모두 임포트 해주어야 한다.

 

public static void main(String[] args) throws IOException {

 

System.in 으로 인한 예외처리도 까먹지 말자.

 

 

그리고 BufferedReader 객체인 br에 .readLine() 을 붙이면 String형으로 별도 조치를 취하지 않아도 무사 입력됨을 확인할 수 있다.

String str = br.readLine();
// BufferedReader를 사용함으로써 readLine() 사용이 가능하고 String 자료 사용이 가능해짐
System.out.print("메시지를 입력하세요 : ");
String message = br.readLine();

System.out.printf("입력된 메시지는 %d 글자입니다.\n", message.length()); // 글자수도 입력 글자 수만큼만 출력

System.out.println(message);

 

드디어 우리가 원하는 형태(String)으로 입력받았다!

요상한 개행문자(입력 끝 나타냄), 엔터 입력없이 깔끔하게 문자만 출력된다.

 

 

System.out.print("정수를 입력하세요 : ");
String input = br.readLine();

int num = Integer.parseInt(input);

System.out.printf("입력된 정수 : %d\n", num);

 

Integer.parseInt(String데이터); 를 통해 int형으로 출력도 가능하다.

 

 

// 메모리 누수 현상을 방지하기 위해 반드시 close 메소드를 호출하여 종료를 해야함
br.close();

 

BufferedReader를 이용하고 나서는 메모리 누수 현상을 방지하기 위해 (BufferedReader객체).close() 메소드를 호출해 종료해주자.

 

 

이쯤에서 의문이 하나 든다.

여태까지 Scanner 클래스를 이용해 int, string 할 것 없이 잘 입력 받고 있었는데, 왜 갑자기 출력 형식이 String으로 고정되는 버퍼를 사용하는 방식도 알아야하는지. (난 그랬다.)

 

결론부터 얘기하면 Buffer를 이용한 입력이 빠르기 때문이다.

Buffer를 사용한 입력은 키보드의 입력을 바로 프로그램에 전달하지 않고 한글자씩 버퍼로 전송한다. 입출력장치(키보드 등)와 프로그램 사이에 버퍼라는 데이터 담는 통이 하나 있는 것이다. 이 데이터 통(버퍼)이 가득차거나 개행문자가 나타나면 버퍼의 내용을 한번에 프로그램에 전달한다.

 

디스크의 속도는 느리고, 외부 입출력 장치도 값을 전달하는데 (컴퓨터입장에서)시간이 제법 소요되는 일이다. 따라서 중간에서 빠르게 데이터를 받고 한번에 프로그램을 전달하는 것이 효율적이라는 것이다.

 

 

 

이 뒤에 입출력 기능이 있는 클래스들이 업그레이드 되어 File, Data 등으로 머리를 어지럽힐 것이다..

예를 들면..

 

FileoutputStream

FileWriter

ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file))); (와;)

 

뭐 이런 것들..

당황하지말고 사용된 입출력 클래스들을 잘 떠올려보자. 형태가 잘보면.. 제법 규칙성 있고 비슷하다 .