본문 바로가기

Java/문법

[Java] 빠른 입출력과 파싱 - BufferedReader(vs. Scanner), StringTokenizer(vs. split()), 주요 메서드 정리, 버퍼, 토큰, 정규 표현식, 빈 문자열 반환

주요 메서드 정리

구분 기능
Scanner
  • next(): 공백(스페이스) 전까지의 다음 토큰을 반환합니다.
  • nextLine(): 한 줄 전체를 읽고 줄바꿈 문자 전까지의 문자열을 반환합니다.
  • nextInt(): 정수형(int) 입력을 반환합니다.
  • nextDouble(): 실수형(double) 입력을 반환합니다.
  • nextBoolean(): Boolean 값을 반환합니다.
  • hasNext(): 다음 입력이 존재하는지 여부를 반환합니다.
  • hasNextInt(): 다음 입력이 정수인지 여부를 확인합니다.
BufferedReader
  • read(): 한 문자를 읽고 정수 값으로 반환합니다. 더 이상 읽을 문자가 없으면 -1을 반환합니다.
  • readLine(): 한 줄 전체를 읽고 문자열을 반환합니다. 더 이상 읽을 줄이 없으면 null을 반환합니다.
  • ready(): 입력 스트림에 읽을 준비가 된 데이터가 있는지 확인합니다.
  • skip(long n): 지정된 수의 문자를 건너뜁니다.
  • close(): 스트림을 닫고 자원을 해제합니다.
StringTokenizer
  • hasMoreTokens(): 남은 토큰이 있는지 여부를 반환합니다.
  • nextToken(): 다음 토큰을 반환합니다.
  • countTokens(): 남은 토큰의 개수를 반환합니다.
  • nextToken(String delim): 새로운 구분자를 설정하고 다음 토큰을 반환합니다.
split()
  • split(String regex): 정규 표현식(regex)에 따라 문자열을 분할하고 문자열 배열을 반환합니다.
  • split(String regex, int limit): 정규 표현식에 따라 문자열을 분할하되, 최대 분할 횟수(limit)를 설정할 수 있습니다. 분할 횟수에 도달하면 나머지 문자열을 반환합니다.

 


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

 


반응형