Kotlin 답게 간단하게 로깅해보기

2024. 4. 30. 12:21스프링 (Spring)/스프링 팁 (Spring Tip)


slf4j

 

스프링에서 간단하게 로깅할 필요가 있었는데

찾아보니 코틀린 진영에서는 kotlin-logging, klogging이 꽤나 유명한가보군요

 

GitHub - oshai/kotlin-logging: Lightweight Multiplatform logging framework for Kotlin. A convenient and performant logging facad

Lightweight Multiplatform logging framework for Kotlin. A convenient and performant logging facade. - oshai/kotlin-logging

github.com

 

GitHub - klogging/klogging: Kotlin logging library with structured logging and coroutines support

Kotlin logging library with structured logging and coroutines support - klogging/klogging

github.com

 

뭐... 당연히 써도 됩니다.

다만 저는 의존성의 저주를 한 번 겪어봤기 때문에,

굳이 외부 라이브러리가 아닌 스프링에 내장된 걸로 사용하고 싶었었네요.

 

스프링에 내장된 로깅 라이브러리로는 slf4j가 있었습니다.

 

SLF4J

Simple Logging Facade for Java (SLF4J) The Simple Logging Facade for Java (SLF4J) serves as a simple facade or abstraction for various logging frameworks (e.g. java.util.logging, logback, log4j) allowing the end user to plug in the desired logging framewor

www.slf4j.org

 

여기서 말하는 퍼사드(facade)란 복잡한 여러 과정을 하나로 묶어

정말 간단하게 사용할 수 있게끔 만들어준 것이라고 생각하면 됩니다.

 

정말 간단하긴 하더라고요

val logger: Logger = LoggerFactory.getLogger("hello")
logger.debug("world!")

 

 

여담으로 스프링 공식 문서를 보면 로깅에 크게 관심이 없는 것 같아 보였네요.

Spring Boot has no mandatory logging dependency, except for the Commons Logging API, which is typically provided by Spring Framework’s spring-jcl module. To use Logback, you need to include it and spring-jcl on the classpath.

 

 

 

 


Wrap Logger

 

시작 전, kotlin-logging 에서 유의미한 정보를 찾을 수 있었습니다.

 

스프링에서 로그 레벨을 지정해도, 무시하고 자원을 쓰나보네요.

해당 로직까지 포함해서 간단하게 만들어보죠.

2024. 05. 02 : 테스트 중 가장 높은 Level이 ERROR임을 확인해서, 사실 error()는 로그 레벨을 확인하는 로직이 필요 없어 제거했습니다.
class SimpleLogger(private val logger: Logger) {

    fun debug(message: String) {
        if (logger.isDebugEnabled) {
            logger.debug(message)
        }
    }

    fun debug(callMessage: () -> Any?) {
        if (logger.isDebugEnabled) {
            logger.debug(callMessage().toString())
        }
    }

    fun info(message: String) {
        if (logger.isInfoEnabled) {
            logger.info(message)
        }
    }

    fun info(callMessage: () -> Any?) {
        if (logger.isInfoEnabled) {
            logger.info(callMessage().toString())
        }
    }

    fun warn(message: String) {
        if (logger.isWarnEnabled) {
            logger.warn(message)
        }
    }

    fun warn(callMessage: () -> Any?) {
        if (logger.isWarnEnabled) {
            logger.warn(callMessage().toString())
        }
    }

    fun error(message: String) {
        logger.error(message)
    }

    fun error(callMessage: () -> Any?) {
        logger.error(callMessage().toString())
    }

}

 

companion object에서 정의한 로그를 사용하기 위해서 abstract class 도 만들어줍니다.

abstract class Logging {

    val log: SimpleLogger = SimpleLogger(logger = LoggerFactory.getLogger(javaClass.enclosingClass))

}

 

log라는 이름은 입맛에 따라 바꿔도 됩니다.

 

 

그리고 혹시 Logger도 매번 생성하는 게 아닐까 걱정되서,

getLogger를 조금 파보면 LogerContext까지 도달하게 되는데

public class LoggerContext extends ContextBase implements ILoggerFactory, LifeCycle { 
	
    private Map<String, Logger> loggerCache = new ConcurrentHashMap();
    
    ...

    public Logger getLogger(String name) {
        if (name == null) {
            throw new IllegalArgumentException("name argument cannot be null");
        } else if ("ROOT".equalsIgnoreCase(name)) {
            return this.root;
        } else {
            int i = 0;
            Logger logger = this.root;
            Logger childLogger = (Logger)this.loggerCache.get(name);
            
        ...
        
    }
    
    ...
    
}

 

이름 기준으로 캐싱해두고 있었네요. 다행입니다 ㅎ

 

 

 

 

 

 


Logging

 

테스트 코드 몇 개 뚝딱 작성해서 실행해보면

class SimpleLoggerTest {

    companion object : Logging()

    @Nested
    @TestMethodOrder(OrderAnnotation::class)
    inner class BlockLoggingTest {

        @Test
        @Order(10)
        fun givenMessageBlock_whenDebug_thenLogged() {
            // given & when & then
            log.debug { "hello debug block!" }
        }

        @Test
        @Order(20)
        fun givenMessageBlock_whenInfo_thenLogged() {
            // given & when & then
            log.info { "hello info block!" }
        }

        @Test
        @Order(30)
        fun givenMessageBlock_whenWarn_thenLogged() {
            // given & when & then
            log.warn { "hello warn block!" }
        }

        @Test
        @Order(40)
        fun givenMessageBlock_whenError_thenLogged() {
            // given & when & then
            log.error { "hello error block!" }
        }

    }

    @Nested
    inner class StringLoggingTest {

        @Test
        @Order(10)
        fun givenMessage_whenDebug_thenLogged() {
            // given & when & then
            log.debug("hello debug message.")
        }

        @Test
        @Order(20)
        fun givenMessage_whenInfo_thenLogged() {
            // given & when & then
            log.info("hello info message.")
        }

        @Test
        @Order(30)
        fun givenMessage_whenWarn_thenLogged() {
            // given & when & then
            log.warn("hello warn message.")
        }

        @Test
        @Order(40)
        fun givenMessage_whenError_thenLogged() {
            // given & when & then
            log.error("hello error message.")
        }

    }

}

 

 

멋지게 실행되네요!

 

테스트 코드에서 볼 수 있듯 아래처럼 정적 변수로 만들어주면 해당 클래스에서 편리하게 사용할 수 있습니다

companion object : Logging()

 

 

 

 

 

 

 


Bonus : logback.xml

 

테스트할 때만 로그 레벨을 낮추고 싶다면 logback.xml을 만들어주면 됩니다.

참고 : https://docs.spring.io/spring-boot/how-to/logging.html#howto.logging.logback

 

Logging :: Spring Boot

Spring Boot has no mandatory logging dependency, except for the Commons Logging API, which is typically provided by Spring Framework’s spring-jcl module. To use Logback, you need to include it and spring-jcl on the classpath. The recommended way to do th

docs.spring.io

 

저는 테스트에서는 DEBUG로 낮추고 싶었기에 test 디렉터리 바로 아래에 resources/logback.xml 파일을 만들고

 

 

가이드에 나온대로 간단하게 아래처럼 작성하고 root level을 원하는 만큼 낮추면 됩니다.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
    <root level="DEBUG">
        <appender-ref ref="CONSOLE" />
    </root>
    <logger name="org.springframework.web" level="DEBUG"/>
</configuration>