[JAVA] BufferedReader와 BufferedWriter 사용법

2024. 2. 13. 01:27언어(Language)/JAVA

나는 java로 코테를 풀 때 항상 Scanner를 사용하여 입력을 받았었다. 하지만 Scanner보다 BufferedReader를 사용하는 것이 훨씬 빠르다는 것을 알게 되어 개념을 정리하고자 한다.

 


Scanner vs BufferedReader

 


Scanner

먼저 Scanner를 살펴보자. 대부분 자바를 처음 배울 때, Scanner를 이용한 입출력을 먼저 배우게 될 것이다.

Scanner는 띄어쓰기개행 문자를 경계로 하여 입력 값을 인식한다. 그렇기 때문에 따로 가공할 필요가 없어 편리하다.

 

가공할 필요가 없다는 뜻은, 가령 int형 변수를 입력 받고자 하면

ex) int x = scanner.nextInt()와 같이 바로 원하는 타입의 입력을 받을 수 있다.

하지만 이와 달리 BufferedReader는 입력 받은 데이터가 String으로 고정되어 있기 때문에 입력 받은 데이터를 원하는 타입으로 가공하는 작업이 필요하다.

이럼에도 BufferedReader를 사용하는 이유는 Scanner는 버퍼 사이즈가 1024char이기 때문에 많은 입력을 필요로 할 경우에는 성능상 좋지 못한 결과를 불러오기 떄문이다.


BufferedReader / BufferedWriter

BufferedReader와 BufferedWriter는 버퍼를 사용하여 읽기와 쓰기를 하는 함수이다.

 

버퍼를 사용하지 않는 입력은 키보드의 입력이 키를 누르는 즉시 바로 프로그램에 전달된다.

반면 버퍼를 사용하는 입력은 키보드의 입력이 있을 때마다 한 문자씩 버퍼로 전송한다.

버퍼가 가득 차거나 혹은 개행 문자가 나타나면 버퍼의 내용을 한 번에 프로그램에 전달한다.

 

한 번 버퍼를 거쳐 출력되는 것보다 키보드의 입력을 받는 즉시 출력하는 것이 더 빠른 것이 아닌가 생각할 수 있다.

하드디스크는 속도가 느리다. 그리고 외부 장치(키보드, 모니터 등)와 데이터 입출력도 생각보다 시간이 오래 걸린다.

그렇기 때문에 키보드의 입력이 있을 때마다 바로 이동시키는 것보다는 중간에 버퍼를 두어 한 번에 묶어 보내는 것이 더 효율적이고 빠른 방법이다.

 

쓰레기통을 비우는 일이라고 생각하면 이해가 쉽다. 쓰레기가 생길 때마다 하나하나 밖에 내다버리는 것보다, 집의 쓰레기통에 하나하나 모았다가 꽉 차면 한 번에 버리는 것이 훨씬 효율적인 것과 비슷한 개념이라고 생각하면 된다.


BufferedReader

앞서 말한 Scanner와 달리 BufferedReader는 개행 문자만 경계로 인식하고 입력 받은 데이터가 String으로 고정된다. 그렇기 때문에 따로 데이터를 가공해야 하는 경우가 많다. 하지만 Scanner보다 속도가 빠르다.

 

밑의 표는 BufferedReader와 Scanner의 속도 차이를 잘 보여주는 예시이다.

10,000,000개의 0~1023 범위의 정수를 한 줄씩 읽고, 입력으로 받은 정수의 합을 출력하는 프로그램을 각각 BufferedReader와 Scanner로 구현할 때의 수행시간이다.

입력 방식 수행시간(초)
java.util.Scanner 6.068
java.util.BufferedReader 0.934

 

그리고 버퍼 사이즈도 Scanner가 1024 char인데 비해, BufferedReader는 8192 char(16,384byte)이기 때문에 입력이 많을 때 BufferedReader가 유리하다.

 

또한 BufferedReader는 동기화 되기 때문에 멀티 쓰레드 환경에서 안전하고, Scanner는 동기화 되지 않기 때문에 멀티 쓰레드 환경에서 안전하지 않다.

 

 

- BufferedReader 특징

1. 엔터(줄바꿈)만을 경계값으로 인식한다.

2. 오직 String 타입만 다룬다.

3. 매우 큰 버퍼 사이즈(8KB) 때문에 속도 면에서 매우 빠르다.

4. 예외처리를 꼭 해주어야 한다.

 

