Go

Golang에서 로그를 출력하는 팁

드리프트2 2024. 6. 1. 17:21

 

Golang으로 프로그램을 작성할 때 로그는 어떻게 출력하시나요?

 

이번 글에서는 로그를 출력할 때의 팁과 주의할 점, 그리고 그 이유에 대해 설명해 드리겠습니다.

 

로그 출력 방법은 라이브러리에서 로그를 출력하는 경우와 애플리케이션에서 로그를 출력하는 경우에 따라 상당히 다릅니다.

TL;DR

라이브러리(패키지)를 작성할 때…

  • 먼저 로그를 출력하지 않는 것을 고려해 보세요. error로 반환하여 라이브러리 사용자가 로그를 출력할지 여부를 선택하게 하세요.
  • *log.Logger를 사용하세요. 기본값은 log.Printf로 출력하고, 라이브러리 사용자가 원하는 *log.Logger로 변경할 수 있게 하세요.
  • 자체 Logger 인터페이스를 정의하세요. 로거 구조체가 아닌 인터페이스로 정의하세요. 기본 구현은 *log.Logger를 출력 대상으로 설정하면 좋습니다.
  • 기존의 로깅 라이브러리를 사용하는 것은 절대 피하세요. 라이브러리 사용자는 특정 로깅 라이브러리를 강제당하는 것을 매우 싫어합니다.

애플리케이션을 작성할 때…

  • *log.Logger를 사용하세요.
  • 기존의 로깅 라이브러리 중 필요한 것을 선택하세요.
  • 직접 필요한 로거를 만들어 보세요!

라이브러리의 로깅

당신이 출력하는 그 로그, 정말로 필요한가요?

 

라이브러리의 작성자가 안심하기 위해서만 로그를 출력하고 있지는 않나요?

 

미래에 발생할지 모르는 문제 해결을 위한 로그를 출력하고 있지는 않나요?

 

대부분의 라이브러리 사용자는 로그가 출력되어도 그 의미를 이해하지 못할 가능성이 큽니다.

 

당연히 어떤 액션을 취해야 할지도 모릅니다. 즉, 그 로그는 단순히 출력만 되면 무의미할 가능성이 높습니다.

 

사용자가 액션을 취해야 하는 로그라면 차라리 error로 반환하세요.

 

그러면 라이브러리 사용자가 로그로 출력할지, 실제 에러로 처리할지 선택할 수 있습니다.

 

미래에 발생할지 모르는 문제를 대비하려면, 그런 문제가 발생하지 않도록 설계를 다시 검토하세요.

라이브러리에서 *log.Logger를 사용하는 경우

라이브러리에서 *log.Logger를 사용하는 샘플 코드를 보여드리겠습니다.

*log.Logger 변수를 공개하여 사용자가 설정할 수 있게 합니다.

 

라이브러리 내부에서는 logf() 함수를 통해 로그를 출력합니다.

package myawesome

import "log"

var Logger *log.Logger

func logf(format string, v ...interface{}) {
    if Logger == nil {
        log.Printf(format, v...)
        return
    }
    Logger.Printf(format, v...)
}

 

애플리케이션 쪽에서 로거를 교체하고 싶다면, 다음 코드와 같이 init() 등 프로그램 실행 초기 단계에서 교체합니다.

package main

import (
    "log"
    "os"
    "github.com/koron/myawesome"
)

func init() {
    myawesome.Logger = log.New(os.Stdout, "myapp", log.LstdFlags)
}

 

로거 교체 시 동기화가 필요하다면, 로거 변경을 함수화하고 sync.Mutex 등을 사용하여 동기화하면 좋습니다.

 

또한 로그 함수 내에서 if 문이 신경 쓰인다면, 기본 로거를 라이브러리 측에서 제공해도 좋습니다.

 

아래 샘플 코드는 두 가지를 모두 구현한 예제입니다.

package myawesome

import (
    "log"
    "os"
    "sync"
)

var (
    logger = log.New(os.Stderr, "", log.LstdFlags)
    logMu  sync.Mutex
)

func SetLogger(l *log.Logger) {
    if l == nil {
        l = log.New(os.Stderr, "", log.LstdFlags)
    }
    logMu.Lock()
    logger = l
    logMu.Unlock()
}

func logf(format string, v ...interface{}) {
    logMu.Lock()
    logger.Printf(format, v...)
    logMu.Unlock()
}

 

이렇게 하면 라이브러리 사용자는 자신이 원하는 로그 출력 대상을 선택할 수 있게 됩니다.

라이브러리에서 자체 Logger 인터페이스를 정의하는 경우

레벨별로 로그를 출력해야 하는 특별한 요구가 있는 경우, 라이브러리에서 자체 Logger 인터페이스를 정의하세요.

 

