Chamy's Blog

Kotlin 로그 남기기 본문

Kotlin

Kotlin 로그 남기기

Chamy619 2025. 10. 21. 16:58

실행중인 애플리케이션의 상태를 추적하거나, 에러가 발생했을 때 원인을 파악하기 위해서 가장 먼저 하는 일이 로그를 확인하는 일이다. 개발할 때 뿐 아니라 운영시 발생하는 이슈를 대응하기 위해서 로그를 남기고 읽는 것이 중요하다.

 

일반적으로 로그를 남기기 위해서 프레임워크를 사용하는데, Java 진영에서 주로 Logback, Log4j 등을 이용해 로그를 남긴다.

 

Kotlin 역시 Java 진영의 로그 프레임워크를 그대로 사용할 수 있는데, SLF4J(Simple Logging Facade for Javva)라는 로그 프레임워크 추상화 인터페이스를 사용해 구현체에 상관 없이 SLF4J API를 호출하는 방식으로 로그를 작성할 수 있다.


SLF4J를 통한 로깅

class MyApplication {
  private val logger = LoggerFactory.getLogger(MyApplication::class.java)

  fun sayHello() {
    logger.info("call sayHello function")
    // 로직
  }
}

 

SLF4J는 Logger 인터페이스를 제공하고 SLF4J를 사용하는 클라이언트는 인터페이스의 구현체가 무엇인지 알 필요 없이 SLF4J 인터페이스를 사용하여 로그를 남길 수 있다.

SLF4J의 Logger 인터페이스에는 info, isWarnEnabled 등 다양한 메서드가 정의되어 있다.
코드 예시의 LoggerFactory 또한 SLF4J에서 제공하는 클래스다.


Kotlin Logging을 통한 로깅

private val logger = KotlinLogging.logger {}

class MyApplication {
  fun sayHello() {
    logger.info { "call sayHello function" }
  }
}

 

Kotlin Logging 역시 SLF4J와 마찬가지로 info, isWarnEnabled 등의 멤버 함수가 정의된 KLogger 인터페이스를 제공한다.

SLF4J를 사용한 로깅과 차이점은 logger를 선언하는 부분과 로그를 작성할 때 ()를 사용하는지 {}를 사용하는지 차이인 것 처럼 보인다.


Kotlin Logging의 기능

일단 앞에서 살펴본 두 가지 차이점을 하나씩 자세히 살펴보자.

  • logger 선언부
  • () 대신 {} 사용

 

logger 선언

 

private val logger = KotlinLogging.logger {} 에서 logger 구현을 찾아가보면 아래처럼 작성되어 있다.

public object KotlinLogging {
  public fun logger(func: () -> Unit): KLogger = logger(KLoggerNameResolver.name(func))  
}

 

logger 멤버 함수는 KLogger를 반환하는데, KLoggerNameResolver를 통해 패키지에 존재하는 클래스의 이름으로 KLogger를 초기화한다.

자세한 내용은 KotlinLogging의 KLoggerNameResolver 객체 참고

 

따라서 SLF4J에서 로거를 가져올 때 MyApplication::class.java 를 매개변수로 넣어줬던 것을 자동화해준다.

 

() 대신 {} 사용

 

Java나 다른 언어에서 사용하는 것과 차이가 많이 나는 부분인데 이 부분이 Kotlin Logging의 핵심 영역이다.

 

일단 () 대신 {} 를 사용한다는 뜻은 람다를 사용한다는 의미이고, 실제로 KLogger의 info를 보면 매개변수로 람다를 받고 있는 것을 확인할 수 있다.

public interface KLogger {
  public fun info(message: () -> Any?): Unit =
    at(Level.INFO) { this.message = message.toStringSafe() }

  public fun at(level: Level, marker: Marker? = null, block: KLoggingEventBuilder.() -> Unit)
}

 

따라서 Kotlin Logging에서 info { “message” }를 호출하게 됐을 때 at 인터페이스의 구현체가 호출되고, 여러 구현체가 있지만 SLF4J의 at 구현체를 보면 로그 레벨이 유효할 경우에만 람다가 실행되는 것을 확인할 수 있다.

