Java

Java BufferedReader와 StringTokenizer

Daesiker 2025. 11. 18. 17:26
반응형

알고리즘 문제를 풀거나 대용량 데이터를 처리할 때, 입력을 어떻게 받느냐가 성능에 큰 영향을 미칩니다. 오늘은 Java에서 빠른 입력 처리를 위한 필수 도구인 BufferedReader와 StringTokenizer에 대해 알아보겠습니다.

왜 BufferedReader를 사용할까?

Java에서 입력을 받는 방법은 여러 가지가 있습니다. 가장 간단한 Scanner부터 BufferedReader까지, 각각 장단점이 있습니다. 하지만 속도가 중요한 상황에서는 BufferedReader가 압도적으로 유리합니다.

Scanner vs BufferedReader 성능 비교

Scanner는 사용하기 편리하지만 느립니다. 내부적으로 정규표현식을 사용하여 데이터를 파싱하고, 버퍼 크기도 작습니다(1KB). 반면 BufferedReader는 단순히 문자열을 읽기만 하며, 큰 버퍼(8KB)를 사용합니다.

실제 속도 차이:

  • 100만 개의 정수를 읽을 때
  • Scanner: 약 5초
  • BufferedReader: 약 0.5초

거의 10배의 성능 차이가 납니다! 코딩 테스트에서 시간 초과가 발생한다면, Scanner를 BufferedReader로 바꾸는 것만으로도 통과할 수 있습니다.

 


BufferedReader

기본 사용법

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

public class BufferedReaderBasic {
    public static void main(String[] args) throws IOException {
        // BufferedReader 생성
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        
        // 한 줄 읽기
        String line = br.readLine();
        
        System.out.println("입력받은 문자열: " + line);
        
        // 사용 후 닫기
        br.close();
    }
}

핵심 개념:

  • InputStreamReader(System.in): 바이트 스트림을 문자 스트림으로 변환
  • BufferedReader: 문자 스트림에 버퍼링 기능 추가
  • readLine(): 한 줄을 읽어 String으로 반환 (줄바꿈 문자는 제거됨)

BufferedReader 핵심 메서드

1. readLine() - 한 줄 읽기

가장 기본이 되는 메서드입니다. 한 줄을 통째로 읽어서 String으로 반환합니다.

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = br.readLine();

특징:

  • 줄바꿈 문자(\n)는 반환 문자열에 포함되지 않습니다
  • 더 이상 읽을 데이터가 없으면 null을 반환합니다
  • 항상 String 타입으로 반환됩니다 (숫자도 String으로 받음)

실전 예시 - 여러 줄 읽기:

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

// 방법 1: 줄 수를 알 때
int n = Integer.parseInt(br.readLine());
for (int i = 0; i < n; i++) {
    String line = br.readLine();
    System.out.println(line);
}

// 방법 2: 끝까지 읽기
String line;
while ((line = br.readLine()) != null) {
    System.out.println(line);
}

2. read() - 한 문자 읽기

한 문자를 읽어서 정수(아스키 코드)로 반환합니다. 실무에서는 거의 사용하지 않지만, 문자 단위 처리가 필요할 때 유용합니다.

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int charCode = br.read();  // 'A' 입력 시 65 반환
char ch = (char) charCode;  // 65를 'A'로 변환

3. close() - 리소스 해제

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// ... 사용 ...
br.close();  // 반드시 호출!

BufferedReader는 시스템 리소스(파일 디스크립터 등)를 사용합니다. close()를 호출하지 않으면 리소스 누수가 발생할 수 있습니다.


데이터 타입 변환

BufferedReader는 항상 String을 반환하므로, 다른 타입이 필요하면 직접 변환해야 합니다.

정수 읽기

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

// int 타입
String input = br.readLine();
int num = Integer.parseInt(input);

// 한 줄로
int num2 = Integer.parseInt(br.readLine());

// long 타입 (큰 숫자)
long bigNum = Long.parseLong(br.readLine());

주의사항: 숫자가 아닌 문자열을 parseInt()에 넣으면 NumberFormatException이 발생합니다.

실수 읽기

// double 타입
double d = Double.parseDouble(br.readLine());

