백준 코딩 테스트에서 입력 처리를 위해 BufferedReader를 사용하고 있다. 속도 면에서 유리하다는 이유로 사용했지만, 정확한 이유는 알지 못했다.
최근 피드백에서 Scanner를 사용하는 것이 유리한 경우도 있다는 이야기를 듣고, 이번 기회에 Scanner와 BufferedReader의 차이점을 알아보려 한다.
1. Scanner
Scanner 클래스는 java.util
패키지에 포함된 클래스이다. 표준 입력(System.in)을 파싱 하여 기본 데이터 타입 및 문자열로 반환하는 기능을 제공한다.
특징
- java.util 패키지 안에 있다. (java.util.Scanner)
- 공백 및 개행을 기준으로 읽는다. (토큰단위)
- 기본 데이터 타입을 지원한다. (nextInt(), nextDouble() 등)
- 버퍼의 사이즈가 1024byte(1KB) 이다.
- UnChecked(Runtime) Exception으로 별도로 예외 처리를 명시할 필요가 없다.
- Thread unsafe 성질을 지니기에 멀티스레드 환경에서 문제가 발생할 수 있다.
- 속도가 느리다.
- 소규모 입력 처리에 적합하다.
사용 방법
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 정수 입력
System.out.print("Enter an integer: ");
int intValue = sc.nextInt();
// 실수 입력
System.out.print("Enter a double: ");
double doubleValue = sc.nextDouble();
// 문자열 입력 (공백 포함)
sc.nextLine(); // 버퍼 비우기 (nextInt(), nextDouble() 호출 후 필요)
System.out.print("Enter a string: ");
String strValue = sc.nextLine();
// 출력
System.out.println("Integer: " + intValue);
System.out.println("Double: " + doubleValue);
System.out.println("String: " + strValue);
sc.close(); // 자원 해제
}
}
위와 같이 System.in을 통해 Scanner 객체를 생성한다. 정수, 실수, 문자열 같이 다양한 타입을 지원한다.
System.in이란?
사용자로부터 입력을 받기 위한 입력 스트림이다. Scanner 클래스뿐 아니라 다른 입력 클래스들도 System.in을 통해 사용자 입력을 받아야 한다.
2. BufferedReader
BufferedReader는 java.io
패키지에 포함된 클래스이다. 문자 입력 스트림을 처리하며, 버퍼를 사용해 대량 데이터를 효율적으로 읽는다.
특징
- java.io 패키지에 속한다. (import java.io.BufferedReader)
- 줃 단위로 구분한다.
- 기본 데이터 타입을 지원하지 않고 String 형태로 가져온다.
- 버퍼의 사이즈가 8192byte(8KB)이다.
- Checked Exception으로 반드시 예외 처리를 명시해야 한다.(I/O Exception을 throw 하거나 try/catch 해야 한다.)
- Thread safe 성질을 지니기에 멀티스레드 환경에서도 안전하다.
- 버퍼가 가득 차거나 개행문자가 나타나면 버퍼의 내용을 한 번에 프로그램으로 전달하기에 Scanner보다 소요되는 시간을 절약할 수 있다.
사용 방법
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));
// 정수 입력
System.out.print("Enter an integer: ");
int intValue = Integer.parseInt(br.readLine());
// 실수 입력
System.out.print("Enter a double: ");
double doubleValue = Double.parseDouble(br.readLine());
// 문자열 입력
System.out.print("Enter a string: ");
String strValue = br.readLine();
// 출력
System.out.println("Integer: " + intValue);
System.out.println("Double: " + doubleValue);
System.out.println("String: " + strValue);
}
}
위와 같이 BufferedReader는 매개변수로 InputStreamReader를 사용하여 객체를 생성한다.
InputStreamReader 란?
문자 기반의 보조 스트림으로써 바이트 기반 스트림을 문자 기반 스트림으로 연결시켜 주는 역할을 한다.
또한 io 패키지 내 클래스를 이용하기 때문에 예외처리를 위한 IOException을 선언해줘야 한다.
자동 형변환을 지원하지 않기 때문에 위와 같이 문자열로 받은 후 형변환을 진행하여야 한다.
그럼 왜 Scanner가 느리다고 할까?
1. 정규 표현식 기반 입력 처리
Scanner는 입력 데이터를 처리할 때 정규 표현식을 사용한다. 예를 들어, nextInt(), next()와 같은 메서드는 내부적으로 입력 데이터를 정규식을 검사하여 원하는 타입으로 변환한다.
정규 표현식을 하여 보내주면 편리할 수 있지만, 문자열 매칭 과정에서 오버헤드가 발생하게 된다. 그러므로 입력 데이터가 많을수록 이 과정이 반복되어 성능이 감소할 수 있다.
2. 공백/토큰화 처리
Scanner는 기본적으로 입력 데이터를 토큰화한다. BufferedReader가 한 줄 단위로 받고 이후 필요에 따라 토큰화를 진행하는 것과 달리 입력 스트림에서 데이터를 읽은 다음 공백, 탭, 줄 바꿈 등을 기준으로 자동으로 구분한다.
이 과정에서 입력 데이터를 한 글자씩 읽고, 구분자를 탐색하며 처리하므로 시간이 걸리게 된다.
3. 입력 버퍼 크기
Scanner는 내부적으로 작은 크기의 버퍼(약 1KB)를 사용한다. 작은 버퍼 크기를 사용하기 때문에 대량 데이터를 처리할 경우 자주 읽기 작업이 발생하여 성능이 저하된다.
반면에, BufferedReader는 상대적으로 더 큰 버퍼(약 8K)를 사용하여 데이터를 한 번에 읽어와 버퍼에 저장한 뒤, 프로그램이 이 데이터를 메모리에서 읽도록 처리하여 성능상 이점이 있다.
4. 입력과 데이터 변환을 동시 처리
Scanner는 앞서 이야기했듯이 데이터 변환처리를 해준다. 예를 들면 nextInt()를 호출하면 입력 데이터를 읽고, 정규식으로 검증하며. int 타입으로 변환해야 한다. 이렇듯 여러 단계를 한 번에 수행하기 때문에 속도가 느려진다.
BufferedReader는 입력을 문자열로만 반환하며, 사용자 필요에 따라 명시적 형변환을 사용하기 때문에 단순하고 빠르다.
성능차이
아래는 Scanner와 BufferedReader를 사용한 성능 비교를 실행하는 프로그램이다. 이 코드는 동일한 입력 데이터를 처리하며 두 방식의 실행 시간을 측정한다.
package fc;
import java.io.*;
import java.util.Scanner;
public class PerformanceComparison {
public static void main(String[] args) throws IOException {
// Generate test input (1 to 100,000)
StringBuilder inputBuilder = new StringBuilder();
for (int i = 1; i <= 100000; i++) {
inputBuilder.append(i).append(" ");
}
String testInput = inputBuilder.toString();
// Scanner simulation
long scannerStartTime = System.currentTimeMillis();
scannerSimulation(testInput);
long scannerEndTime = System.currentTimeMillis();
// BufferedReader simulation
long bufferedReaderStartTime = System.currentTimeMillis();
bufferedReaderSimulation(testInput);
long bufferedReaderEndTime = System.currentTimeMillis();
// Calculate and print performance results
long scannerTime = scannerEndTime - scannerStartTime;
long bufferedReaderTime = bufferedReaderEndTime - bufferedReaderStartTime;
System.out.println("Scanner Time: " + scannerTime + " ms");
System.out.println("BufferedReader Time: " + bufferedReaderTime + " ms");
}
// Simulate Scanner-style input processing
private static void scannerSimulation(String input) {
Scanner scanner = new Scanner(input);
long sum = 0;
while (scanner.hasNextInt()) {
sum += scanner.nextInt();
}
scanner.close();
System.out.println("Scanner Result: " + sum);
}
// Simulate BufferedReader-style input processing
private static void bufferedReaderSimulation(String input) throws IOException {
BufferedReader br = new BufferedReader(new StringReader(input));
String[] tokens = br.readLine().split(" ");
long sum = 0;
for (String token : tokens) {
sum += Integer.parseInt(token);
}
br.close();
System.out.println("BufferedReader Result: " + sum);
}
}
1에서 100,000까지 공백으로 구분되는 StringBuilder를 만들어 그 입력값을 각각 Scanner와 BufferedReader로 실행한 코드이다.
결과는 다음과 같다. 두 코드의 결과값은 같으나 시간에 차이가 나는 것을 알 수 있다.
Scanner Result: 5000050000
BufferedReader Result: 5000050000
Scanner Time: 309 ms
BufferedReader Time: 56 ms
간단한 입력을 받을 경우, 두 방식의 차이는 크지 않을 것이다. 하지만 입력 데이터가 많아질수록 차이는 커지게 된다. 위에 설명한 듯이 BufferedReader는 버퍼 기븐으로 데이터를 한꺼번에 읽어와 처리하기 때문에 상대적으로 안정적인 속도를 유지하며, Scanner는 내부적으로 다양한 과정과 작은 버퍼의 사용으로 추가적인 오버헤드가 발생하게 된다.
결론
Scanner 활용
- 적합한 경우:
- 소규모 입력 처리:
- 입력 데이터가 많지 않거나 데이터 타입이 다양하게 섞여 있는 문제
- 데이터를 개별적으로 읽는 경우:
- 정수와 문자열 입력이 번갈아 나오는 문제에서 간단하게 사용할 수 있습니다
- 코드 가독성이 중요할 때:
- 간결한 코드로 데이터를 읽고자 할 때 적합
- 소규모 입력 처리:
BufferedReader 활용
- 적합한 경우:
- 대량 데이터 처리:
- 입력 크기가 큰 문제(예: 10만 개 이상의 숫자)
- 시간 초과 위험이 있는 문제:
- 빠른 처리 속도가 필요한 경우
- 줄 단위로 데이터를 읽어야 할 때:
- 여러 줄로 주어진 입력 데이터를 처리하는 문제
- 대량 데이터 처리:
물론 둘을 번갈아 사용하기가 쉽지 않다면 BufferedReader를 활용해 푸는 것도 나쁘지 않은 것 같다.
'코딩테스트 > 이론' 카테고리의 다른 글
[코딩테스트] 경우의 수, 합의 법칙, 곱의 법칙 (1) | 2024.11.30 |
---|---|
[JAVA] 출력문 println, print, printf 어떤 차이가 있을까? (0) | 2024.11.29 |
[JAVA] 문자를 숫자로 변환하기 Char to Int (0) | 2024.11.28 |
[JAVA] 정수와 문자열 입력값의 각 자리수 합 구하기 (0) | 2024.11.28 |
[코딩테스트] 문자열 활용 - 문자열 뒤집기(특정단어) with Java (0) | 2024.08.30 |