즉, BufferedReader는 문자열을 단순히 읽고 저장하는 것이 중점이며 Scanner는 문자열을 구분하고 분석하는 것이 중점이 된다. 그러므로입력 데이터를 가공없이 빠르게 저장할 때에는 BufferedReader를 사용하고 입력 데이터를 가공할 때에는 Scanner를 사용하는 것이 효율적이다.

 

- BufferedReader 사용법

 

BufferedReader를 사용하기 위해선 다음의 import가 추가적으로 필요하다.

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

import java.io.*; //한 번에 선언 가능

 

 

앞서 말한 것과 같이 BufferedReader를 사용할 때에는 예외처리를 꼭 해주어야 한다. 이때 방법은 두 가지로 readLine()을 할 때마다 매번 try & catch를 사용해서 예외처리를 해주거나 throws IOException을 이용하여 예외처리를 해주면 된다.

일반적으로는 throws IOException을 이용하며 예외처리를 해준다.

 

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

 

 

마지막으로 다음과 같은 방식으로 선언하고 readLine()을 통해 입력을 받아오면 된다.

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

String str = br.readLine();

 

 

전체적인 코드는 이와 같다.

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

public class Main {
	public static void main(String[] args) throws IOException {   	
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); //선언

		String s = br.readLine();
		int i = Integer.parseInt(br.readLine());
    }
}

 

입력 받은 값은 String으로 리턴 값이 고정되어 있기 때문에 다른 타입으로 입력을 받고자 한다면 반드시 형변환이 필요하다.

 

 

- 데이터 가공

 

BufferedReader를 통해 읽어온 데이터는 개행문자 단위(line 단위)로 나누어진다. 만약 이를 공백 단위로 데이터를 가공하고자 한다면 따로 작업을 해주어야 한다. 이때 사용하는 것이 StringTokenizer나 String.split() 함수이다.

 

//StringTokenizer
import java.util.StringTokenizer;

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());

int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());

//String.split() 함수
String arr[] = s.split(" ");

 

StringTokenizer의 nextToken() 함수를 사용하면 readLine()을 통해 입력 받은 값을 공백 단위로 구분하여 순서대로 호출할 수 있다.

String.split() 함수를 사요아면, 배열에 공백 단위로 끊어 데이터를 저장하여 사용할 수 있다.

이와 관련된 글은 이후에 추가로 작성할 예정이다.

 

- BufferedReader 클래스의 메인 함수들

Modifier and Type Method Description
void close() 스트림을 닫고 스트림에 저장되어 있던 자료를 날린다.
Stream<String> lines() 스트림을 반환한다.
void mark(int readAheadLimit) 스트림의 현재 위치를 마크한다.
boolean markSupported() 현재 스트림에서 mark()의 가능 여부를 알려준다.
int read() 한 글자만 읽는다.
int read(char[] cbuf, int off, int len) cbuf[]에서 off번째 글자부터 len 길이만큼 문자를 읽는다.
String readLine() 한 줄을 읽는다.
boolean ready() 스트림이 입력을 읽을 준비가 되었는지를 알려준다.
void reset() 가장 최근 마크된 부분으로 스트림을 돌린다.
long skip(long n) n개의 문자를 건너뛴다.

 

 

+) close()의 경우 자바에서는 Garbage Collector/Collection에 의해서 내부 객체 및 리소스들이 정리되기 때문에 굳이 close()를 콜하지 않아도 이후에 자동으로 정리가 되어 문제가 발생하지 않는다. 하지만 Garbage Collector가 실행되고 완료될 때까지 스트림에 할당된 리소스들이 그대로 유지되기 때문에 최적화 관점에서 close()를 콜해 스트림을 종료해주는 것이 좋다.

 

+) read()의 경우 만약 숫자 8을 입력하면 8이 반환되는 것이 아니라, 8을 가리키는 아스키코드, 유니코드의 값이 나오는 것이다.

 

+) reset()의 경우 만약 마크된 지점이 없을 때에는 가장 처음으로 돌아간다.


System.out.println() vs BufferedWriter

System.out.println()과 BufferedWriter은 모두 문자열을 출력하는 클래스이지만 각 클래스가 지향하는 바가 다르므로 각 클래스의 특징에 대해 이해하고 상황에 맞게 사용하는 것이 좋다.


System.out.println()

System.out.println()은 Scanner와 마찬가지로 사용자에게 편리성을 제공해준다. 일단 println()에는 출력함수 print()와 줄바꿈함수 newLine()을 포함하고 있다. 또한 파라미터에 다양한 데이터 타입이 올 수 있도록 오버로딩 되어 있다.

 

