Logback

June·2024년 9월 22일
1

사이드프로젝트

목록 보기
2/2

Logback이란?

Logback은 Java & Kotlin 기반 애플리케이션에서 로그 관리를 위한 로깅 프레임워크입니다. SpringBoot의 경우 기본적으로 Lobback을 로깅 시스템으로 채택하고 있습니다.

SLF4J(Simple Logging Framework For Java Application)는 인터페이스이고 그걸 구현한 것들 (Logback, Log4j..) 중 하나가 Logback입니다.

왜 로깅 프레임워크를 사용해야할까?

System.out.println()을 써도 필요한 정보를 콘솔에 남길 수 있지만 로그 레벨 설정이 불가능합니다. 개발환경인지, 운영환경인지에 따라 남겨야 할 로그가 다를 수 있습니다. 이를 정하는 것이 로그레벨입니다. 로컬에서는 간단한 디버그 목적으로 남기는 로그가 프로덕트 환경에서까지 남는다면 많은 트래픽이 올때 불필요하게 많은 데이터를 생성할 수 있습니다.

또한 println은 성능 저하를 가져올 수 있습니다 (synchornized를 쓰기 때문).

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

그럼 ConsoleAppender는..?

System.out.println()이 내부적으로 synchronized를 쓰기 때문에 성능이 떨어진다는 것은 이해했다. 그런데 로깅 프레임워크에서 ConsoleAppender는 그럼 어떻게 콘솔에 출력하는걸까?

Logback의 ConsoleAppender 구현
https://github.com/qos-ch/logback/blob/42caff87ae6eb553dcbf77e25ba87a9d340357ee/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java#L44

ConsoleAppender에서는 OutputStream을 쓰고 있는 것을 볼 수 있다

결론은 System.out은 PrintStream을 쓰고 대부분의 메서드가 synchronized로 선언되어 있어 스레드 안전하지만 성능 저하가 발생할 수 있다. OutputStreamWriter: 자체적으로 동기화를 제공하지 않습니다. 필요한 경우 외부에서 동기화를 구현해야 한다.

또 ConsoleAppender는 로깅을 비동기적으로 처리할 수 있어서 성능차이가 나는듯.

Logback 특징

  1. 고성능: 빠른 속도와 적은 메모리 사용량. 비동기 로깅 지원.
  2. 유연성: 로깅 출력 대상, 로그레벨, 패턴 등 세밀하게 제어 가능
  3. 자동 롤링 지원: RollingFileAppender는 용량이나 시간에 따라 자동으로 분할해서 관리 가능
  4. 다양한 Appender 지원: 파일, 콘솔, db등으로 보낼 수 있는 Appender 제공

Logback vs log4j2

둘다 지원하는 기능은 거의 같다.

log4j2 홈페이지에서 사라진 성능 비교

https://logging.apache.org/log4j/2.x/performance.html
여기 들어가면 not found가 나온다. https://logging.apache.org/log4j/2.x/manual/performance.html#layouts-location
여기 가도 이제 logback과 비교하는건 없다.

logback 홈페이지에 있는 성능 비교

https://logback.qos.ch/performance.html

logback 홈페이지에는 logback이 더 좋다고 나오긴한다. log4j2 측에서 제시한건 사라진걸 보면 logback 성능이 더 좋기는한듯.

사실 그걸 떠나서 log4j2는 2022년에 보안 이슈가 2번 터져서 신뢰하기 어렵다라는 생각도 있다.