// float 타입
float f = Float.parseFloat(br.readLine());

여러 개의 정수 읽기 (공백 구분)

한 줄에 여러 개의 숫자가 공백으로 구분되어 있을 때는 split()을 사용합니다.

// 입력: "10 20 30"
String[] input = br.readLine().split(" ");
int a = Integer.parseInt(input[0]);  // 10
int b = Integer.parseInt(input[1]);  // 20
int c = Integer.parseInt(input[2]);  // 30

하지만 split()은 느립니다. 더 빠른 방법은 StringTokenizer를 사용하는 것입니다!


StringTokenizer 완벽 가이드

StringTokenizer란?

StringTokenizer는 문자열을 특정 구분자로 나누는 클래스입니다. split()보다 빠르고, 반복 작업에 최적화되어 있습니다.

왜 빠른가?:

  • split()은 정규표현식을 사용 (느림)
  • StringTokenizer는 단순 문자 비교 (빠름)
  • 대량의 토큰을 처리할 때 성능 차이가 크게 남

기본 사용법

import java.util.StringTokenizer;

String input = "10 20 30 40 50";
StringTokenizer st = new StringTokenizer(input);

while (st.hasMoreTokens()) {
    String token = st.nextToken();
    System.out.println(token);
}
// 출력:
// 10
// 20
// 30
// 40
// 50

기본 구분자: 공백(스페이스), 탭, 줄바꿈

BufferedReader와 함께 사용

가장 일반적인 패턴입니다. 코딩 테스트에서 거의 필수로 사용됩니다.

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

// 첫 번째 줄: 데이터 개수
int n = Integer.parseInt(br.readLine());

// 두 번째 줄: n개의 정수 (공백 구분)
StringTokenizer st = new StringTokenizer(br.readLine());

int[] arr = new int[n];
for (int i = 0; i < n; i++) {
    arr[i] = Integer.parseInt(st.nextToken());
}
```

**실전 예시**:
```
입력:
5
10 20 30 40 50

결과:
arr = [10, 20, 30, 40, 50]

StringTokenizer 핵심 메서드

1. nextToken() - 다음 토큰 가져오기

구분자로 나뉜 문자열 조각(토큰)을 하나씩 반환합니다.

StringTokenizer st = new StringTokenizer("apple banana cherry");
String first = st.nextToken();   // "apple"
String second = st.nextToken();  // "banana"
String third = st.nextToken();   // "cherry"

주의: 토큰이 없는데 nextToken()을 호출하면 NoSuchElementException이 발생합니다.

2. hasMoreTokens() - 토큰이 더 있는지 확인

남은 토큰이 있으면 true, 없으면 false를 반환합니다. 주로 while문과 함께 사용합니다.

StringTokenizer st = new StringTokenizer("1 2 3 4 5");

while (st.hasMoreTokens()) {
    int num = Integer.parseInt(st.nextToken());
    System.out.println(num);
}

안전한 패턴: hasMoreTokens()로 확인한 후 nextToken()을 호출하면 예외가 발생하지 않습니다.

3. countTokens() - 남은 토큰 개수

현재 남아있는 토큰의 개수를 반환합니다.

StringTokenizer st = new StringTokenizer("A B C D E");
System.out.println(st.countTokens());  // 5

st.nextToken();  // "A" 가져감
System.out.println(st.countTokens());  // 4

st.nextToken();  // "B" 가져감
System.out.println(st.countTokens());  // 3

활용: 배열 크기를 미리 알고 싶을 때 유용합니다.

String input = "10 20 30 40 50";
StringTokenizer st = new StringTokenizer(input);
int size = st.countTokens();  // 5

int[] arr = new int[size];
for (int i = 0; i < size; i++) {
    arr[i] = Integer.parseInt(st.nextToken());
}

4. hasMoreElements() & nextElement()

hasMoreTokens()와 nextToken()과 동일하게 동작합니다. Enumeration 인터페이스 구현을 위한 메서드이지만, 일반적으로는 hasMoreTokens()와 nextToken()을 사용합니다.


커스텀 구분자 사용

다른 구분자 지정

