티스토리 뷰

개요 

운영 중인 애플리케이션에서 문제가 발생하면, 원인을 파악하기 위해 당시의 정보가 필요하다. 이를 위해 예외(Exception)가 발생했거나, 중요한 기능이 실행되는 부분에서 적절한 로그를 남겨야 한다.

가장 간단한 방법으로 System.out.println()을 이용해 로그를 출력하는 것이다. 이를 활용하면 실행 중 다양한 입출력 및 변수 값을 확인할 수 있다.

하지만 System.out.println()은 출력되는 로그의 양이나 수준을 조절할 수 없으며, 로그를 파일에 저장하기도 어렵다.

또한, 애플리케이션 성능 저하의 원인이 될 수 있다. println()의 내부 구현을 살펴보면 newLine()메소드를 호출하는데, 해당 메소드에는 synchronized키워드가 적용되어 있다. 즉, newLine()메소드는 임계영역(Critical Section)이 되어, 멀티스레드 환경에서 A 스레드가 해당 메소드를 실행하면, 다른 스레드는 A 스레드가 락을 반환할 때까지 대기해야 한다.

 

따라서 운영 환경에서는 System.out.println()을 사용하면 안 되며, 대신 적절한 로깅 프레임워크(Logback, Log4j 등)를 활용하는 것이 바람직하다.

println() 메소드 내부 구조
newLine() 메소드 내부

로깅 프레임워크

자바 개발자들을 위한 로깅 프레임워크는 다양하며, 자주 사용하는 주요 로깅 프레임워크는 다음과 같다.

  • Log4j2: Log4j는 Apache 재단에서 제공하는 로깅 프레임워크로, Log4j 1.x는 한때 가장 많이 사용되었으나 보안 문제로 단종되었다. 현재는 Log4j2가 Logback과 함께 주요 로깅 프레임워크로 사용된다.
  • Logback: Log4j 1.x를 개발한 Ceki Gulcu가 기존 Log4j의 단점을 개선하고 추가 기능을 더해 개발한 로깅 프레임워크이다. SLF4J와 기본적으로 호환되며, 성능이 뛰어나고 XML 기반의 설정이 가능하다.

SLF4J는 이러한 로깅 프레임워크를 하나의 통일된 방식으로 사용할 수 있도록 돕는다.

 

SLF4J(Simple Logging Facade For Java)란?

SLF4J는 로깅 Facade(퍼사드)로, 로깅에 대한 추상 레이어를 제공하는 로깅 인터페이스 라이브러리다. 여러 로깅 프레임워크를 하나의 통일된 방식으로 사용할 수 있는 방법을 제공한다.

 

SLF4J의 주요 구성 요소는 SLF4J API, Binding, Bridging으로 구성되어 있다. 각 구성 요소의 역할에 대해서 알아보자.

 

SLF4J API(slf4j-api.jar)

SLF4J API는 공통 로깅 인터페이스를 제공하여, 다양한 로깅 프레임워크(Logback, Log4j 등)와의 직접적인 결합을 제거하고 유연한 로깅 구성을 가능하게 한다. 이를 통해 개발자는 특정 로깅 구현체에 종속되지 않고, Logger와 LoggerFactory를 사용하여 로그를 남길 수 있다.

 

하지만 SLF4J 자체는 로그를 기록하지 않으며, 실제 로그 기록은 바인딩(Binding)을 통해 연결된 로깅 구현체(Logback, Log4j 등)가 수행한다. 따라서 코드 변경 없이 바인딩만 교체하면 손쉽게 로깅 프레임워크를 변경할 수 있어 유지보수성과 확장성이 향상된다.

 

정리하자면, SLF4J API는 로깅을 직접 처리하지 않고, 바인딩을 통해 적절한 로깅 프레임워크에 로그 출력을 위임하는 역할을 수행한다.

