자바로 코딩 테스트를 시작한 지 얼마 되지 않았을 때 문제가 생겼다. 코드의 결과는 맞췄는데, 아무리 고민해도 시간 초과를 해결할 수 없었다. 찾아보니 System.out.print 계열 메서드가 오버헤드가 있어서 BufferedWriter + StringBuilder 조합을 사용해야 했다. 당시에는 문제 풀기에 급급해서 그냥 사용했는데, 이제는 왜 그런지 정확히 이해해보자.
System.out.println의 동작 구조System.out.println("hello world");
위 코드는 단순한 출력 코드이지만, 내부 동작을 깊이 파고들어 보자.
// PrintStream.java
public void println(String x) {
if (getClass() == PrintStream.class) {
writeln(String.valueOf(x));
} else {
synchronized (this) {
print(x);
newLine();
}
}
}
System.out.println()은 PrintStream의 println() 메서드를 통해 출력된다. 상속 여부를 체크한 후, 내부적으로 writeln()을 호출한다.
// PrintStream.java
private void writeln(String s) {
try {
synchronized (this) {
ensureOpen();
textOut.write(s);
textOut.newLine();
textOut.flushBuffer(); // 즉시 플러시 발생
charOut.flushBuffer();
if (autoFlush)
out.flush();
}
} catch (IOException x) {
trouble = true;
}
}
System.out.print는 즉시 출력이 이루어지도록 flushBuffer()를 호출하여 I/O 호출이 자주 발생하게 된다.
BufferedWriter의 동작 방식// BufferedWriter.java
public void write(char cbuf[], int off, int len) throws IOException {
synchronized (lock) {
ensureOpen();
if (len >= nChars) {
flushBuffer();
out.write(cbuf, off, len);
return;
}
int b = off, t = off + len;
while (b < t) {
int d = Math.min(nChars - nextChar, t - b);
System.arraycopy(cbuf, b, cb, nextChar, d);
b += d;
nextChar += d;
if (nextChar >= nChars)
flushBuffer();
}
}
}
BufferedWriter는 버퍼(기본 8192바이트)를 사용하여 일정 크기 이상이 되면 한 번에 출력하기 때문에 I/O 호출 횟수가 줄어들어 성능이 향상된다.
import java.io.*;
public class PrintVsBufferedWriter {
public static void main(String[] args) throws IOException {
int N = 8192 * 10; // 출력 횟수
// System.out.print 사용
long start1 = System.currentTimeMillis();
for (int i = 0; i < N; i++) {
System.out.print(i + " "); // 즉시 출력됨
}
long end1 = System.currentTimeMillis();
long printTime = end1 - start1; // 실행 시간 저장
// BufferedWriter 사용
long start2 = System.currentTimeMillis();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
for (int i = 0; i < N; i++) {
bw.write(i + " "); // 버퍼에 저장됨
}
bw.flush(); // 최종 출력
long end2 = System.currentTimeMillis();
long bufferedTime = end2 - start2; // 실행 시간 저장
// 최종 결과 비교 출력
System.out.println("\n===== 성능 비교 결과 =====");
System.out.println("System.out.print 실행 시간: " + printTime + "ms");
System.out.println("BufferedWriter 실행 시간: " + bufferedTime + "ms");
System.out.println("=========================");
bw.close();
}
}
===== 성능 비교 결과 =====
System.out.print 실행 시간: 325ms
BufferedWriter 실행 시간: 22ms
=========================
| 비교 항목 | System.out.print() | BufferedWriter.write() |
|---|---|---|
| 출력 방식 | 즉시 OS에 출력 | 버퍼에 저장 후 한 번에 출력 |
| 버퍼 사용 여부 | 없음 | 있음 (기본 8KB) |
| I/O 호출 횟수 | 많음 (출력할 때마다 OS 접근) | 적음 (버퍼가 찼을 때만 OS 접근) |
| 성능 | 느림 | 빠름 |
결론
System.out.print는 즉시 플러시(flush)되므로, 한 글자라도 출력할 때마다 OS에 요청을 보냄 → 성능 저하.BufferedWriter는 버퍼를 사용하여 일정 크기 이상이 될 때만 출력 → I/O 요청 횟수가 줄어들어 성능이 크게 향상됨.BufferedWriter를 사용해야 함.