[Java] 입출력

bien·2026년 1월 1일

코딩테스트

목록 보기
16/18

개념 정리

1) 입출력 가속화(I/O Acceleration)

(1) 입력: Scanner vs BufferReader

Scanner (편리하지만 느림)

  • 3줄 요약 (Summary)
    • 수도꼭지에서 물방울이 떨어질때마다 컵에 바로바로 담는 방식
    • 장점: nextInt(), next() 등 메서드가 직관적이라 사용하기 매우 편리함.
    • 단점: 입력마다 문자를 해석(Parsing)하고 정규식을 검사하는 과정 때문에 매우 느림. (대용량 데이터 처리에 부적합)

상세 분석: 🔍 왜 Scanner는 느릴까?
Scanner는 단순한 입력 도구가 아닌 편의성 중심의 파싱 도구로 설계되었다.

  • 즉시 파싱 (On-the-fly Parsing): 단순히 문자를 읽는 것이 아니라, 데이터를 구획 문자(Delimiter, 공백 등)를 기준으로 토큰화(Tokenizing)하고, 호출할 때마다 내부적으로 정규식 매칭과 타입 변환이 발생하여 오버헤드(Overhead)가 크다.
  • 작은 버퍼 사이즈 (Small Buffer): 내부 버퍼 사이즈가 1024 char(약 1KB)로 작아, 입력이 많을 경우 OS로부터 데이터를 읽어오는 횟수가 상대적으로 많다.

BufferedReader (불편하지만 매우 빠름)

  • 3줄 요약 (Summary)
    • 수도꼭지에서 물을 큰 양동이(Buffer)에 가득 받아두고, 필요할 때마다 퍼서 쓰는 방식
    • 장점: 데이터를 덩어리(Chunk) 채로 가져오므로 입출력 횟수가 줄어 Scanner보다 약 10배 이상 빠름
    • 단점: 무조건 String으로만 읽어오므로, 데이터를 가공(형변환 등)하려면 추가적인 코드가 필요함

상세 분석: 🔍 왜 BufferedReader는 빠를까?
BufferedReader성능 중심의 버퍼링 도구이다. 하드웨어적인 입출력 효율을 극대화한다.

  • 대용량 버퍼링(Buffering): 입력 스트림과 프로그램 사이에 내부 버퍼(Internal Buffer, 기본 8192 char = 8KB)를 둔다. 운영체제로부터 데이터를 청크(Chunk) 단위로 미리 적재(Pre-fetch)한 뒤, 프로그램은 메모리(RAM)에서 고속으로 읽어 온다.
  • 시스템 콜 최소화 (Minimize System Calls): 디스크나 외부 장치에 접근하는 I/O 요청(System Call) 횟수를 획기적으로 줄여, 비싼 비용인 컨텍스트 스위칭(Context Switching)을 최소화한다.
  • 단순한 로직(Rawy String): 데이터를 파싱하거나 정규식을 검사하지 않고, 단순히 문자열 덩어리(혹은 라인) 그대로 가져오기 때문에 CPU 연산 비용이 매우 낮다.

(2) 문자열 자르기: split vs StringTokenizer

StringTokenizer (단순하고 빠른 커서 방식)

  • 개념(Concept): 마치 책갈피(Cursor)를 끼워두고, 긴 문자열을 처음부터 끝까지 한 번만 훑어가며(Scan) 구분자를 만날 때마다 그 위치의 문자열을 잘라내여 반환하는 방식.
  • 기술적 특징:
    • 정규식 미사용 (No Regex): 복잡한 정규시 표현식을 사용하지 않고, 단순히 문자(Char) 단위의 비교만 수행하므로 CPU 연산 비용이 매우 낮다.
    • 이터레이터 패턴 (Iterator Pattern) : 결과값을 미리 배열에 담아두지 않고, nextToken()을 호출할 때마다 현재 위치(Cursor)에서 다음 구분자까지만 탐색하여 반환한다. 이로 인해 초기 메모리 할당 비용이 거의 없다.
    • 레거시 클래스(Legacy): 자바 초기 버전부터 존재했으나, 성능상의 이점 때문에 알고리즘 풀이에서는 여전히 현역으로 쓰인다. (단, 결과값이 문자열이라 형변환이 필요하다)

String.split() (유연하지만 무거운 정규식 방식)

  • 개념(Concept): 입력받은 구분자를 정규 표현식(Regular Expression)으로 컴파일 한 뒤, 전체 문자열을 패턴 매칭하여 일치하는 모든 부분을 잘라내고, 이를 새로운 문자열 배열(String Array)에 담아서 반환하는 방식
    • 기술적 특징:
      • 정규식 컴파일 오버헤드 (Regex Compilation): 내부적으로 java.util.regex.Pattern 객체를 생성하고 컴파일하는 과정이 포함되어 있어 초기 구동 비용이 발생한다.
      • 전체 배열 할당(Full Array Allocation): 문자열 전체를 파싱하여 결과값을 String[]배열 객체로 한 번에 생성한다. 잘린 문자열이 많을 수록 힙 메모리 (Heap Memory) 사용량이 순간적으로 급증할 수 있다.
      • 유연성 (Flexibility): 단순 문자가 아니라 복잡한 패턴 (예: "공백이거나 콤마이거나 점일 때")으로 자를 수 있어 비즈니스 로직 구현에 강하다.