각 메서드의 시그니처는 일반적인 로깅 라이브러리에 맞추는 것이 좋습니다.

 

추가로 기본 구현으로 *log.Logger에 출력하는 것을 제공하면, golang의 표준 로그 시스템에 맞춰 쉽게 사용할 수 있습니다.

 

 

다음 샘플 코드는 라이브러리가 warning과 debug 레벨의 로그를 출력하는 것을 가정한 예제입니다.

 

기본 구현에서는 warning만 *log.Logger로 출력하고 있습니다. 앞서 설명한 동기화는 하지 않았습니다.

 

라이브러리 코드는 로그를 출력할 때 logger.Warnf()logger.Debugf()를 직접 호출하는 것을 가정합니다.

package myawesome

import (
    "log"
    "os"
)

type Logger interface {
    Warnf(string, ...interface{})
    Debugf(string, ...interface{})
}

var logger Logger = &BasicLogger{
    Logger: log.New(os.Stderr, "", log.LstdFlags),
}

func SetLogger(l Logger) {
    logger = l
}

type BasicLogger struct {
    Logger *log.Logger
}

func (bl *BasicLogger) Warnf(format string, v ...interface{}) {
    bl.Logger.Printf(format, v...)
}

func (bl *BasicLogger) Debugf(format string, v ...interface{}) {
    // suppress debug logs
}

 

이러한 Logger 인터페이스가 정의되어 있다면, 라이브러리 사용자는 애플리케이션 요구에 따라 *log.Logger를 사용하거나 다른 로깅 라이브러리를 사용할 수 있으며, 적절한 래퍼를 쉽게 작성할 수 있습니다.

 

다음 코드는 그러한 래퍼의 예제입니다.

package main

import "github.com/koron/myawesome"

func init() {
    myawesome.SetLogger(&appLogger{})
}

type appLogger struct {}

func (l *appLogger) Warnf(format string, v ...interface{}) {
    // TODO: wrap your favorite logger.
}

func (l *appLogger) Debugf(format string, v ...interface{}) {
    // TODO: wrap your favorite logger.
}

기존의 로깅 라이브러리는 절대 No Good

라이브러리를 작성할 때 기존의 로깅 라이브러리를 사용하는 것은 절대 피하세요.

 

사용자는 당신의 라이브러리 기능을 사용하고 싶어하지, 당신이 선택한 로깅 라이브러리를 강제당하고 싶어하지 않습니다.

애플리케이션의 로깅

라이브러리의 로깅에 비해 애플리케이션의 로깅 전략은 더 단순합니다.

 

먼저 *log.Logger로 충분한지 검토해 보세요.

 

로그 로테이션 등은 OS의 표준 기능을 사용하는 것이 가장 범용적입니다.

 

로그의 출력 대상을 컨테이너 등에서 실행할 때를 고려하면, 표준 출력이나 에러 출력으로 설정하는 것이 가장 일반적입니다.

 

이러한 점들을 고려하면 *log.Logger로 충분한 경우가 많습니다.

 

하지만 *log.Logger는 포맷 선택이나 로그 레벨 제어 측면에서 부족할 때가 많습니다.

 

그런 경우에는 유명한 로깅 라이브러리를 적극적으로 사용하세요.

 

어떤 로깅 라이브러리가 좋은지는 각 애플리케이션의 요구 사항에 따라 다르므로, 여기서는 다루지 않겠습니다.

 

최근에는 DevOps 등으로 인해 애플리케이션의 로그 요구 사항이 점진적으로 변화하는 경우가 많습니다.

 

요구 사항이 변화하면서 기존 로깅 라이브러리가 기능과 비용 측면에서 맞지 않게 될 수도 있습니다.

 

그런 경우에는 직접 로거를 작성하는 것도 고려해 볼 수 있습니다. $GOROOT/log/log.go의 코드를 보세요.

 

400줄도 되지 않습니다. 이를 복사하여 필요한 코드를 추가하고 불필요한 부분을 제거하면, 당신이 필요로 하는 최적의 로거가 탄생할 것입니다.

글을 마치며

이 글에서 다룬 로그에 대한 팁들은 업무 중에 서버를 작성하면서 여러 라이브러리의 로그를 통합하고 운영에 필요한 로그로 정리하는 과정에서 겪은 어려움에서 비롯되었습니다.

 

크게 두 가지 문제가 있었는데, 라이브러리가 사용하는 로깅 라이브러리가 임의로 인수를 추가하는 것과, 로그가 출력되어도 어떻게 처리해야 할지 몰라 곤란했던 점입니다.

 

이런 문제들이 앞으로 조금이라도 줄어들기를 바라는 마음으로 이 글을 작성했습니다.