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
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#L44ConsoleAppender에서는 OutputStream을 쓰고 있는 것을 볼 수 있다
결론은 System.ou
t은 PrintStream
을 쓰고 대부분의 메서드가 synchronized로 선언되어 있어 스레드 안전하지만 성능 저하가 발생할 수 있다. OutputStreamWriter: 자체적으로 동기화를 제공하지 않습니다. 필요한 경우 외부에서 동기화를 구현해야 한다.
또 ConsoleAppender는 로깅을 비동기적으로 처리할 수 있어서 성능차이가 나는듯.
둘다 지원하는 기능은 거의 같다.
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번 터져서 신뢰하기 어렵다라는 생각도 있다.
Logger: 애플리케이션 코드에서 로그 메시지를 생성하는 객체
Appender: Logger로부터 전달받은 로그 이벤트를 실제 출력 대상으로 내보내는 역할
Layout: 로그 메시지의 형식을 정의합니다.
Filter: 로그 이벤트를 필터링하여 특정 조건에 부합하는 로그만을 처리
LoggerContext: Logger와 Appender 등의 구성요소를 관리하는 환경. 설정 파일의 변경을 감지하여 동적으로 재구성할 수 있음
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>
<springProperty scope="context" name="LOG_LEVEL" source="logging.level.root" defaultValue="INFO" />
<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>
<root level="${LOG_LEVEL}">
<appender-ref ref="CONSOLE" />
<appender-ref ref="SOCKET" />
</root>
<springProfile name="dev">
<logger name="com.example" level="DEBUG" />
</springProfile>
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 사용
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을 쓰는걸까?
실시간 변경 가능: XML 설정 파일은 애플리케이션을 재시작하지 않고도 변경할 수 있으며, Logback의 설정을 통해 변경 사항을 자동으로 감지
재컴파일 불필요: Java 코드로 설정을 변경하면 애플리케이션을 다시 컴파일하고 배포해야 하지만, XML 설정 파일은 이러한 과정 없이도 변경 가능
로그 레벨을 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개 이상부터는 가변인자로 받게되어있음. 가변인자로 받으면 매번 새로운 배열을 할당하기 때문에 성능에 안좋을 수 있음. 그래서 성능에 민감하고 로그가 많이 찍힌다면 두개까지만 찍는게 좋다.
private logger = KotlinLogging.logger {}
logger.info{"test"} // 지연 로깅이 된다
logger.info("test") // 지연 로깅이 안된다
코틀린에서 로그를 남길때도 마찬가지로 람다를 이용하도록 {}
안에 로그를 남기는게 좋다.