[JAVA] BufferedReader 와 Scanner 차이 및 사용법

limchard·2024년 1월 9일
0

java

목록 보기
46/48
post-thumbnail

Scanner

Scanner클래스는 입력받은 데이터(바이트)를 다양한 타입으로 변환하여 반환하는 클래스이다.
간단하게 기본형과 String 타입을 정규표현식을 사용해 파싱(parse)할 수 있다.

Scanner의 특징

  • java.uitil 패키지에 속한다. (java.util.Scanner)
  • 공백(띄어쓰기) 및 개행(줄바꿈)을 기준으로 읽는다.(' ', '\t', '\r', '\n' 등)
  • 원하는 타입으로 읽을 수 있다. (따로 형변환 등 가공할 필요가 없다)
  • 버퍼의 사이즈가 1024 char 이다. (많은 입력을 필요로 할 경우 성능상 좋지 않을 수 있다.)
  • UnChecked(Runtime) Exception으로 별도로 예외 처리를 명시할 필요가 없다.
  • Thread unsafe 성질을 지니기에 멀티스레드 환경에서 문제가 발생할 수 있다.
  • 데이터를 입력받을 경우 즉시 사용자에게 전송되며 입력받을 때마다 전송되어야 하기에 많은 시간이 소요된다.
  • Scanner는 동기화가 되지 않기 때문에 멀티 쓰레드 환경에서 안전하지 않다.

BufferedReader

데이터를 한번에 읽어와 버퍼에 보관한 후 버퍼에서 데이터를 읽어오는 방식으로 동작하는 클래스이다. 즉 사용자가 입력한 문자 스트림(Stream)을 읽는 것(read) 이라 한다.

  • 여기서 버퍼(buffer)란?
    데이터를 한 곳에서 다른 한 곳으로 전송하는 동안 일시적으로 해당 데이터를 보관하는 임시 메모리 영역이다. 주로 입출력 속도 향상을 위해 버퍼를 사용한다.
    자바는 버퍼를 BufferredReader와 BufferedWriter 라는 클래스를 제공하여 다룰 수 있다.

BufferedReader의 특징

  • java.io 패키지에 속한다. (import java.io.BufferedReader)
  • 개행(줄바꿈)문자만 경계로 인식한다.
  • 데이터를 파싱하지 않고 String으로만 읽고 가져온다.
  • 원하는 타입에 따라 형변환 등 추가 가공이 필요하다.
  • 버퍼의 사이즈가 8192 char(16,384byte) 이다.
  • Checked Exception으로 반드시 예외 처리를 명시해야 한다. (IOException을 throw하거나 tyr/catch 해야한다.)
  • Thread safe 성질을 지니기에 멀티스레드 환경에서도 안전하다.
  • 버퍼가 가득차거나 개행문자가 나타나면 버퍼의 내용을 한번에 프로그램으로 전달하기에 Scanner보다 소요되는 시간을 절약할 수 있다.
  • BufferedReader는 동기화 되기 때문에 멀티스레드 환경에서 안전하다.

그럼 먼저 간단하게 Scanner의 작성법을 알아보자 ```java import java.util.Scanner; ... Scanner sc = new Scanner(System.in); String st = sc.nextLine(); ``` 위와 같이 `System.in` 을 통해 Scanner 객체를 생성한다.

System.in 이란?
사용자로부터 입력을 받기 위한 스트림(Stream)이다. Scanner 클래스뿐만 아니라 다른 입력 클래스들도 System.in을 통해 사용자 입력을 받아야 한다.

다음으로 BufferedReader 작성법도 알아보자.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
...
public static void main(String[] args) throws IOException {
  BufferReader br = new BufferedReader(InputStreamReader(System.in));
  String st = br.readLine();

  int a = Integer.parseInt(br.readLine());
  int b = Integer.parseInt(st);
}

위와 같이 BufferedReader는 매개변수로 InputStreamReader 를 사용하여 객체를 생성한다.

InputStreamReader 란?
문자 기반의 보조 스트림으로써 바이트 기반 스트림을 문자 기반 스트림으로 연결시켜 주는 역할을 한다.

또한 패키지의 경우 java.io 하위 BufferedReader, InputStreamReader 와 예외처리를 위한 IOException 을 모두 사용하며 모두 간단히 java.io.* 로 포함할 수 있다.

Scanner와 BufferedReader의 차이점

