주요 메서드 정리
구분 | 기능 |
Scanner |
|
BufferedReader |
|
StringTokenizer |
|
split() |
|
BufferedReader
ℹ️ 데이터를 읽을 때 버퍼링을 통해 읽기 성능을 크게 향상시킵니다.
- 내부적으로 버퍼(buffer)를 사용하여 입력을 처리합니다.
- 버퍼를 사용하는 이유는 무엇일까요?
: 데이터가 입출력 장치와 CPU 사이에서 즉각적으로 전달되는 것을 방지하고, 데이터를 일시적으로 저장하여 더 큰 단위로 처리할 수 있게 도와줍니다. 이를 통해 입출력 장치의 느린 속도와 프로세서의 빠른 속도 간 속도차이를 완화할 수 있습니다.- 버퍼 미사용 시: 키보드와 같은 입력 장치로부터 데이터 입력 → 입력이 발생할 때마다 데이터가 프로그램에 즉시 전달됩니다. → 입출력 동작이 잦아져 CPU가 빈번하게 I/O 작업을 처리해야 하므로, 전반적인 성능 저하가 발생할 수 있습니다.
- 버퍼 사용 시: 키보드의 입력이 있을 때 데이터는 버퍼에 임시로 저장 → 버퍼가 일정 크기만큼 가득 차거나 특정 조건(개행 문자 등)이 충족 → 버퍼에 저장된 데이터가 한 번에 프로그램으로 전달됩니다. 이를 통해 입출력 호출 횟수를 줄여 CPU가 더 큰 단위로 데이터를 처리할 수 있게 되어 성능이 향상됩니다. (특히, 대량의 데이터를 읽고 쓸 때 유용합니다.)
- 입출력 장치의 느린 속도와 프로세서의 빠른 속도 간 속도 차이?
- 하드디스크의 느린 속도: 하드디스크는 기계적인 특성으로 인해 접근 시간이 오래 걸리고, 전송 속도가 느립니다. 이 때문에 데이터를 하나씩 읽고 쓰는 방식은 매우 비효율적입니다.
- 하드디스크의 기계적인 특성
: 하드디스크는 자기 디스크 위에 데이터를 기록하는 기계적인 장치입니다. 데이터를 읽고 쓰기 위해서는 다음과 같은 물리적인 동작이 필요합니다.
1. 헤드 이동(읽고 쓰려는 데이터가 위치한 트랙으로 헤드를 이동시키는 과정)
→ 2. 회전(디스크가 회전하여 헤드 아래로 원하는 데이터가 오도록 하는 과정)
→ 3. 데이터 읽기/쓰기(헤드가 데이터를 읽거나 쓰는 과정)
이러한 기계적인 동작은 전자적인 신호 처리에 비해 상당히 느리기 때문에 하드디스크의 접근 시간이 길어지고, 전송 속도가 느려지는 주요 원인이 됩니다.
- 하드디스크의 기계적인 특성
- CPU와의 속도 차이: CPU는 데이터를 빠르게 처리할 수 있지만, 하드디스크나 키보드와 같은 외부 장치와 데이터를 주고받을 때 속도 병목이 발생합니다. 버퍼링을 통해 이러한 속도 차이를 완화함으로써 프로그램이 더 많은 데이터를 효율적으로 처리할 수 있습니다.
- 속도 병목 현상
: 시스템의 특정 부분에서 처리 속도가 느려져 전체 시스템의 성능이 저하되는 현상을 말합니다. 하드디스크의 경우, CPU가 데이터를 빠르게 처리할 수 있음에도 불구하고 하드디스크의 느린 속도 때문에 데이터 입출력 과정에서 속도 병목 현상이 발생합니다. - 속도 병목 현상이 발생하는 이유
- 기계적인 한계: 하드디스크는 물리적인 제약으로 인한 데이터 접근 속도가 느립니다.
- 데이터 위치: 하드디스크 내 데이터가 분산되어 저장되어 있기에 헤드가 이동해야 하는 거리가 길어질 수 있습니다.
- 데이터 전송 속도: 데이터 전송 속도가 CPU의 처리 속도에 비해 상대적으로 느립니다.
- 속도 병목 현상
- 하드디스크의 느린 속도: 하드디스크는 기계적인 특성으로 인해 접근 시간이 오래 걸리고, 전송 속도가 느립니다. 이 때문에 데이터를 하나씩 읽고 쓰는 방식은 매우 비효율적입니다.
- 버퍼를 사용하는 이유는 무엇일까요?
- 파일이나 콘솔로부터 데이터를 읽을 때 데이터를 한 문자씩 읽는 대신 일정 크기의 버퍼에 저장한 후 한번에 읽어 들이기 때문에, 여러 번 I/O 작업을 수행하는 것보다 더 적은 입출력 작업이 필요합니다. 입출력 성능이 크게 향상됩니다.
Scanner vs. BufferedReader
💡작은 규모의 입력 처리나 편리한 데이터 타입 변환이 필요할 경우 Scanner
대규모 데이터 처리나 성능 최적화가 필요한 경우 BufferedReader를 사용하는 것이 좋습니다.
- 모두 Java에서 입력을 처리할 때 사용하지만, 두 클래스는 입출력 방식과 성능에서 큰 차이가 있습니다.
- Scanner와 BufferedReader의 속도 차이 예시
: 10,000,000개의 0~1023 범위의 정수를 한 줄씩 읽고, 입력으로 받은 정수의 합을 출력하는 프로그램을 각각 BufferedReader와 Scanner로 구현할 때의 수행시간은 아래와 같습니다.
입력 방식 | 수행시간(초) |
java.util.Scanner | 6.068 |
java.io.BufferedReader | 0.934 |
비교 | Scnnaer | BufferedReader |
입력 방식 | 토큰화 기반 입력 처리 - 입력을 토큰 단위로 처리합니다. - next(), nextInt(), nextLine() 등의 메서드를 사용해 구분자(기본적으로 공백)를 기준으로 입력을 자동으로 나눕니다. |
버퍼링을 통한 입력 처리 - 데이터를 라인 단위로 읽습니다. - readLine() 메서드를 사용해 한 줄씩 데이터를 입력 받습니다. |
다양한 데이터 타입 처리 : int, double, float, String 등 다양한 타입의 입력을 자동으로 변환해 주는 메서드를 제공합니다. |
단순 문자 입력 처리 - 문자열 입력만 지원하고, Scanner처럼 데이터 타입별로 자동 변환해 주는 기능이 없습니다. - 숫자나 다른 형식의 입력을 처리하려면 직접 파싱을 해야 합니다. |
|
정규 표현식 지원 : 구분자를 지정할 때 정규 표현식을 사용할 수 있어 다양한 패턴을 쉽게 처리할 수 있습니다. |
대용량 데이터 처리 : 데이터를 한 번에 읽고 처리하는 구조로 되어 있어, 반복적인 I/O 호출이 발생하지 않도록 최적화되어 있습니다. |
|
성능 | 느림 : 정규 표현식으로 사용해 토큰을 구분하므로 상대적으로 성능이 느립니다. |
빠름 : 데이터를 버퍼에 저장한 후 처리하기 때문에 I/O 작업이 덜 빈번하게 발생합니다. 따라서 대용량 데이터를 빠르게 처리할 수 있습니다. |
적은 입력에 적합 : 소규모 입력이나 복잡한 입력(정규식, 데이터 타입 변환)이 필요한 경우엔 유용하지만, 대량의 입력을 처리할 때는 속도가 떨어집니다. |
고성능 : 대량의 텍스트 파일이나 대규모 네트워크 데이터를 읽을 때 Scanner보다 훨씬 더 빠릅니다. |
|
빈번한 I/O 작업 : 입력마다 내부적으로 I/O 작업을 반복하기 때문에 대용량 데이터를 처리할 때 병목 현상이 발생할 수 있습니다. |
낮은 연산 오버헤드 : 단순한 문자열 처리만 하므로, 추가적인 연산(정규식, 토큰화 등)이 없어 성능 면에서 유리합니다. |
|
메모리 사용 | 상대적으로 많이 사용합니다. (토큰화 및 구분자 처리) | 메모리 효율적입니다. (버퍼 사용) |
- 토큰화
: 문자열을 의미 있는 최소 단위인 토큰(Token)으로 나누는 과정을 말합니다.
ex) "안녕하세요, 저는 꿈도입니다."라는 문장을 토큰화한다면 아래와 같이 나눠진 토큰들을 프로그램에서 더 쉽게 처리하고 분석할 수 있는 단위가 됩니다.
→ 안녕하세요
→ ,
→ 저는
→ 꿈도입니다
예제 코드
// Scanner
Scanner sc = new Scanner(System.in);
int number = sc.nextInt();
System.out.println("정수 입력: " + number);
String text = sc.next();
System.out.println("문자열 입력: " + text);
sc.close();
// BufferedReader
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line = reader.readLine();
System.out.println("문자열 입력: " + line);
int number = Integer.parseInt(reader.readLine());
System.out.println("정수 입력: " + number);
reader.close();
StringTokenizer
ℹ️ 문자열을 빠르게 분할하여 파싱 성능을 높입니다.
- 공백이나 특정 구분자를 기준으로 빠르게 분할해 주는 도구입니다.
split() vs. StringTokenizer
💡복잡한 구분자로 정규 표현식을 사용해야 하거나 유연한 구분자 처리가 필요하다면 split()
단순한 구분자로 빠른 성능이 필요하다면 StringTokenizer를 사용하는 것이 좋습니다.
- 모두 문자열을 구분자로 나누는 기능을 하지만, 그 구현 방식과 성능에 차이가 있습니다.
비교 | split() | StringTokenizer |
동작 방식 | 정규 표현식 - 문자열을 정규 표현식을 사용해 분리합니다. - 다양한 구분자와 복잡합 패턴을 처리할 수 있습니다. - 문자열을 구분자로 나눈 후, 배열을 결과로 반환합니다. - 정규 표현식이 복잡해질수록 연산 비용이 증가해 성능이 느려질 수 있지만, 유연성이 매우 뛰어납니다. |
순차적으로 분리 - 문자열을 구분자로 순차적을 분리합니다. - 입력 문자열을 파싱하는 동안 매번 토큰을 찾고 반환하며, 반환된 토큰은 StringTokenizer 객체 내부에서 순차적으로 접근할 수 있습니다. - split()과 달린 정규 표현식을 사용하지 않고 단순한 문자 구분자만 처리할 수 있습니다. - 사용이 간편하지만 유연성이 부족합니다. |
성능 | 정규 표현식의 연산 비용 - 정규 표현식을 사용하므로 복잡한 구분자 처리가 가능하지만, 연산 비용이 있기 때문에 성능이 StringTokenizer보다 느릴 수 있습니다. - 구분자가 단순할 경우에는 차이가 크지 않으나, 정규 표현식을 사용하는 복잡한 구분자를 처리할 때는 더 많은 메모리와 시간을 소모합니다. |
더 적은 메모리와 연산 시간 - 내부적으로 정규 표현식을 사용하기 않기에 더 적은 메모리와 연산 시간이 소요됩니다. - 단순한 구분자(공백, 콤마 등)을 사용할 때 매우 빠릅니다. - 대량의 데이터를 처리하거나 단순한 구분자를 사용할 때 성능이 split()보다 빠릅니다. |
구분자 처리 | - 복수의 구분자(, ; | 등)를 자유롭게 처리할 수 있습니다. - 구분자 사이의 빈 문자열도 반환할 수 있습니다. |
- 단일 구분자만 사용합니다. (복수의 구분자 처리 시 여러 번 호출하거나 복잡한 처리가 필요함) - 구분자 사이의 빈 토큰은 무시됩니다. (빈 문자열이 반환되지 않음) |
예제 코드
// split()
String str = "Java,Python,C++,JavaScript";
String[] tokens = str.split(",");
for (String token: tokens) {
System.out.println(token);
}
// Java
// Python
// C++
// JavaScript
String str = "hello-world%inpa@tistory#com";
String[] splitter = str.split("[%-@#]");
for (int i=0; i < splitter.length; i++) {
System.out.printf("%d위치 : %s\n", i, splitter[i]);
}
// 0위치 : hello
// 1위치 : world
// 2위치 : inpa
// 3위치 : tistory
// 4위치 : com
// StringTokenizer
StringTokenizer tokenizer = new StringTokenizer("Java,Python,C++,JavaScript", ",");
while (tokenizer.hasMoreTokens()) {
System.out.println(tokenizer.nextToken());
}
// Java
// Python
// C++
// JavaScript
String str2 = "Welcome%to%The%Java%HelloWorld";
StringTokenizer st = new StringTokenizer(str2, "%");
System.out.println(st.countTokens());
while (st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
// Welcome
// to
// the
// Java
// HelloWorld
반응형