[Java] System.out.println의 사용을 지양하자
개발자는 프로젝트를 진행하면서 요청을 기록 및 관찰하기 위해 logging을 한다.
아마 초반에 토이 프로젝트를 하다보면 요청이 잘 갔는지 확인해보기 위해 System.out.println을 사용하는 경우가 많을 것이다.
이번에 말할 주제는 제목과 같이 System.out.println의 사용을 지양해야 한다는 것이다. 왜 그래야하는지 한 번 살펴보자.
logging framework
만약 자바/스프링부트 기반의 프로젝트를 진행해봤다면, 스프링부트 프로젝트 생성 시 로깅 프레임워크들(log4j2, logback 등)이 추가되는 것을 볼 수 있다.
이 로깅 프레임워크들을 이용해서 다음과 같이 logging을 할 수 있다.
class App {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(App.class);
logger.info("start");
}
}
기본적으로 프로젝트할 때 이런 로깅 프레임워크를 통해 로깅할 것을 권장하고 있다. 이는 위에서 말한 "System.out.println 사용을 지양하자"와 이어지는 말이다. 그 이유가 무엇일까?
System.out.println의 문제점
성능 문제
먼저 성능의 문제이다.
System.out.println은 Stream을 통해 콘솔에 출력을 해주는 역할을 한다. 여기서 알 수 있듯이 당연하게 I/O 작업이 동반된다. 실행하고 있는 해당 스레드에서 blocking I/O 처리로 인해 프로그램이 느려지는 결과가 초래된다.
또 println의 내부 코드를 살펴보면
이렇게 synchronized block으로 감싸 동기화 처리를 해준 것을 볼 수 있는데, 이 때문에 성능 저하가 올 수 있다. 물론 적은 요청에서는 눈에 띌만 한 차이는 발생하지 않는다. 더군다나 console에 출력할 경우 로그의 경우도 동기화처리는 필수적이기 때문에 큰 성능 차이를 기대하기 힘들다.
1,000,000의 요청을 가정하고 실행 시간을 테스트 해봤을 때 오히려 log가 더 오래 걸린 것을 확인할 수 있다.
로깅 프레임워크들은 thread safe 하지 않은건가?
스프링에서 기본적으로 추가되는 로깅 프레임워크들(log4j2, logback)은 thread safe하게 구현되어 있다.
다만 synchronized block을 사용하여 동기화 처리를 하기 보다는, 좀 더 효율적으로 처리한다. 또한 비동기 로깅 방식도 지원한다.
위 logback에서는 ReentrantLock을 통해 처리하는 것을 확인할 수 있다.
로그 레벨 관리 문제
로깅 프레임워크는 여러 로그 레벨에 따라 구분하여 로깅할 수 있다. 예를 들어, 디버깅 목적일 경우 debug level, 정보 목적일 경우 info level, 경고 목적일 경우 warn level 등으로 설정할 수 있다.
logger.trace("Trace");
logger.debug("Debug");
logger.info("Info");
logger.error("Error");
logger.warn("Warn");
logger.fatal("Fatal");
하지만 println()의 경우 System.out.println()과 System.err.println() 두 가지로만 사용할 수 있다. 여러 상황이 많은 운영 환경에서는 이러한 레벨별 로그 관리가 중요하므로 로깅 프레임워크 사용을 권장한다.
로그 추적 문제
로깅 프레임워크를 통해 로그를 남길 경우 다음과 같이 기본적인 시간, 로그 레벨 등의 정보를 남길 수 있다.
하지만 System.out.println() 의 경우 이러한 기능을 지원해주지 않아 하나씩 직접 남겨야하며, 특히 파일에 로그를 기록해야 할 경우 추가적인 파일 입출력 구현이 필요하다.
로깅 프레임워크는 안심하고 사용해도 되나?
그럼 System.out.println() 대신 로깅 프레임워크를 쓴다면 안심하고 써도 될까?
예상했듯이 로깅 프레임워크도 조심해서 써야할 필요가 있다.
로깅 프레임워크를 통해 로깅하는 것도 결국 I/O 작업을 수반하며, 그에 따른 동기화 처리도 불가피하게 작동되어야 한다. 때문에 로그를 남발하게 되면 애플리케이션에 성능 저하가 일어날 수 있으므로 조심해서 써야한다.
로그는 꼭 필요한 곳에 사용하고 레벨을 적절히 사용하는 것이 중요하다. 또 추가적으로 배치 처리나 버퍼링을 통해 I/O 작업을 줄일 수 있다.
로깅 프레임워크들은 비동기 처리를 지원하므로, 만약 대규모 애플리케이션이라면 이를 통해 성능 개선을 기대할 수 있다.
https://logging.apache.org/log4j/2.x/manual/async.html
마무리
로깅 프레임워크를 사용하는 습관을 들이는 것이 중요해보인다. 또 로깅도 성능적으로 무시할 수 없는 부분이 많다. 이를 위해선 로깅 프레임워크의 구조와 활용 전략에 대해 더 알아보는 것이 좋을 것 같다.
참고 자료
https://logging.apache.org/log4j/1.x/faq.html#a1.7
https://logging.apache.org/log4j/2.x/manual/async.html
https://docs.oracle.com/javase/7/docs/api/java/util/logging/Logger.html
https://www.quora.com/What-is-Log4j-and-how-is-it-thread-safe
https://www.quora.com/Is-Log4J-thread-safe-Is-Log4j-only-for-Java
https://liltdevs.tistory.com/180
'Language > Java' 카테고리의 다른 글
[Java] 위도/경도 값에 BigDecimal or double? (2) | 2024.04.30 |
---|---|
[Java] utility class는 무엇으로 구현하는 것이 좋을까? (1) | 2024.04.15 |
[Java] 제네릭(Generic) (0) | 2024.03.12 |
[Java] Mockito (0) | 2023.07.31 |
[Java] hashCode() (1) | 2023.07.02 |
댓글
이 글 공유하기
다른 글
-
[Java] 위도/경도 값에 BigDecimal or double?
[Java] 위도/경도 값에 BigDecimal or double?
2024.04.30 -
[Java] utility class는 무엇으로 구현하는 것이 좋을까?
[Java] utility class는 무엇으로 구현하는 것이 좋을까?
2024.04.15 -
[Java] 제네릭(Generic)
[Java] 제네릭(Generic)
2024.03.12 -
[Java] Mockito
[Java] Mockito
2023.07.31