internal class LocationAwareKLogger(override val underlyingLogger: LocationAwareLogger) : KLogger, DelegatingKLogger<LocationAwareLogger>, Slf4jLogger<LocationAwareLogger>() {
  override fun at(level: Level, marker: Marker?, block: KLoggingEventBuilder.() -> Unit) {
    if (isLoggingEnabledFor(level, marker)) {
      KLoggingEventBuilder().apply(block).run {
        if (payload != null) {
          logWithPayload(this, level, marker)
        } else {
          logWithoutPayload(this, level, marker)
        }
      }
    }
  }
}

 

이렇게 람다를 통해 로그를 남기면, 호출 시점에 람다가 실행되기 때문에 불필요한 연산을 막을 수 있다는 장점이 있다. at의 구현체를 보면, 로그가 현재 남길 수 있는 상황에서야 로그를 남기는 람다가 실행된다. 지금까지의 예제에서 살펴본 일반 문자열 로깅에서는 이것이 주는 이점이 크지 않지만, 특정 연산의 결과를 DEBUG 레벨로 로그를 남긴다고 했을 때, 운영 환경에서는 로그를 남기지 않는데도 연산을 하게 되는 성능상의 단점이 존재한다.

 

아래 코드처럼 SLF4J만을 사용해 로그를 남기게 된다면, run 함수가 실행되었을 때 로그 레벨이 DEBUG 보다 높을 경우에도 calculate 함수가 실행된다.

// SLF4J를 통한 로그 작성
class MyApplication {
  private val logger = LoggerFactory.getLogger(MyApplication::class.java)

  fun run() {
    logger.debug("연산 결과: ${calculate()}")
    // 로직
  }

  fun calculate(): Int {
    // 복잡하고 자원을 많이 소모하는 로직
  }
}

 

하지만 Kotlin Logging를 사용해 로그를 남기게 되면, calculate는 DEBUG 로그를 작성하게 될 때 수행된다.

// Kotlin Logging을 통한 로그 작성 
private val logger = KotlinLogging.logger {}  

class MyApplication {   
    fun run() {     
        logger.debug { "연산 결과: ${calculate()}" }     
        // 로직   
    }    

    fun calculate(): Int {     
        // 복잡하고 자원을 많이 소모하는 로직   
    } 
}

 

결국 호출할 때 연산을 수행할지, 실제 로그를 남길 때 연산을 수행할지의 차이인데, DEBUG 레벨에서는 둘 모두 거의 비슷한 성능을 보이겠지만, 로그 레벨이 더 높을 경우 연산을 수행하지 않는 Kotlin Logging이 성능적으로 우수하다고 할 수 있다.

 

Kotlin Logging에서도 SLF4J 방식처럼 람다 대신 문자열 그대로 넘기는 로깅도 인터페이스를 제공하고 있다. 그러나 이런 형태는 Kotlin Logging의 장점을 활용하는 것이 아니기 때문에 Deprecated 어노테이션이 붙어있고, 람다를 사용한 방식을 사용하도록 가이드하고 있다.


결론

결론을 말하면, Kotlin Logging이 SLF4J 보다 Kotlin String Template을 사용할 때 더 좋은 성능을 보여준다. Kotlin String Template을 사용할 때만이라는 전제 조건이 붙은 이유는 SLF4J에서도 로그 레벨을 먼저 체크한 뒤 로그를 남기는 방식인 Parameterized Logging을 제공하고 있는데, SLF4J의 Parameterized Logging을 사용하는 것이 Kotlin Logging을 사용하는 것보다 더 좋은 성능을 발휘한다.

// SLF4J Parameterized Logging
public abstract class AbstractLogger implements Logger, Serializable {
    public void info(String format, Object arg) {
        if (isInfoEnabled()) {
            handle_1ArgsCall(Level.INFO, null, format, arg);
        }
    }
}

 

지금까지 Kotlin Logging의 장점만 이야기하다가 결론에 와서 다른 이야기를 하는 것 같지만, 프레임워크를 선택할 때 완벽한 기준은 존재하지 않는다. 성능만을 봤을 때는 SLF4J의 Parameterized Logging을 사용하는 것이 가장 유리해보이지만, 이 방법은 가독성이 그리 좋지는 않다(사용 예시: logger.debug(“연산 결과: {}”, this.calculate())).

 

적어도 지금까지 완벽한 로그 프레임워크는 존재하지 않지만 우리는 Kotlin 진영에서 꽤 좋은 성능과 가독성을 챙길 수 있는 Kotlin Logging을 사용하기로 했다. 로그 프레임워크를 비교하는 많은 사람들에게 조그마한 도움이 되었으면 좋겠다.