Java

System.out.println()와 로그

변위니 2025. 1. 7. 02:03

실무에서는 System.out.println() 대신 Logging를 사용한다.

 

 

System.out.println()

System.out.println()은 Java 에서 콘솔에 출력할 때 사용되는 메서드로 주로 간단한 디버깅 목적으로 사용한다.

 

내부적으로 출력 버퍼를 사용하여 데이터를 출력한다. 따라서 작은 양의 데이터로 버퍼를 채우고 비우는 작업이 필요하다.

따라서, 오버헤드가 발생할 수 있고, 객체를 출력하기 위해 내부적으로 문자열 변환을 해야 하기 때문에, 문자열 작업이 추가적으로 필요하다.

 

System.out은 단일 인스턴스로, 여러 스레드가 동일한 System.out 객체에 동시에 접근할 수 있다. 이 때문에 동시에 여러 스레드가 출력을 시도하면 출력 결과가 뒤섞일 수 있다.

이를 방지하기 위해 System.out.println은 동기화(synchronized)되어 있어, 한 번에 하나의 스레드만 이 메서드를 실행할 수 있다.

동기화된 메서드는 한 번에 하나의 스레드만 접근할 수 있기 때문에, 다른 스레드는 현재 실행 중인 스레드가 System.out.println()을 완료할 때까지 대기해야 한다. 이 과정에서 스레드가 차단되어, CPU 자원이 낭비될 수 있다.

예를 들어, 두 개의 스레드가 System.out.println()을 호출하려고 대기하고 있으면, 두 번째 스레드는 첫 번째 스레드가 출력 작업을 끝낼 때까지 기다려야 한다.

 

System.out.println은 콘솔에 출력을 하기 위해 I/O 작업을 수행하는데, 이 작업은 Blocking 방식으로 동작한다.

따라서, 출력 작업이 완료될 때까지 스레드는 대기 상태에 있는다. 출력이 네트워크 연결에 의존하거나, 콘솔에 출력되는 시간이 길어지는 경우, 출력 작업이 완료될 때까지 스레드는 기다려야 한다. 이로 인해 I/O 대기 시간이 길어지며, 스레드가 차단되고 CPU 자원이 낭비된다.

 

여러 스레드가 System.out.println()을 호출할 때, 스레드가 차단되거나 대기하는 동안 CPU 자원을 사용하지 않기 때문에, CPU의 사용률이 낮아지고 성능 저하를 초래할 수 있다.

 

동기화는 여러 스레드가 동일한 자원에 동시 접근할 때 발생할 수 있는 경쟁 조건을 방지할 수는 있지만, 다른 스레드가 대기 상태가 되어 시스템 성능에 악영향을 미칠 수 있다.

스레드의 차단이 많을 수록 전체 시스템의 처리 속도가 떨어지고, 응답 시간이 증가할 수 있다. 

 

System.out.println의 이러한 특성(synchronized, blocking I/O)때문에 멀티스레드 환경에서는 System.out.println보다는 로깅 프레임워크를 사용하는 것이 좋다.




해결 

log4j, SLF4J, java.util.logging 등과 같은 로깅 프레임워크를 사용하면 로그 레벨 조절, 파일에 로그를 저장, 비동기 로그 처리가 가능하다.

 

로깅 레벨을 설정하여 디버깅, 정보, 경고, 오류 등 로그의 중요도에 맞게 출력할 수 있다.

예를 들어, 개발 중에는 디버그 로그를 출력하고, 배포 환경에서는 정보와 경고 수준의 로그만 출력하도록 할 수 있다.

 

<Log Level>

TRACE > DEBUG > INFO > WARN > ERROR > FATAL
  • TRACE: DEBUG보다 더 자세한 정보.
  • DEBUG: 디버깅용 정보.
  • INFO: 일반 정보나 상태 변경.
  • WARN: 주의가 필요한 상황, 문제의 징후.
  • ERROR: 처리 중 오류 발생.
  • FATAL: 심각한 오류로 시스템 작동 불가.
Logger log = LoggerFactory.getLogger(getClass());
log.info( "log info ~" )​
 

 

로그를 콘솔뿐 아니라 파일에 기록할 수 있으며, 여러 로그 파일로 분리하여 저장하거나, 파일 크기가 일정 크기를 넘으면 새로운 파일로 로그를 분리할 수 있다. 그리고 로그를 비동기로 처리하여 성능 저하를 방지할 수 있다.  