즉, 단순히 파라미터만을 출력하는 것이 아니라 줄바꿈 기능이 포함되어 있으며 다양한 데이터 타입을 출력할 수 있도록 오버로딩 되어 있어 매우 편리하게 사용할 수 있다.

 

하지만 이러한 편리성은 속도 면에서는 효율이 떨어질 수 밖에 없다. 앞서 말한 이유 이외에도 print()의 속도 저하 이유에는 다양한 이유가 존재한다.


BufferedWriter

BufferedWriter은 println()과 같은 편의성은 존재하지 않지만 속도 면에서는 뛰어나다.

버퍼에 출력 문자열을 저장했다가 버퍼가 다 찼거나, 사용자의 요청이 들어오면 버퍼에 저장되어 있던 문자열을 한 번에 출력하는 방식이다.

 

즉, System.out.println()은 문자열을 구분하고 분석하는 것이 중점이 되고, BufferedWriter은 단순히 저장하여 출력하는 것이 중점이 된다. 그러므로 데이터를 빠르게 출력하고자 할 때에는 BufferedWriter을 사용하고 다양한 데이터 타입을 출력하거나 다양한 문자열 구분 기능을 이용하고자 할 때에는 System.out.println()을 사용하는 것이 효율적이다.

 

- BufferedWriter 사용법

 

BufferedWriter을 사용하기 위해선 다음의 import가 추가적으로 필요하다.

import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.io.IOException;

import java.io.*; //한 번에 선언 가능

 

 

BufferedWriter도 BufferedReader와 같이 예외처리를 꼭 던져야 한다. 이때 readLine()을 할 때마다 매번 try & catch를 이용하여 예외처리를 해주거나 throws IOException을 이용하여 예외처리를 해주면 된다.

일반적으로는 throws IOException을 이용하여 예외처리를 해주는 방법을 많이 사용한다.

 

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

 

 

그 후 다음과 같은 방식으로 선언한다.

BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

 

 

write()를 통해 버퍼에 출력문을 저장한다

+) writer()로 출력되는 것이 아니라 버퍼에 저장하는 것이다.

bw.write("Hello World!");

 

+) BufferedWriter에는 println()과 달리 개행기능이 포함되어 있지 않기 때문에 줄바꿈을 할 경우에는 write("\n")을 하여 버퍼에 추가해주거나 newLine()을 콜해주면 된다.

//write("\n") 예시
bw.write("Hello World! \n");

//newLine(); 예시
bw.write("Hello World!");
bw.newLine();

 

 

flush()를 통해 버퍼 안에 저장되어 있는 모든 문자열을 출력한다. 만약 버퍼가 다 찬 경우엔 자동으로 flush()를 통해 모든 문자열이 출력된다.

bw.flush();

 

 

BufferedWriter의 사용이 끝났다면 close()를 통해 스트림을 종료해준다. close()를 하기 전에는 혹시라도 버퍼에 데이터가 남아있을 수 있기 때문에 꼭 flush()를 통해서 스트림을 비워준다.

bw.close();

 

 

전체 코드는 아래와 같다.

import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.io.IOException;

public class Main {
	public static void main(String[] args) throws IOException {
    	BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        
        bw.writer("Hello World!");
        bw.flush();
        bw.close();
    } 
}

 

 

- BufferedWriter 클래스의 메인 함수들

Modifier and Type Method Description
void close() 스트림을 닫는다. 닫기 전에 flush()를 먼저 콜해준다.
void flush() 스트림을 비운다.
void newLine() 줄바꿈, 개행한다.
void write(char[] cbuf, int off, int len) cbuf[]의 off 위치부터 len 길이만큼 버퍼에 저장한다.
void write(int c) 한 글자 c의 코드값을 버퍼에 저장한다.
void write(String s, int off, int len) 문자열 s에서 off 위치부터 len길이만큼 버퍼에 저장한다.

 

 

+) close()의 경우 자바에서는 Garbage Collector/Collection에 의해서 내부 객체 및 리소스들이 정리되기 때문에 굳이 close()를 콜하지 않아도 이후에 자동으로 정리가 되어 문제가 발생하지 않는다. 하지만 Garbage Collector가 실행되고 완료될 때까지 스트림에 할당된 리소스들이 그대로 유지되기 때문에 최적화 관점에서 close()를 콜해 스트림을 종료해주는 것이 좋다.

 

+) write(int c)의 경우 만약 숫자 8을 파라미터로 입력하면 8이 버퍼에 저장되는 것이 아니라 8을 가리키는 아스키 코드, 유니코드의 값이 버퍼에 저장되는 것이다.