SLF4J API(출처: https://gmlwjd9405.github.io/2019/01/04/logging-with-slf4j.html)

import org.slf4j.Logger;  // slf4j을 사용하지만, 실제 로깅은 binding을 통해 의존성 추가한 구현체로 동작
import org.slf4j.LoggerFactory;

public class Slf4jExample {
    // Logger 인스턴스 생성 (클래스 이름을 로깅에 사용)
    private static final Logger log = LoggerFactory.getLogger(Slf4jExample.class);

    public static void main(String[] args) {
        log.info("This is an info message");
    }
}

 

 

SLF4J Binding

SLF4J 바인딩은 SLF4J API와 특정 로깅 구현체(Logging Framework)를 연결하는 어댑터 역할을 수행한다. 즉, SLF4J API에서 호출된 로깅 요청을 실제 로깅 프레임워크(Logback, Log4j 등)으로 전달하는 기능을 한다.

 

SLF4J는 컴파일 시점에 하나의 로깅 프레임워크만 바인딩되도록 설계되었으며, 클래스패스에서 바인딩된 구현체가 발견되지 않으면 기본적으로 no-operation(NOP)모드로 동작하여 로그가 출력되지 않는다. 따라서, 원하는 로깅 프레임워크를 사용하려면 해당 프레임워크에 맞는 SLF4J 바인딩을 추가해야한다.

// binding을 찾지 못하는 경우(no-operation)
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". 
SLF4J: Defaulting to no-operation (NOP) logger implementation 
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

SLF4J 바인딩 위치(출처 - https://www.slf4j.org/manual.html)

 

이 중, logback-classic.jar는 SLF4J의 기본 로깅 구현체로 널리 사용된다. Logback을 사용할 경우, SLF4J를 네이티브 구현하므로 별도의 SLF4J 바인딩(slf4j-[binding].jar)이 필요하지 않으며, 메모리 오버헤드 없이 최적화된 성능을 제공한다.

네이티브 구현 logback-classic(출처: https://www.slf4j.org/manual.html)

 

logback-classic을 추가하면 SLF4J API(slf4j-api.jar)와 로깅 구현체(logback-core.jar)까지 포함되는데, 이는 Logback이 SLF4J의 공식 로깅 구현체이기 때문이다. 즉, logback-classic을 사용할 경우 별도로 SLF4J API을 추가할 필요가 없다.

 

그러나 slf4j-api를 명시하면 프로젝트에서 일관된 버전 관리 가능하고 다른 로깅 프레임워크로 변경시에도 유연하게 대처 가능하므로 분리 해주는 편이 좋다.

dependencies {
	// SLF4J API, 구현체 포함(SLF4J API를 직접 구현하므로 Binding은 필요하지 않음.)
    implementation 'ch.qos.logback:logback-classic:1.2.3'
}
dependencies {
	// 명시적으로 분리 해주기(일관된 버전 관리, 로깅 프레임워크 변경시 유연하게 대처 가능)
    implementation 'org.slf4j:slf4j-api:1.7.25'
    implementation 'ch.qos.logback:logback-classic:1.2.3'
}

 

 

SLF4J Bridging

일부 컴포넌트가 SLF4J이외에 로깅 구현체(Log4j, JUL 등)에 직접 의존하고 있다면, 로깅 관리가 일관되지 않게 된다. SLF4J Bridging은 구현체에 대한 호출을 대신 SLF4J API에 대한 것처럼 리다이렉션 하여 SLF4J 바인딩을 통해 실제 로깅 구현체로 전달하는 역할을 한다.

 

따라서 다른 로깅 API -> Bridge -> SLF4J API -> Bindging -> 로깅 구현체 순으로 동작하게 된다.

SLF4J Bridge 동작 과정(출처: https://gmlwjd9405.github.io/2019/01/04/logging-with-slf4j.html)

 

 

SLF4J 올바른 사용법

로깅 레벨은 로그의 중요도(심각도) 를 나타내며, 설정된 레벨에 따라 출력 여부를 결정할 수 있다.

로깅 레벨은 TRACE < DEBUG < INFO < WARN < ERROR 순으로 높아지며, 레벨이 높을수록 더 중요한 로그를 의미한다.

 

예를 들어, 현재 로그 레벨이 INFO로 설정되어 있다고 가정하면, INFO 이상(INFO, WARN, ERROR)만 출력되고, DEBUG와 TRACE 로그는 출력되지 않는다.

 

logback.xml설정(로그레벨: INFO)
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoggingExample {
    private static final Logger log = LoggerFactory.getLogger(LoggingExample.class);

    public static void main(String[] args) {
        log.trace("TRACE 레벨의 로그입니다.");  // 출력되지 않음
        log.debug("DEBUG 레벨의 로그입니다.");  // 출력되지 않음
        log.info("INFO 레벨의 로그입니다.");    // 출력됨
        log.warn("WARN 레벨의 로그입니다.");    // 출력됨
        log.error("ERROR 레벨의 로그입니다.");  // 출력됨
    }
}

 

성능을 위해 치환 문자를 사용하자

현재 아래의 예시는 모두 동일한 결과를 출력한다. 그리고 INFO, WARN, ERROR로그 레벨 처럼 DEBUG보다 상위 레벨일 경우 4개의 방법 모두 동일하게 출력이 일어나지 않는다. 그러나, 성능면에서는 차이가 발생한다.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoggingExample {
    private static final Logger log = LoggerFactory.getLogger(LoggingExample.class);

    private void test(String str) {
        // 문자열 직접 연산 (비효율적인 방식)
        log.debug("DEBUG 레벨의 로그: " + str + ".");

        // 문자열 연산을 방지하는 방법1 (가용 로그 레벨 체크)
        if (log.isDebugEnabled()) {
            log.debug("DEBUG 레벨의 로그: {}: "+ str + ".");
        }

        // 문자열 연산을 방지하는 방법2 (치환문자 사용)
        log.debug("DEBUG 레벨의의 로그: {}.", str);
        
        // 문자열 연산을 방지하는 방법3 (가용 로그 레벨 체크 & 치환문자 사용)
        if (log.isDebugEnabled()) {
            log.debug("엄청 긴 DEBUG 레벨의 로그: {}.", str);
        }
        
    }

}

 

 

문자열 직접 연산 (비효율적인 방식)

가용 로그 레벨을 상위 로그 레벨인 INFO로 설정했다고 가정하면, 로그가 남는 과정이 생략되기 때문에 성능 개선이 이뤄져야한다. 그러나, log.debug()메소드가 실행되기전에  "DEBUG 레벨의 로그: " + str + "."문자열 연산이 먼저 일어나서 문자열 연산만큼 성능이 낭비된다.

즉, 출력되지 않는 로그임에도 불필요한 연산 비용이 발생하여 성능이 낭비된다.

// 문자열 직접 연산 (비효율적인 방식)
log.debug("DEBUG 레벨의 로그: " + str + ".");

 

문자열 연산을 방지하는 방법1 (가용 로그 레벨 체크)

가용 로그 레벨을 체크하는 조건문을 추가하면 INFO 로그 레벨이라면 log.debug()메소드가 실행되지 않을 뿐더러, 문자열 연산도 일어나지 않기 때문에 성능 낭비가 없다.

// 문자열 연산을 방지하는 방법1 (가용 로그 레벨 체크)
if (log.isDebugEnabled()) {
    log.debug("DEBUG 레벨의 로그: {}: "+ str + ".");
}

 

문자열 연산을 방지하는 방법2 (치환문자 사용)

치환문자"{}"를 사용하면 SLF4J 내부적으로 로그 레벨을 먼저 체크한 후, 필요할 때만 치환 문자를 실제 값으로 변환한다. 가독성도 상당히 좋아 가장 권장하는 방식이다.

// 문자열 연산을 방지하는 방법2 (치환문자 사용)
log.debug("DEBUG 레벨의 로그: {}.", str);

SLF4J AbstractLogger 추상 클래스

 

문자열 연산을 방지하는 방법3 (가용 로그 레벨 체크 & 치환문자 사용)

긴 문자열을 포함하는 경우에 가장 권장 되는 방식이다. 일반적으로는 방법2 처럼 치환 문자만 사용하면 가독성도 챙기면서 충분한 성능을 보장하지만, 로그 문자열이 아주 긴 경우 문자열 리터럴이 생성되는 비용까지 줄이기 위해 if (logger.isDebugEnabled())로 한번 더 체크 해주는 것이 유리하다.

// 문자열 연산을 방지하는 방법3 (가용 로그 레벨 체크 & 치환문자 사용)
if (log.isDebugEnabled()) {
    log.debug("엄청 긴 DEBUG 레벨의 로그: {}.", str);
}

 

 

 

참고 자료

- 박재성, 자바 웹 프로그래밍 Next Step, 2016.09.19 로드북, 107p - 112p

- 로깅을 System.out.println() 로 하면 안되는 이유, https://hudi.blog/do-not-use-system-out-println-for-logging/

- [Logging] SLF4J를 이용한 Logging, https://gmlwjd9405.github.io/2019/01/04/logging-with-slf4j.html

- slf4j 공식 문서, https://www.slf4j.org/manual.html

- [Logging] SLF4J란?, https://livenow14.tistory.com/63

- SLF4J Logger 사용법 & 잘못된 사용법: Binding Parameters, Logging Exception Stack Trace

'Spring' 카테고리의 다른 글

리플렉션을 이용한 IoC 컨테이너 구현해보기  (0) 2025.07.13
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/12   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함