내가 이해한 바로는 Scanner는 사용자가 입력받은 데이터를 입력 받은 즉시 타입에 구애받지 않고 반환하는 것이며, BufferedReader는 사용자가 입력한 데이터 자체를 문자열 타입으로 읽어들이는 것이라 이해했다.

1. Scanner는 BufferedReader보다 타입에 구애받지 않는다.

  • BufferedReader는 String 형식으로만 읽고 저장하기에 형변환을 위한 추가적인 코드 작성이 불가피한 반면에 Scanner는 원하는 타입으로 읽고 파싱할 수 있다.
  • Scanner의 경우 int, long, short, float, double의 경우 nextInt(), nextLong(), nextShort(), nextFloat(), nextDouble()과 같은 함수들을 사용할 수 있다.
  • 반면에 BufferedReader는 readLine() 함수만을 사용한다.

2. BufferedReader는 Scanner보다 효율적인 메모리 용량을 가진다.

  • BufferedReader의 버퍼 메모리가 8KB로 Scanner의 버퍼 메모리가 1KB보다 크기에 많은 입력이 있을 경우 더 효율적이다.
  • 다만, BufferedReader의 경우 일단 큰 메모리를 잡아먹게 된다.

3. BufferedReader는 Scanner보다 안전하다.

  • Scanner는 Thread-unsafe 하기에 멀티스레드 환경에서 안전하지 않지만 BufferedReader는 안전하다.
  • 스레드 간 Scanner는 공유할 수 없지만 BufferedReader는 공유할 수 있다.
    • 동기화를 지원하는 BufferedReader는 싱글스레디은 Scanner보다 약간 느린데, Scanner의 경우 정규식을 사용하여 입력을 받으므로 BufferedReader가 문자열을 더욱 빠르게 입력받을 수 있다.

4. BufferedReader가 Scanner보다 실행 속도가 빠르다.

  • 백준에서 작성된 입력속도 비교표를 확인해보면 BufferedReader는 0.68585초, Scanner는 4.8448초가 소요되며 둘의 시간차이가 생각보다 크다.

  • 왜 빠를까? 하드디스크는 원래 속도가 엄청 느리다. 하드뿐만 아니라 키보드나 모니터와 같은 외부 장치와의 데이터 입출력은 생각보다 시간이 걸리는 작업이다. 버퍼링 없이 키보드가 눌릴 때마다 눌린 문자의 정보를 목적지로 바로 이동시키는 것보다 중간에 버퍼를 둬서 데이터를 한데 묶어서 이동시키는 것이 보다 효율적이고 빠르다.
    (버퍼 없이 전송하게 되면 CPU와 성능 갭이 많이 나서 비효율적이다.)

속도차이가 얼마나 날까?

그럼 Scanner와 BufferedReader의 예제를 작성해보고 실행속도 차이를 몸소 느껴보자.
간단한 A와 B 두 수의 합을 구하는 백준 1000번 문제를 풀어보았다.

1. Scanner

import java.util.Scanner;

public class Main{
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int a = sc.nextInt();
        int b = sc.nextInt();
        int sum = a + b;
        System.out.println(sum);
        sc.close();
    }
}

Scanner 객체를 생성해서 nextInt() 함수를 통해 a와 b를 입력받아 합을 구했다.

2. BufferedReader

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

public class Main{
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String s = br.readLine();
        StringTokenizer st = new StringTokenizer(s);
        int a = Integer.parseInt(st.nextToken());  
        int b = Integer.parseInt(st.nextToken());
        System.out.println(a+b);
        br.close();       
    }
}

BufferedReader 객체를 생성하고 한줄 단위로 처리할 수 있는 readLine() 함수를 통해 s에 저장한 뒤 StringToknizer 객체를 사용하여 a와 b에 공백을 기준으로 나누어 저장한 뒤 합을 구했다.

StringTokenizer 란?

문자열을 지정한 구분자롤 문자열을 쪼개주는 클래스이다.
그렇게 쪼개어진 문자열을 우리는 토큰(token)이라 부른다.
java.util.StringTokenizer 라이브러리를 import해야 사용할 수 있다.

Scanner 실행시간

BufferedReader 실행시간

Scanner를 사용했을 경우 212ms, BufferedReader는 124ms로 BufferedReader의 코드길이가 더 길어도 빠른 속도를 보여주었다.

