System.out.println 로깅시 사용하지 말아야 하는이유?

2022. 12. 10. 15:21카테고리 없음

1)System.out.pirntln 메소드는 무엇인가?

Hello World!

우리는 너무도 쉽게 System.out.println()을 사용한다.
다음은 Hello world!를 출력하는 코드이다.

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}
Hello World!

뭔가 이상하다

그런데 이런 생각을 한 적 있는가?
println() 메서드는 PrintStream 클래스의 메서드이지만 코드 어디에도 PrintStream 이라는 이름은 찾을 수 없다. 게다가 System.out.println() 대신 PrintStream.println()을 사용하면 컴파일 에러가 발생한다.

java: non-static method println(java.lang.String) cannot be referenced from a static context

println() 메서드는 인스턴스(non-static) 메서드이기 때문에 클래스를 통해 외부로 호출할 수 없다. 이는 PrintStream 클래스의 내부 코드를 보면 알 수 있다.

    public void println(String x) { // non-static method
        synchronized (this) {
            print(x);
            newLine();
        }
    }

여기서 만약 println() 메서드를 사용하려면 java.io 패키지를 import하고 PrintStream 클래스에 대한 객체를 생성해야 한다. 그러나 System.out은 import 조차 없이 println() 메서드를 호출한다. 대체 System.out이 뭘까?

java.lang.System

먼저 System 클래스는 java.lang 패키지에 속한 클래스이다. java.lang 패키지는 컴파일 단계에서 암묵적으로(implicitly) 다음과 같이 추가된다.

import java.lang.*

이 때문에 우리는 java.lang 패키지를 명시하지 않아도 System 클래스를 사용할 수 있다.

out

자바에서는 점 표기법(dot notation)을 통해 클래스의 속성(attribute)에 접근한다. 즉 System.out에서 out은 System 클래스의 필드 또는 메서드이다.

	public final class System {
      ...
      
      public static final PrintStream out = null;
      
      ...
	}

다음과 같이 System 클래스는 out이라는 이름의 PrintStream 클래스 변수(상수)를 필드로 가진다. 위에서 살펴보았듯이 PrintStream 클래스는 println() 메서드를 포함하기 때문에 우리는 System.out 객체를 통해 println() 메서드를 호출할 수 있다!

결론

  1. println() 메서드는 PrintStream 클래스의 인스턴스 메서드이다.
  2. java.lang 패키지는 컴파일 단계에서 묵시적으로 선언되므로 java.lang.System 클래스는 import로 명시하지 않아도 된다.
  3. System 클래스는 PrintStream 클래스 타입의 필드를 가진다(==out). 정적(static)으로 선언되어 있기 때문에 어디서든 System.out과 같이 클래스를 통해 호출할 수 있다 (애초에 System 클래스는 인스턴스 생성이 안 된다. private 생성자로 막아두었기 때문).

2)로깅을 System.out.println() 로 하면 안되는 이유

 

휘발된다

System.out.println() 은 로그가 표준 출력으로 출력된다. 즉, 파일로 저장되지 않고 휘발된다는 의미이다. 로그는 에러가 발생한 상황을 기록하고, 추후 확인하여 문제를 진단하고, 재현하고, 고치기 위해 사용된다. 하지만 표준 출력으로 한번 출력되고 어디에도 저장되지 않으면 로그의 제 역할을 할 수 없다.

로그된 데이터는 실제로 기록되어야 한다. 하지만 System.out.println() 만으로는 불가능하다.

에러 발생 시 추적할 수 있는 최소한의 정보가 남지 않는다

System.out.println() 은 인자로 전달한 문자열만을 출력한다. 문제가 발생한 날짜, 시각 그리고 문제의 수준, 로그가 발생한 위치 등 최소한의 정보가 기록되지 않는다는 것 이다. 이런 제한적인 정보만으로는 문제를 해결하기 어려울 것 이다. 물론 이런 정보도 함께 인자로 전달한다면 충분히 에러와 장애를 추적할 수 있는 정보를 남길수야 있지만… 매번 그런 정보를 일일히 남기기엔 번거로울 것 이다.

로그 출력 레벨을 사용할 수 없다

로컬에서 개발할 때에는 디버깅을 위한 아주 상세한 정보가 출력되어 확인할 수 있어야한다. 하지만, 프로덕션에서 동작하는 코드는 에러/장애가 발생할 때 문제를 진단할 수 있는 정보만을 남겨야한다. 개발시에만 사용되는 정보와 문제 상황에 대한 정보가 함께 로깅된다면 문제 해결을 위한 정작 중요한 정보를 얻기 힘들 뿐더러, 민감한 정보를 로그로 남길수도 있기 때문이다. 또한 의미없는 로그가 쌓여 서버 용량을 차지할 수도 있다.

따라서 로깅 라이브러리는 환경에 맞게(로컬 개발 환경, 개발 서버, 프로덕션 서버 등) 로그가 출력될 수 있도록 로그 출력 레벨이라는 기능을 제공한다. 많이 사용되는 Logback이라는 라이브러리에서는 TRACE, DEBUG, INFO, WARN, ERROR, FATAL 와 같은 레벨을 제공한다. 하지만 System.out.println() 은 이런 기능을 제공하지 않는다. 어떤 환경에서든 동일한 로그가 출력된다. 프로덕션에서 이런 로그를 제거하려면 코드를 일일히 제거하거나 주석처리하거나 별도의 조건문을 설정하는 등 번거로운 일들을 해야한다.

성능저하의 원인이 될 수 있다

System.out.println() 의 구현을 한번 살펴보자.

/**
 * Terminates the current line by writing the line separator string.  The
 * line separator string is defined by the system property
 * {@code line.separator}, and is not necessarily a single newline
 * character ({@code '\n'}).
*/
public void println() {
    newLine();
}

println()  newLine() 을 호출한다. newLine() 의 구현도 살펴보자.

private void newLine() {
    try {
        synchronized (this) {
            ensureOpen();
            textOut.newLine();
		// ...

synchronized 키워드가 붙어있다. 이때 newLine() 메소드는 임계영역(critical section)이 된다. 멀티 쓰레드 환경에서 A 쓰레드가 newLine() 메소드를 실행하면, 메소드는 잠기게 된다. 다른 쓰레드는 A 쓰레드가 모두 사용하고 잠금을 풀어준 뒤에서야 newLine() 메소드를 실행할 수 있다. 오버헤드가 발생하게 되는 것 이다.

*오버해드:단순히 처리하면 10초 걸리는데 , 안전성을 고려 하고 부가적인 B라는 처리를 추가한 결과 처리시간에 15초를 걸렸다면 오버해드는 5초이다, 또한 이 처리 B를 개선해 "B"라는 처리를 한 결과 , 처리시간이 12초가 되었다면 이경우 오버해드가 3초 단축되었다고 말한다.

 

스프링을 실행하는 톰캣은 멀티 쓰레드로 동작한다. 요청이 오면 쓰레드 풀에서 쓰레드를 하나 가져와 요청을 처리한다. 그런데, System.out.println() 을 여러 쓰레드가 사용하면 그만큼 위에서 이야기한 오버헤드가 발생하고 처리가 느려질 것 이다. 따라서 실제 프로덕트의 코드에서는 System.out.println() 을 절대 사용해서는 안된다.

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

 
Previous Post