🔗 문제 링크
https://www.acmicpc.net/problem/1032
💡시니어 해설
[문자열 다루기]
🔍 접근 방법
문자열 패를 활용하는 문제
[핵심 포인트]
• 모든 파일 이름의 같은 위치에 있는 문자 비교
• 모두 같으면 해당 문자 유지, 다르면 '?'로 치환
• 첫 파일 이름을 기준으로 비교접근
[방법]
1. 첫 번째 파일 이름을 기준으로 설정
2. 각 문자 위치별로 N개의 파일을 비교
3. 동일하면 그대로, 다르면 '?'를 결과에 추가
[주의 사항]
• 파일 이름이 하나일 경우 예외 처리로 바로 출력
• 모든 파일 이름의 길이가 같다는 조건 활용
💻 풀이 코드
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
public class Solution {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
String[] files = new String[n];
for (int i = 0; i < n; i++) {
files[i] = br.readLine();
}
if (n == 1) {
System.out.println(files[0]);
return;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < files[0].length(); i++) {
char ch = files[0].charAt(i);
boolean same = true;
for (int j = 1; j < n; j++) {
if (files[j].charAt(i) != ch) {
same = false;
break;
}
}
sb.append(same ? ch : '?');
}
System.out.println(sb.toString());
}
}
📝 코드 설명
[입력]
• BufferReader 입력을 받아 파일 개수와 파일 이름들을 배열에 저장 (입력이 조금 더 빨라짐 Scanner 보다)
• 파일 이름이 1개일 경우는 예외 처리하여 바로 출력
[로직]
• 첫 번째 파일 이름을 기준으로 각 문자 위치를 검사
• 모든 파일에서 같은 위치의 문자가 모두 같으면 그 문자를 결과에 추가
• 하나라도 다르면 ‘?’를 결과에 추가
[출력]
• 완성된 패턴 문자열을 출력
📊 복잡도 분석
• 시간 복잡도: O(N * L) (N = 파일 수, L = 파일 길이)
• 공간 복잡도: O(L) (패턴 저장 공간)
⚡ 최적화 팁
1. [불필요한 반복 최소화] : 첫 파일과 나머지만 비교하므로 최소한의 비교
2. [빠른 문자열 입력] : Scanner 보다는 BufferReader를 활용
🎯 학습 포인트
• 패턴 비교 기반 문자열 문제 접근법
• 조건을 활용한 예외 처리
• 문자열 처리 시 리스트 활용법
🔗 관련 문제
• 문자열 비교 문제에서는 "기준 문자열을 잡고 비교"하는 방식이 가장 간단
• 문제 조건(문자열 길이 동일, 알파벳/. 제한)을 잘 활용하면 예외를 줄일 수 있음
• 문자열 조작은 성능에 민감하므로 리스트 활용을 습관화
🔍 톺아보기!
📌 Scanner보다 빠른 BufferedReader
버퍼를 사용하지 않는 입력 방식은 키보드에서 입력이 발생할 때마다 즉시 프로그램으로 전달됩니다.
버퍼를 사용하는 입력 방식은 키보드 입력을 임시 저장 공간인 버퍼에 모아둡니다. 버퍼가 가득 차거나 개행 문자를 만나면, 그때 버퍼의 내용을 한 번에 프로그램으로 전달합니다.
언뜻 생각하면 버퍼를 거치지 않고 키보드 입력을 즉시 처리하는 것이 더 빠를 것 같습니다. 하지만 실제로는 그렇지 않습니다.
외부 장치와의 입출력 작업은 CPU 연산에 비해 매우 느립니다. 키보드 같은 외부 장치와 통신할 때마다 시스템은 상당한 오버헤드를 감수해야 합니다. 따라서 입력이 있을 때마다 개별적으로 처리하는 것보다, 버퍼에 모아서 한 번에 처리하는 것이 훨씬 효율적입니다.
// Scanner
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
// BufferedReader
// 대량 데이터 처리 (> 10,000개)
// 시간제한이 빡빡한 문제 - 코딩 테스트
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String s = br.readLine(); // 한 줄 전체를 버퍼에서 읽음
int n = Integer.parseInt(br.readLine());
1️⃣ 입력 버퍼링 방식 차이
(1) 버퍼 크기와 할당
Scanner
- 버퍼 크기: 1024 문자 (약 1KB)
- 내부적으로 버퍼가 부족하면 크기를 동적 확장하도록 구현되어 있음
→ 아주 긴 토큰을 만나면 버퍼를 증가시킬 수 있음
BufferedReader
- 버퍼 크기: 8192 문자 (약 8KB)
- 생성자에서 버퍼 크기 변경 가능
(2) 버퍼 유형과 메모리
Scanner
- java.nio.CharBuffer를 사용하여 버퍼를 관리
- 내부적으로 char[]로 구현되어 있음
- 버퍼가 JVM 힙 메모리에 저장됨
BufferedReader
- 단순한 char[] 배열을 직접 버퍼로 사용. 입력 시 이 배열에 문자를 채우는 방식으로 동작
- 배열이 JVM 힙에 할당되며 재사용됨
(3) 입력 스트림 읽기 방식
Scanner
- 생성자에 InputStream을 넘기면 내부에서 new InputStreamReader(source)를 호출하여 문자 스트림으로 변환
- 토큰 단위로 읽도록 설계되어 필요한 만큼씩 반복적으로 읽기를 수행
- Readable 인터페이스의 read(CharBuffer)를 호출하여 버퍼에 데이터를 채움. 남은 버퍼 용량이 부족하면 compact()하거나 버퍼를 확장한 후 계속 읽음
- 기본적으로 1KB씩 읽고, 토큰이 완료될 때마다 처리를 멈춤
- 토큰 완료를 위해 필요하면 다시 읽는 방식으로 동작
→ 네이티브 호출(OS로부터 데이터 읽기)이 BufferedReader보다 빈번하게 발생
BufferedReader
- 일반적으로 InputStreamReader와 함께 사용되어 문자 스트림으로 변환된 입력을 버퍼링함
- System.in과 같은 바이트 스트림을 new BufferedReader(new InputStreamReader(System.in)) 형태로 감싸서 사용
- 내부적으로 8192자 버퍼를 채울 때까지 read() 호출을 수행
- 한 번 fill() 동작이 일어나면 최대 8192자까지 대량으로 읽어 버퍼를 채움
→ 네이티브 I/O 호출 횟수를 크게 줄여주는 효과
(4) 구분자 처리
Scanner
- 토큰 구분자를 정의하고 그 패턴에 따라 입력을 나눔
- 기본 구분자는 공백 패턴으로, 공백이나 개행을 모두 토큰 구분자로 취급
- 내부적으로 Pattern 객체와 Matcher를 사용하여 입력 버퍼에서 패턴 매치를 수행
- next() 호출 시 정규식 매칭으로 구분자 부분을 스킵한 후 다음 구분자까지의 문자를 토큰으로 추출
- 정규식 기반 처리로 유연성을 제공하지만, Matcher.find() 등의 연산으로 추가 오버헤드가 발생
- nextLine()
- 구분자 설정과 무관하게 한 줄 전체를 읽어옴
- findWithinHorizon 메서드를 이용해 (?s).* 패턴으로 개행 전까지를 처리
- 기본 whitespace 구분자의 영향을 받지 않음
- nextInt() 등 사용 후 nextLine() 호출 시 남은 개행 문자로 인해 빈 문자열을 반환하는 문제가 발생
BufferedReader
- 개행 문자를 경계로 한 줄씩 읽는 readLine() 메서드 제공
- 내부 버퍼를 순차적으로 탐색하면서 개행 문자를 찾음
- 저장된 문자 배열을 인덱스로 증가시키며 '\n' 또는 '\r' 문자 확인
- 개행 문자를 발견하면 그 위치까지의 문자를 모아 문자열로 반환. 개행 문자는 소비하지만 결과 문자열에는 포함하지 않음
- 시스템 콜이나 정규식 처리 없이 단순한 char 비교로 개행을 인식하여 오버헤드가 낮음
2️⃣ 스레드 안정성과 동시성
Scanner
- 내부에 동기화가 전혀 적용되지 않도록 설계
- 메서드 구현에서 synchronized를 사용하거나 전역 락으로 보호하는 부분이 없음
→ 하나의 Scanner 인스턴스를 두 개 이상의 스레드가 동시에 사용하면 안전하지 않음 - 멀티스레드 환경에서 내부 버퍼 position이나 matcher 상태가 서로 간섭하여 정상적인 토큰 분리가 안 되거나 예외 발생 가능
- 공식 JavaDocs에서도 Thread-safe 하지 않으므로 멀티스레드 상황에서 공유하지 말 것을 명시
- 부득이 여러 스레드에서 사용해야 한다면 호출 측에서 직접 동기화해야 함
- 외부 동기화를 적용하더라도, BufferedReader 사용이나 입력 처리 방식 재설계를 더 권장
BufferedReader
- 내부에서 대부분의 메서드를 동기화하여 구현
- Reader 클래스의 보호용 객체 lock을 상속받아 입출력 연산 시 동기화하도록 설계
- BufferedReader 생성 시 하위 Reader(InputStreamReader 등)와 같은 락 객체를 통해 동기화
- readLine() 메서드는 내부에서 synchronized(lock) 블록으로 구현되어 스레드 안전성 보장
- 여러 스레드가 동시에 한 BufferedReader를 호출하더라도 내부 버퍼의 일관성이 깨지지 않음
- 기본적으로 thread-safe하다고 문서상에서도 언급
- 스레드 안전하지만 여러 스레드에서 효율적으로 사용할 수 있다는 의미는 아님
- 두 스레드가 동시에 readLine()을 호출하면, 어느 스레드가 어느 줄을 읽을지 예측하기 어렵고 줄이 섞일 위험 존재
- OpenJDK 버그 리포트에서 다중 스레드 동시 호출 시 빈 문자열을 반환하는 레이스 컨디션 이슈 언급됨
- 동시에 같은 Reader를 읽는 패턴은 권장되지 않지만, 락 보호로 인해 치명적인 오류는 방지됨
📌 System.out.println vs StringBuilder
// 느림: 매번 System.out.println()
for (int i = 0; i < N; i++) {
System.out.println(result[i]); // 시간초과 위험
}
// 빠름: StringBuilder 사용
StringBuilder sb = new StringBuilder();
for (int i = 0; i < N; i++) {
sb.append(result[i]).append('\n');
}
System.out.print(sb);
📚 참고 자료
Scanner vs BufferedReader: JVM 레벨 입력 처리 차이 분석
문자열 계산기 구현 과정에서 Scanner를 사용했지만, 리뷰어의 피드백을 계기로 BufferedReader와의 차이점을 JVM 내부 동작 수준까지 깊이 있게 분석해보았습니다. 단순한 사용 편의성보다는 버퍼링
velog.io
https://rlakuku-program.tistory.com/33
[Java] 빠른 입출력을 위한 BufferedReader, BufferedWriter, StringTokenizer, StringBuilder
BufferedReader / BufferedWriter BufferedReader와 BufferdWriter는 버퍼를 사용하여 읽기와 쓰기를 하는 함수이다. 버퍼를 사용하지 않는 입력은, 키보드의 입력이 키를 누르는 즉시 바로 프로그램에 전달된다.
rlakuku-program.tistory.com