Logback 구성과 사용예시

  1. Logger: 애플리케이션 코드에서 로그 메시지를 생성하는 객체

    • TRACE, DEBUG, INFO, WARN, ERROR 등 레벨을 가진다.
  2. Appender: Logger로부터 전달받은 로그 이벤트를 실제 출력 대상으로 내보내는 역할

    • ConsoleAppender, FileAppender, RollingFileAppender, SocketAppender 등등
  3. Layout: 로그 메시지의 형식을 정의합니다.

  4. Filter: 로그 이벤트를 필터링하여 특정 조건에 부합하는 로그만을 처리

  5. LoggerContext: Logger와 Appender 등의 구성요소를 관리하는 환경. 설정 파일의 변경을 감지하여 동적으로 재구성할 수 있음

  6. Configuration: Logback은 XML, Groovy, 또는 Java 코드를 통해 구성

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!-- Spring 프로퍼티 가져오기 -->
    <springProperty scope="context" name="LOG_LEVEL" source="logging.level.root" defaultValue="INFO" />

    <!-- ConsoleAppender 정의 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- AsyncAppender로 ConsoleAppender 감싸기 -->
    <appender name="ASYNC_CONSOLE" class="ch.qos.logback.classic.AsyncAppender">
        <queueSize>5000</queueSize>
        <discardingThreshold>0</discardingThreshold>
        <neverBlock>true</neverBlock>
        <appender-ref ref="CONSOLE" />
    </appender>

    <!-- SocketAppender 정의 -->
    <appender name="SOCKET" class="ch.qos.logback.classic.net.SocketAppender">
        <!-- 원격 로그 서버의 호스트와 포트 -->
        <remoteHost>${LOG_SERVER_HOST:-localhost}</remoteHost>
        <port>${LOG_SERVER_PORT:-4560}</port>
        <!-- 연결 재시도 간격 (밀리초) -->
        <reconnectionDelay>10000</reconnectionDelay>
        <!-- 호출자 데이터 포함 여부 -->
        <includeCallerData>false</includeCallerData>
    </appender>

    <!-- AsyncAppender로 SocketAppender 감싸기 -->
    <appender name="ASYNC_SOCKET" class="ch.qos.logback.classic.AsyncAppender">
        <queueSize>5000</queueSize>
        <discardingThreshold>0</discardingThreshold>
        <neverBlock>true</neverBlock>
        <appender-ref ref="SOCKET" />
    </appender>

    <!-- 루트 로거 설정 -->
    <root level="${LOG_LEVEL}">
        <appender-ref ref="ASYNC_CONSOLE" />
        <appender-ref ref="ASYNC_SOCKET" />
    </root>

    <!-- Spring 프로파일을 활용한 환경별 설정 -->
    <!-- 개발 환경 -->
    <springProfile name="dev">
        <logger name="com.example" level="DEBUG" />
    </springProfile>

    <!-- 운영 환경 -->
    <springProfile name="prod">
        <logger name="com.example" level="WARN" />
    </springProfile>

</configuration>

1. Spring 프로퍼티 가져오기

<springProperty scope="context" name="LOG_LEVEL" source="logging.level.root" defaultValue="INFO" />
  • logging.level.root 값을 가져와서 LOG_LEVEL 변수에 저장한다
  • 이 변수는 이후에 ${LOG_LEVEL}로 참조된다.

2. ConsoleAppender

  • filter:
    - thresholdFilter를 사용하여 지정된 레벨 이상의 로그만 출력합니다.
  • encoder:
    - pattern을 사용하여 로그 메시지의 형식을 지정합니다.

3. AsyncAppender

<appender name="ASYNC_CONSOLE" class="ch.qos.logback.classic.AsyncAppender">
    <queueSize>5000</queueSize>
    <discardingThreshold>0</discardingThreshold>
    <neverBlock>true</neverBlock>
    <appender-ref ref="CONSOLE" />
</appender>

<appender name="ASYNC_SOCKET" class="ch.qos.logback.classic.AsyncAppender">
    <queueSize>5000</queueSize>
    <discardingThreshold>0</discardingThreshold>
    <neverBlock>true</neverBlock>
    <appender-ref ref="SOCKET" />
</appender>
  • queueSize: 비동기 큐의 크기를 지정
  • discardingThreshold: 큐가 가득 찼을 때 버릴 로그의 레벨 임계값
  • neverBlock: true로 설정하면 큐가 가득 찼을 때 로그 이벤트를 버리고, false로 설정하면 로그를 기록할 때까지 블로킹

4. 루트 로거 설정

<root level="${LOG_LEVEL}">
    <appender-ref ref="CONSOLE" />
    <appender-ref ref="SOCKET" />
</root>
  • level: ${LOG_LEVEL} 변수를 사용하여 루트 로거의 로그 레벨을 설정
  • appender-ref: CONSOLE과 SOCKET Appender를 모두 참조하여 로그 이벤트를 두 곳으로 출력