로그 파일을 통해 특정 문제의 발생 시점을 추적하거나, 예외 및 오류를 빠르게 파악할 수 있다.

 

 

 

로깅에서는 자주 문자열을 결합하여 출력한다. 예로 로그 메시지 생성 시간, 로그 레벨, 메시지 내용 등을 결합한다고 했을 때, 문자열을 + 연산자로 결합하면 매번 새로운 객체가 생성된다.

이럴 때, StringBuilder, String.format을 사용하자.

StringBuilder는 내부 버퍼를 사용하여, 문자열을 결합할 때마다 기존 버퍼에 추가하여 처리하며, String.format을 사용해 포맷팅된 문자열을 생성할 수 있다. 반복적인 문자열의 결합이 필요한 경우 성능 향상에 도움이 되며, 가변 크기의 문자열을 처리할 수 있고, 문자열을 효율적으로 조작할 수 있다.

 

로깅 시스템은 여러 로그 메시지를 한 번에 처리하여 I/O 작업 횟수와 오버헤드를 줄이고 성능을 개선할 수 있다.




출력 빈도를 최소화하여 성능을 개선하는 방식

반복적인 출력을 피하기 위해 데이터를 한 번에 모아서 출력하거나, 조건을 걸어서 출력이 필요한 경우에만 출력할 수도 있다.
또는 로그 레벨을 설정하여 특정 레벨 이상의 로그만 출력하도록 할 수 있다.

출력 버퍼링을 사용할 경우, 적절한 버퍼의 크기를 선택해야 한다. 작은 크기는 자주 비워져야하기 때문에 입출력이 자주 발생할 수 있고, 반대로 너무 큰 버퍼는 메모리를 많이 차지한다.

 

또한 멀티 스레드 환경에서 여러 스레드가 동시에 출력 버퍼에 접근할 수 있기 때문에 동기화를 통해 스레드간의 충돌을 방지해야 하며, 출력 중에 발생할 수 있는 예외를 처리하는 방법이 필요하다.

 

 

 

멀티 스레드 환경에서 출력 버퍼

멀티스레드 환경에서 여러 스레드가 동일한 자원(콘솔, 로그 파일)에 동시에 접근하려고 할 때,

만약 여러 스레드가 동시에 출력 작업을 수행하면 출력 결과가 뒤섞이거나 예상치 못한 결과가 발생할 수 있다.

 

이를 방지하려면, 출력 버퍼(예: 콘솔, 로그 파일)에 접근하는 작업을 동기화하여 한 번에 하나의 스레드만 접근할 수 있도록 해야 한다.

synchronized 메서드나 블록을 사용하여 한 번에 하나의 스레드만 접근할 수 있도록 하거나, ReentrantLock를 사용하여 락을 획득하고 해제할 수 있다.

 

 

출력 중에 발생한 예외를 처리하려면

출력하는 동안 발생할 수있는 예외를 try-catch블록으로 감싸서 처리하자.

아니면, 메서드 내에서 예외를 직접 처리하지 않고 호출자에게 예외를 던지도록 하자. 예외 발생시 로그를 남겨 문제를 쉽게 발견할 수 있도록 하고, 예외 발생 이전 상태로 돌리는 방법을 고려하자.

 

catch 블록에서 예외를 로그로 기록하고, 상태를 복구하기 위해 어떻게 로깅을 해야하는지

catch 블록에 예외 로그를 기록할 때 로그 레벨과 예외 메시지를 포함하여 기록하자.

상태를 복구하기 위해 상태 복구 메서드를 직접 생성하여 호출할 수 있다. 예를 들어 데이터베이스 트랜잭션에서 예외 발생 시 이전 상태로 롤백하는 작업을 수행한다.

 

 

 

 

 

이 글의 결론은 System.out.println()을 사용하는 것보다 로깅 프레임워크를 활용하는 것이 좋다.

'Java' 카테고리의 다른 글

StringBuffer & StringBuilder  (0) 2025.01.07
자바 직렬화 Serialization  (0) 2025.01.07
synchroinzed와 DeadLock  (0) 2025.01.07
Generic과 Object  (0) 2025.01.07
Exception  (1) 2025.01.07