몇 달 전 부터, 알고리즘을 풀며 Scanner 및 System.out.prinln() 보다 속도가 빠르다는 이유로 BufferedReader와 BufferedWriter을 사용해왔다. 하지만, 이 기능들을 사용하면 메모리와 시간만 절약하다는 것을 알 수 있었지만, 무슨 이유에서 사용하는 것인지 그리고 IOException 예외처리는 왜 하는 것인지 알고 싶었다.
public static void main(String[] args) throws IOException {
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringTokenizer st = new StringTokenizer(bf.readLine());
int n = Integer.parseInt(st.nextToken());
int m = Integer.parseInt(st.nextToken());
int[] num = new int[m];
st = new StringTokenizer(bf.readLine());
for (int i = 0; i < m; i++) num[i] = Integer.parseInt(st.nextToken());
bw.write(solution(n, m, num) + " ");
bw.flush(); bw.close(); bf.close();
}
Scanner는 입력문자를 space, enter로 구분하고 BufferedReader는 enter로만 구분하고 버퍼 공간에 저장해 두었다가 한 번에 내보내는 방식이다. BufferedReader & BufferedWriter는 버퍼링을 통해 데이터를 읽고 쓰기 때문에 속도가 빠른 것을 알 수 있었는데, 버퍼링은 데이터를 작은 덩어리로 여러 번 읽고 쓰는 대신, 한 번에 큰 덩어리로 읽고 쓰는 방식이라고 한다.
그리고, Scanner는 휘발성이지만, BufferedReader는 String으로 가공하여 내보내는 작업이 필요하다고 한다. Scanner는 버퍼 사이즈가 1024 char 인 반면, BufferedReader는 8192 char 로 입력 값을 많이 받을 수 있다고 한다. 또한, 동기화가 가능하여 멀티 쓰레드 환경에선 유리한 기능이라고 볼 수 있다.
라인 단위 처리(Line-by-line processing)에서도 차이를 볼 수 있었는데, Scanner와 System.out.println() 는 라인 단위로 입,출력을 실행하지만, parsing 작업(오버헤드의 위험) 및 새로운 라인으로 이동하는 등 작업을 수행하는 반면, BufferedReader와 BufferedWriter는 라인 단위로 입력을 읽어오고 데이터를 버퍼에 모아 한 번에 출력하기 때문에 효율적이라는 결과를 볼 수 있었다.
👉 정리하자면,
- 버퍼링(Buffering)
- 입출력 스트림(Input/Output Stream)
- 라인 단위 처리 (Line-by-line processing)
보통, IOException을 쓰지 않을 때 오류나는 부분은 bf.readLine() 을 해주었을 때 빨간 줄이 발생한다. 인터넷에 검색해보고 readLine()메서드를 뜯어봤는데,
String readLine(boolean ignoreLF) throws IOException {
StringBuilder s = null;
int startChar;
synchronized (lock) {
ensureOpen();
boolean omitLF = ignoreLF || skipLF;
bufferLoop:
for (;;) {
if (nextChar >= nChars)
fill();
if (nextChar >= nChars) { /* EOF */
if (s != null && s.length() > 0)
return s.toString();
else
return null;
}
boolean eol = false;
char c = 0;
int i;
/* Skip a leftover '\n', if necessary */
if (omitLF && (cb[nextChar] == '\n'))
nextChar++;
skipLF = false;
omitLF = false;
charLoop:
for (i = nextChar; i < nChars; i++) {
c = cb[i];
if ((c == '\n') || (c == '\r')) {
eol = true;
break charLoop;
}
}
startChar = nextChar;
nextChar = i;
if (eol) {
String str;
if (s == null) {
str = new String(cb, startChar, i - startChar);
} else {
s.append(cb, startChar, i - startChar);
str = s.toString();
}
nextChar++;
if (c == '\r') {
skipLF = true;
}
return str;
}
if (s == null)
s = new StringBuilder(defaultExpectedLineLength);
s.append(cb, startChar, i - startChar);
}
}
}
함수 내에서 synchronized 함수를 호출하는 것을 볼 수 있다.
❓synchronized?
synchronization01 - 링크텍스트
synchronization02 - 링크텍스트
한 마디로 처음에 설명했던 것 처럼, 동기화를 위해 동시 호출을 막아주는 메서드이며 다른 코드블럭은 대기 후 먼저 실행된 코드가 다 호출된 이후에 들어가서 실행된다고 한다. BufferedReaderd에서는 readLine에 접근하여 InputStreamReader가 열렸는지 확인해준다고 한다.
private void ensureOpen() throws IOException {
if (in == null)
throw new IOException("Stream closed");
}
ensureOpen 함수에서는 in이 null일 경우 IOException을 던진다. 즉, readLine()에 입력된 값이 null일 때 대비하여 예외처리를 해준다고 한다. null인 경우, 변수를 선언할 때 int a = null 선언한 이후 a 라는 변수에 다른 입력 값이 들어가지 않은 상태로 null 그대로 전달 된 경우 IOException을 반환한다고 한다.