5. Spring 프로파일별 설정

<springProfile name="dev">
    <logger name="com.example" level="DEBUG" />
</springProfile>
  • dev 프로파일이 활성화되었을 때 적용됩니다.

logback.xml vs logback-spring.xml

logback.xml:
Logback 프레임워크에 의해 직접 로드된다.
Spring Boot의 외부 설정(예: application.properties의 값)을 참조할 수 없다.
Spring Boot의 프로파일(profile) 기능을 사용할 수 없다.

logback-spring.xml:
Spring Boot에 의해 처리되는 설정 파일로, Spring Boot가 먼저 로드하여 필요한 전처리를 수행한다.
Spring의 속성 플레이스홀더(예: ${})를 사용할 수 있으며, application.properties나 application.yml의 값을 참조할 수 있습니다.
Spring Boot의 프로파일 조건부 로딩 기능을 사용할 수 있습니다.

운영환경마다 다른 레벨의 로그를 찍으려면 logback-spring.xml 사용

왜 XML인가?

Logback 설정은 자바코드로도 가능하다.

        // LoggerContext 가져오기
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();

        // 기존 구성 지우기
        loggerContext.reset();

        // PatternLayoutEncoder 생성 및 설정
        PatternLayoutEncoder encoder = new PatternLayoutEncoder();
        encoder.setContext(loggerContext);
        encoder.setPattern("%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [userId=%X{userId}] - %msg%n");
        encoder.start();
        
        // 이런식

그런데 왜 xml을 쓸까? 예전에 스프링에서 빈 관리를 xml로 하다가 자바로 넘어갔는데. 왜 이건 자바 코드가 아니라 xml을 쓰는걸까?

1. 관심사의 분리

  • XML 설정 파일을 사용하면 애플리케이션 코드와 로깅 설정을 분리

2. 설정의 용이한 변경 및 배포

  • 실시간 변경 가능: XML 설정 파일은 애플리케이션을 재시작하지 않고도 변경할 수 있으며, Logback의 설정을 통해 변경 사항을 자동으로 감지

    • spring actuator를 이용해서 api 호출해서 변경 가능. 근데 현실적으로 이렇게 쓰나싶음.
  • 재컴파일 불필요: Java 코드로 설정을 변경하면 애플리케이션을 다시 컴파일하고 배포해야 하지만, XML 설정 파일은 이러한 과정 없이도 변경 가능

    • Logback은 설정 파일의 변경을 감지하기 위해 파일 시스템을 주기적으로 확인

Lazy Logging

로그 레벨을 info로 설정했다고 생각해보자.

logger.debug("Hello " + userName + ".");

이렇게 코드를 작성하면 INFO 레벨로 설정했기 때문에 로그 자체는 안남지만 문자열 연산 자체는 일어난다. 그래서 성능 저하가 발생한다. 이걸 막기 위해서는

    if(logger.isDebugEnabled()) {
        logger.debug("Hello " + userName + ".");
    }
        
        
        // 또는
	logger.debug("계산 결과: {}", () -> computeResult());

이렇게 작성해야 하지만 가독성이 좋지 않다.

logger.info("사용자 {}가 로그인했습니다.", userId);

이렇게 플레이스홀더{}를 사용하면 로그 레벨을 먼저 확인하고 로그를 찍게 추상화해놨다.

내부 구현

void info(String messagePattern, Object... args) {
    if (isInfoEnabled()) {
        FormattingTuple ft = MessageFormatter.arrayFormat(messagePattern, args);
        // 메시지 포맷팅 및 로그 출력
    }
}

참고로

플레이스홀더 인자가 3개 이상부터는 가변인자로 받게되어있음. 가변인자로 받으면 매번 새로운 배열을 할당하기 때문에 성능에 안좋을 수 있음. 그래서 성능에 민감하고 로그가 많이 찍힌다면 두개까지만 찍는게 좋다.

KotlinLogging

private logger = KotlinLogging.logger {}

logger.info{"test"} // 지연 로깅이 된다
logger.info("test") // 지연 로깅이 안된다

코틀린에서 로그를 남길때도 마찬가지로 람다를 이용하도록 {} 안에 로그를 남기는게 좋다.

0개의 댓글