다만 메모리의 경우 Scanner가 17704KB, BufferedReader가 14188KB로 큰 차이는 나지 않았다. 이를 통해 적은 입력값에는 Scanner를 사용해도 무관할 것으로 생각이 들었다.

마무리

상황에 따라 Scanner와 BufferedReader를 적절히 사용해야 겠지만 속도 차이가 영향을 주게되는 경우엔 BufferedReader를 사용하여 코드를 작성해야겠다.


사용법

BufferedReader

BufferedReader bf = new BufferedReader(new InputStreamReader(System.in)); //선언
String s = bf.readLine(); //String
int i = Integer.parseInt(bf.readLine()); //Int

선언은 위에 있는 예제처럼 진행하면 된다.
그러면 아래와 같이 import가 된다.

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
  1. 입력은 readLine() 시 리턴값을 String으로 고정되기에 String이 아닌 다른 타입으로 입력을 받으려면 꼭 형변환을 해줘야 한다.

  2. 예외처리를 꼭 해주어야 한다. readLine()을 할때마다 try & catch를 활용하여 예외처리를 해주어도 되지만 대개 throws IOException 을 통하여 작업한다.

throw 이용 시

(1) 클래스를 import해주어야 한다.
import java.io.IOException;
(2) main 클래스 옆에 throws IOException를 작성한다.
public static void main(String[] args) throws IOException {}

StringTokenizer st = new StringTokenizer(s); //StringTokenizer인자값에 입력 문자열 넣음
int a = Integer.parseInt(st.nextToken()); //첫번째 호출
int b = Integer.parseInt(st.nextToken()); //두번째 호출

String array[] = s.split(" "); //공백마다 데이터 끊어서 배열에 넣음

Read한 데이터는 Line 단위로 나눠지기에 공백단위로 데이터를 가공하려면 따로 작업을 해주어야하는데, 위의 두가지 방법이 대표적이다.

  1. StringTokenizer에 nextToken() 함수를 쓰면 readLine()을 통해 입력받은 값을 공백단위로 구분하여 순서대로 호출할 수 있다.
  2. String.split() 함수를 활용하여 배열에 공백단위로 끊어서 데이터를 넣고 사용하는 방식이다.

BufferedReader 클래스의 메인 함수들

Modifier and TypeMethod and Description
voidclose()
입력 스트림 닫고, 사용하던 자원을 해제
voidmark(int, readAheadLimit)
스트림의 현재 위치를 마킹
intread()
한 글자만 읽어 정수형으로 반환 (e.g, '3'을 읽어 정수형인 (int)'3'=51 로 반환
StringreadLine()
한줄을 읽음
booleanready()
입력 스트림이 사용할 준비가 되었는지 확인 (1이 준비 완료)

BufferedWriter

BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));   //할당된 버퍼에 값 넣어주기
String s = "abcdefg";   //출력할 문자열
bw.write(s);   //버퍼에 있는 값 전부 출력
bw.newLine(); // 줄바꿈
bw.flush();   //남아있는 데이터를 모두 출력시킴
bw.close();   //스트림을 닫음
  • bw.write에는 System.out.println(); 과 같이 자동개행기능이 없기 때문에 개행을 해주어야 할 경우에는 newLine(); 혹은 bw.write("\n"); 을 통해 따로 처리해주어야 한다.
  • BufferedWriter 의 경우 버퍼를 잡아 놓았기 때문에 반드시 flush() / close() 를 호출하여 뒤처리를 해줘야 한다. 그리고 close() 를 하게 되면, 출력 스트림을 아예 닫아버리기 때문에 한번 출력 후, 다른 것도 출력하고 한다면 flush()를 사용하면 된다.

BufferedWriter 클래스의 메인 함수들

Modifier and TypeMethod and Description
voidclose()
스트림을 닫음. 닫기 전 flush()
voidflush()
스트림을 비움
voidnewLine()
개행 문자 역할
voidwrite(char[] buf, int offset, int length)
버퍼 offset 위치부터 length 크기 만큼 write
voidwrite(int c)
한 글자씩 쓰기
voidwrite(String s, int offset, int length)
문자열에서 offset에서부터 일정 길이만큼 write

참고 블로그
https://st-lab.tistory.com/41
https://rlakuku-program.tistory.com/33
https://velog.io/@langoustine/Scanner-VS-BufferedReader

profile
java를 잡아...... 하... 이게 맞나...

0개의 댓글