예시 코드

입력 속도 비교: Scanner vs BufferedReader

10만 개의 정수가 한 줄에 하나씩 입력되는 상황

Scanner (느림 - 편의성 위주)

별도의 예외 처리가 필요 없고, nextInt()로 바로 정수를 가져올 수 있어 코드가 간결하다.

import java.util.*;

public class ScannerExample {
	public static void main(String[] args) {
    	// 내부 버퍼가 작고 (1KB), 입력마다 정규식 검사를 수행함
        Scanner sc = new Scanner(System.in);
        
        int n = sc.nextInt(); // 정수 파싱을 자동으로 수행
        
        for (int i = 0; i < n; i++) {
        	int value = sc.nextInt(); // 편리하지만 느림
            // 로직 처리 ...
        }
    }
}

BufferedReader (빠름 - 성능 위주)

데이터를 한 번에 문자열(String)로 가져온다. 반드시 형변환(Integer.parseInt)이 필요하며, 예외 처리(throws IOException)를 명시해야 한다.

import java.io.*;

public class BufferedExample {
	public static void main(String[] args) throws IOException { // 1. 예외 처리 필수
    	// 8KB 버퍼를 사용하여 덩어리째 읽어옴
        BufferedReader br = new BufferedReader(new InputStraemReader(System.in));
        
        // 2. 라인 단위로 읽어서 직접 형변환 수행 (Raw String -> int)
        int n = Integer.parseInt(br.readLine());
        
        for (int i = 0; i < n; i++) {
        	int value = Integer.parseInt(br.readLine()); // 빠름
            // 로직 처리...
        }
    }
}

문자열 분리 비교: split vs StringTokenizer

공백으로 구분된 긴 문장려 10 20 30 ...를 잘라서 처리하는 상황

String.split() (느림 - 정규식 사용)

코드는 한 줄로 끝나지만, 내부적으로 정규식 컴파일과 뱅러 전체 할당이 일어나 메모리와 시간을 많이 쓴다.

String line = "10 20 30 40 50";

// 정규식 (" ")을 사용하며, 결과를 String[] 배열 객체로 한 번에 생성함
String[] value = line.split(" ");

for(String s : value) {
	int value = Integer.parseInt(s);
    // 로직 처리...
}

StringTokenizer (단순하고 빠른 커서 방식)

정규식을 사용하지 않고 단순히 문자를 훑으며 자른다.
배열을 미리 만들지 않아 메모리 효율이 매우 좋다.

import java.util.StringTokenizer;

String line = "10 20 30 40 50";

// 1. 객체 생성 시 문자열 전달 (기본 구분자는 공백)
StringTokenier st = new StringTokenizer(line);

// 2. 이터레이터 패턴: 다음 토큰이 있는지 확인하며 하나씩 꺼냄
while (st.hasMoreToken()) {
	// 꺼낼 때만 해당 위치의 문자열을 생성하므로 메모리 할당 비용이 낮음
    int value = Integer.parseInt(st.nextToken());
    // 로직 처리...
}

최종 예시 코드

public class FastIO {
	public static void main(String[] args) throws IOException {
    	// 1. 입력 가속: BufferedReader 사용
        BufferedREader br = new BufferedREader(new InputStreamReader(System.in));
        
        // 2. 출력 가속: 매번 출력하지 않고 StringBuilder에 모았다가 한 번에 출력
        StringBuilder sb = new StringBuilder();
        
        // 첫 줄에 데이터 개수 N이 들어온다고 가정
        int n = Integer.parseInt(br.readLine());
        
        // 3. 문자열 분리: StringTokenizer 사용 (split 보다 훨씬 빠름)
        // br.readLine()으로 한 줄을 통째로 읽어와 토크나이저에 전달
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        for (int i = 0; i < n; i++) {
        	// st.nextToken()으로 문자열을 하나씩 꺼내고 형변환
            int value = Integer.parseInt(st.nextToken());
            
            // 결과값을 StringBuilder에 추가 (\n은 줄바꿈)
            sb.append(value * 2).append("\n");
        }
        
        // 4. 최종 결과 한 번에 출력 (시스템 콜 최소화)
        System.out.println(sb.toString());
    }
}
        

📌 최종 요약

기술 도구추천 사용처핵심 장점 (Key Point)
Scanner데이터 양이 적고 빠른 구현이 필요할 때nextInt() 등 타입별 자동 파싱 지원으로 사용이 매우 편리함
BufferedReader알고리즘 문제, 대용량 파일 읽기8KB 버퍼 활용 및 시스템 콜 최소화로 압도적인 입력 속도
String.split()복잡한 구분자(정규식) 처리가 필요할 때코드가 간결하며 정규 표현식을 활용한 유연한 분리 가능
StringTokenizer공백 기준의 대량 문자열 분리정규식 미사용 및 커서 기반 탐색으로 메모리 및 CPU 효율 극대화
System.out.println단발성 출력, 디버깅 용도사용법이 가장 직관적이나 호출 시마다 I/O가 발생하여 속도가 느림
StringBuilder반복문 내 연속적인 출력 발생 시내부 버퍼에 문자열을 모아 단 한 번의 출력으로 처리 (속도 가속)
profile
Good Luck!

0개의 댓글