본문 바로가기
IT

System.out.println()의 성능 이슈와 Logback 비교

by urosie 2026. 1. 15.
반응형

실제 사례

한 번 요청 시 5000명의 사용자를 요청하고, 처리 과정에서 응답시간이 20초 걸리는 사이트가 있는데, 원인을 알아보니 5000명의 정보를 다 System.out.println()으로 처리하고 있던 것이다. 이는 System.out.println()을 줄임으로써 응답시간이 6초까지 줄었다.

- 이상민, 자바 성능 튜닝이야기, 인사이트, 2013

20초에서 6초로, 14초가 단축되었습니다. 정말 이게 가능한 일일까요?

System.out.println()이 느린 이유

1. 동기화(Synchronized) 오버헤드

System.out.println()은 내부적으로 PrintStream의 synchronized 메서드를 사용합니다.

 
 
java
public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

멀티스레드 환경에서 thread-safe를 보장하기 위해 매번 락을 획득하고 해제하는 과정이 필요합니다.

2. I/O 작업의 본질적인 느림

콘솔 출력은 결국 I/O 작업입니다. CPU 연산에 비해 매우 느리며, 특히 대량의 데이터를 출력할 때는 성능 저하가 두드러집니다.

3. 자동 버퍼 플러시

각 println() 호출마다 자동으로 버퍼를 flush하여 즉시 출력하려고 하기 때문에 추가적인 오버헤드가 발생합니다.

성능 계산

  • 20초 → 6초 = 14초 절약
  • 5000명으로 나누면 사용자당 약 2.8ms 절약
  • 이는 충분히 현실적인 수치입니다

Logback은 어떨까?

Logback도 동기화를 사용합니다

Logback 역시 기본적으로 동기화를 사용합니다. 그럼에도 불구하고 System.out.println()보다 빠른 이유는 무엇일까요?

1. 비동기 로깅 옵션

 
 
xml
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="FILE" />
    <queueSize>512</queueSize>
</appender>

AsyncAppender를 사용하면 로그를 큐에 넣고 별도 스레드가 처리하므로, 메인 로직은 블로킹되지 않습니다.

2. 로그 레벨 필터링 (가장 중요!)

 
 
java
// 프로덕션에서 INFO 레벨로 설정했다면
logger.debug("User: {}", user);  // 아예 실행 안 됨
System.out.println("User: " + user);  // 무조건 실행됨

디버그용 로그를 코드에 남겨둬도 프로덕션에서는 오버헤드가 거의 없습니다.

3. Lazy 문자열 평가

 
 
java
// GOOD - 로그 레벨이 OFF면 문자열 연산 자체가 일어나지 않음
logger.debug("User: {}, Data: {}", user, expensiveOperation());

// BAD - 무조건 문자열 연산 발생
System.out.println("User: " + user + ", Data: " + expensiveOperation());

내부 동작 차이:

 
 
java
// 문자열 연결 방식 (BAD)
String temp = "User: " + user + ", Data: " + expensiveOperation(); // 먼저 실행
logger.debug(temp); // 로그 레벨 체크 후 버림

// 파라미터 치환 방식 (GOOD)
if (logger.isDebugEnabled()) { // 먼저 체크
    // 여기서만 파라미터 평가 및 문자열 생성
    logger.debug("User: {}, Data: {}", user, expensiveOperation());
}

4. 효율적인 버퍼링

  • System.out은 매번 자동 flush
  • Logback은 버퍼를 모아서 한 번에 쓰기 (설정 가능)

비동기 모드를 사용하지 않는다면?

비동기 모드를 사용하지 않아도 로그 레벨 필터링만으로도 큰 성능 이점이 있습니다.

 
 
java
// 프로덕션에서 INFO 레벨로 설정된 상황

// 나쁜 예 - 매번 연산 발생
for (int i = 0; i < 5000; i++) {
    logger.debug("Data: " + heavyCalculation()); // heavyCalculation() 무조건 실행
}

// 좋은 예 - 거의 오버헤드 없음
for (int i = 0; i < 5000; i++) {
    logger.debug("Data: {}", heavyCalculation()); // 아예 실행 안 됨
}

주의사항: Logback도 잘못 쓰면 느립니다

 
 
java
// 이렇게 쓰면 Logback도 System.out.println()과 비슷하게 느립니다
logger.debug("User: " + user + ", Data: " + expensiveOperation());

문자열 연결 방식(+)을 사용하면:

  • 로그 레벨과 상관없이 무조건 문자열 연산 발생
  • expensiveOperation()도 무조건 실행됨
  • 로그 레벨 필터링의 이점을 못 살림

반드시 파라미터 치환 방식({})을 사용해야 합니다!

실무 권장사항

1. 프로덕션 코드에서 System.out.println() 제거

 
 
java
// ❌ 절대 하지 마세요
for (User user : users) {
    System.out.println("Processing: " + user);
    // 비즈니스 로직
}

// ✅ 이렇게 하세요
for (User user : users) {
    logger.debug("Processing: {}", user);
    // 비즈니스 로직
}

2. 적절한 로그 레벨 설정

  • 개발: DEBUG
  • 스테이징: INFO
  • 프로덕션: WARN 또는 INFO

3. 반복문 안의 로깅 주의

대량 데이터를 처리할 때 반복문 안에서 로깅하면 성능에 큰 영향을 줍니다.

 
 
java
// 필요하다면 조건부로
if (i % 1000 == 0) {
    logger.info("Processed {} records", i);
}

4. 비동기 로깅 고려

높은 처리량이 필요한 경우 AsyncAppender 사용을 검토하세요.

결론

  • System.out.println()은 생각보다 훨씬 느립니다
  • 특히 대량 데이터 처리 시 병목이 될 수 있습니다
  • Logback을 사용하되, **파라미터 치환 방식({})**으로 사용해야 성능 이점을 누릴 수 있습니다
  • 사소해 보이는 코드도 대량 처리 시에는 큰 영향을 미칠 수 있습니다

프로덕션 환경에서 디버깅용 출력문을 제거하는 것만으로도 상당한 성능 개선을 얻을 수 있습니다.

반응형

댓글