개발을 하다 보면 반드시 마주치게 되는 것이 로그다. 에러의 원인을 추적할 때, 성능 병목을 진단할 때, 혹은 보안·감사 목적의 이력을 남길 때 로그는 거의 유일한 단서가 된다. 콘솔에 출력하든 파일에 남기든, 혹은 외부 시스템으로 전송하든 “무엇을 언제 어떻게 기록할 것인가”는 모든 서버 개발에서 공통적인 고민이다. Java 역시 오래전부터 로깅을 지원해왔고, 시간이 지나며 여러 방식과 라이브러리가 등장했다.
이 글에서는 Java에서 로그가 어떻게 발전해왔는지, 왜 지금의 구조가 되었는지, 그리고 실무에서 어떤 선택을 하는 것이 합리적인지 핵심만 정리한다.
Java 로그 JUL(java.util.logging)
Java에는 표준 라이브러리로 제공되는 로깅 기능이 있다. 바로 java.util.logging, 흔히 JUL이라 불리는 로깅 API다. JDK에 기본 포함되어 있어 별도 의존성 없이 사용할 수 있고, SEVERE, WARNING, INFO, FINE 등의 로그 레벨을 제공한다. 기본적인 로그 기록 자체는 충분히 가능하다.
문제는 구조와 성능이다. JUL은 Handler라는 개념을 통해 ConsoleHandler, FileHandler 등으로 로그를 출력한다. 이 Handler 내부를 보면 로그를 publish하거나 flush할 때 synchronized 블록 안에서 동작한다. 즉 하나의 Handler 인스턴스 기준으로 한 번에 하나의 스레드만 로그를 처리한다.
여기에 호출자 추론 비용, 포맷팅 비용, 비동기 로깅 미지원까지 더해지면 트래픽이 많은 서버 환경에서는 부담이 된다. 설정 방식도 직관적이지 않고, 환경별로 세밀한 제어를 하기 어렵다는 단점이 있다. 이 때문에 JUL은 “존재는 하지만 적극적으로 선택되지 않는” 표준 기능이 되었다.
Java 로그 SLF4J 퍼사드
JUL과 log4j 같은 구현체 중심 로깅의 가장 큰 문제는 “라이브러리를 바꾸기 어렵다”는 점이었다. 로깅 API가 코드에 직접 박혀 있으면 구현체를 교체할 때마다 모든 로깅 코드를 수정해야 한다. 이 문제를 해결하기 위해 등장한 개념이 퍼사드다.
SLF4J는 로깅의 실제 구현을 제공하지 않는다. Logger 인터페이스만 정의하고, 실제 로그 출력은 다른 라이브러리에 위임한다. 개발자는 SLF4J API만 사용하고, 어떤 구현체를 쓸지는 런타임에 결정한다. 이 구조 덕분에 코드는 고정하고 구현체만 교체할 수 있게 되었다.
중요한 점은 SLF4J는 반드시 바인딩이 필요하다는 것이다. 구현체가 없으면 로그는 출력되지 않는다. 또한 바인딩이 여러 개 존재하면 JVM의 ServiceLoader 동작에 따라 임의의 하나가 선택된다. 이 순서는 명세상 보장되지 않기 때문에 실무에서는 반드시 하나의 구현체만 두는 것이 원칙이다.
Java 로그 Logback 구현체
현재 Java 진영에서 가장 널리 사용되는 SLF4J 구현체는 Logback이다. Logback은 SLF4J를 만든 개발자가 직접 설계한 라이브러리로, 구조적 일관성과 안정성이 높다. 비동기 로깅, 설정 파일 분리, 환경별 로그 레벨 제어 등 실무에서 필요한 기능을 대부분 제공한다.
Logback은 XML 또는 Groovy 설정을 통해 콘솔, 파일, 롤링 정책 등을 세밀하게 제어할 수 있다. 또한 로그 레벨 판단을 먼저 수행해 불필요한 문자열 연산을 줄이는 구조를 갖는다. 이 덕분에 로그가 많아도 상대적으로 성능 부담이 적다.
Log4j2 역시 경쟁 구현체로 존재하지만, 일반적인 웹 애플리케이션에서는 Logback이 충분한 성능과 안정성을 제공한다. 중요한 것은 SLF4J + 단일 구현체 조합을 유지하는 것이다. 이 원칙만 지켜도 로깅으로 인한 문제의 상당 부분을 예방할 수 있다.
정리하면, Java 로그의 흐름은 JUL 같은 표준 기능에서 출발해 구현체 중심 로깅을 거쳐 SLF4J 퍼사드 구조로 수렴했다. 현재 기준에서 가장 무난한 선택은 SLF4J API를 사용하고 Logback을 단일 구현체로 두는 방식이다. 로그는 많을수록 좋지 않고, 필요한 정보를 일관되게 남기는 것이 핵심이다.