기본 구분자(공백, 탭, 줄바꿈) 대신 다른 문자를 구분자로 사용할 수 있습니다.

// 쉼표로 구분
String csv = "apple,banana,cherry,date";
StringTokenizer st = new StringTokenizer(csv, ",");

while (st.hasMoreTokens()) {
    System.out.println(st.nextToken());
}
// 출력:
// apple
// banana
// cherry
// date

실무 활용: CSV 파일 파싱, 특정 형식의 데이터 처리

여러 개의 구분자

여러 문자를 구분자로 지정할 수 있습니다.

// 쉼표, 세미콜론, 콜론을 모두 구분자로
String data = "apple,banana;cherry:date";
StringTokenizer st = new StringTokenizer(data, ",;:");

while (st.hasMoreTokens()) {
    System.out.println(st.nextToken());
}
// 출력:
// apple
// banana
// cherry
// date

구분자도 토큰으로 포함하기

세 번째 인자로 true를 전달하면 구분자도 토큰으로 반환됩니다.

String data = "10+20-30";
StringTokenizer st = new StringTokenizer(data, "+-", true);

while (st.hasMoreTokens()) {
    System.out.println(st.nextToken());
}
// 출력:
// 10
// +
// 20
// -
// 30

활용: 수식 파싱, 특수 문자 처리가 필요한 경우


실전 활용 패턴

패턴 1: 여러 줄, 각 줄마다 여러 숫자

// 입력 예시:
// 3
// 10 20
// 30 40 50
// 60

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());

for (int i = 0; i < n; i++) {
    StringTokenizer st = new StringTokenizer(br.readLine());
    
    while (st.hasMoreTokens()) {
        int num = Integer.parseInt(st.nextToken());
        System.out.print(num + " ");
    }
    System.out.println();
}

핵심: 각 줄마다 새로운 StringTokenizer를 만듭니다.

패턴 2: 정해진 개수의 입력

// 입력: "5 10 15 20 25"
// 항상 5개의 숫자가 입력됨

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());
int c = Integer.parseInt(st.nextToken());
int d = Integer.parseInt(st.nextToken());
int e = Integer.parseInt(st.nextToken());

주의: 개수가 정확히 맞아야 합니다. 부족하면 예외 발생!

패턴 3: 2차원 배열 입력

// 입력:
// 3 4
// 1 2 3 4
// 5 6 7 8
// 9 10 11 12

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

int rows = Integer.parseInt(st.nextToken());
int cols = Integer.parseInt(st.nextToken());

int[][] matrix = new int[rows][cols];

for (int i = 0; i < rows; i++) {
    st = new StringTokenizer(br.readLine());
    for (int j = 0; j < cols; j++) {
        matrix[i][j] = Integer.parseInt(st.nextToken());
    }
}

코딩 테스트 필수 패턴: 그래프, 행렬 문제에서 자주 사용됩니다.

패턴 4: 빠른 출력 (StringBuilder)

입력뿐만 아니라 출력도 많을 때는 StringBuilder를 함께 사용합니다.

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

int n = Integer.parseInt(br.readLine());

for (int i = 0; i < n; i++) {
    StringTokenizer st = new StringTokenizer(br.readLine());
    int a = Integer.parseInt(st.nextToken());
    int b = Integer.parseInt(st.nextToken());
    
    sb.append(a + b).append('\n');
}

System.out.print(sb);  // 한 번에 출력

성능 향상: System.out.println()을 여러 번 호출하는 것보다 훨씬 빠릅니다.


split() vs StringTokenizer 비교

성능 비교

// 100만 개의 토큰을 처리할 때

// split() 사용
String[] tokens = input.split(" ");  // 약 300ms

// StringTokenizer 사용
StringTokenizer st = new StringTokenizer(input);
while (st.hasMoreTokens()) {
    String token = st.nextToken();  // 약 100ms
}

결론: StringTokenizer가 약 3배 빠릅니다!

기능 비교

기능 split() StringTokenizer
속도 느림 빠름
정규표현식 지원 미지원
빈 문자열 포함 가능 포함 안 됨
반환 타입 배열 토큰 순차 접근
메모리 모든 토큰 저장 필요할 때만

 

반응형