로그 작성하는 방법이 Go 언어에서는 기존 Java, .NET과는 약간 다른 방식으로 사용됩니다. 그래서 로그 작성 라이브러 관련 정보를 찾다가 다음의 글들을 보게 되었습니다.

위 글을 읽고 로그 작성이라는 관점에서 많은 고민을 하게되었습니다. 위 글은 Go 관점으로 이야기를 하지만 다른 언어에서도 공통적인 고민 요소라고 생각되었습니다.

회사에서는 Kotlin & Java 로 현재 개발을 진행 중인데 프로젝트를 진행하며 로그에 대해서 깊게 생각하지 않았다는 생각이 들어서 다음과 같은 고민을 해보았습니다.

로그 왜 적을까?

먼저 지금까지 어떻게 로그를 사용하였고 무엇을 왜 적는가에 대한 고민입니다.

지금까지 작성한 로그

로그를 적는다는 것은 사실 프로그래밍을 하며 너무나 생각없이 이뤄지는 기계적인 행위가 많이 되고 있습니다. 특히 Java, Kotlin을 사용한다면 slf4j, logback, log4j 등을 이용해서 다음과 같은 로거 생성 구문을 기계적으로 클래스 최상단에 작성하는 경우가 많습니다.

// Kotlin
@Component
class WebRouter(
        private val userRouteHandler: UserRouteHandler,
        private val articleRouteHandler: ArticleRouteHandler
) {
    private val log = LoggerFactory.getLogger(this::class.java)

    ...
}

그리고는 생성한 로거(log)를 이용하여 다음과 같이 로그를 작성합니다.

log.error("Catch an unexpected exception", error)

위와 같은 구현에 설정에서 Appender를 연결하면 원하는 형태로 쉽게 로그를 작성할 수 있습니다. 그런데 여기서 큰 의문이 생깁니다.

“로그를 작성하는 요구사항”에 대한 고민입니다.

당연히 기획 시 특별한 요구사항이 있다면 로그 작성에 대한 명시적인 요구사항이 정의되겠지만 대부분의 경우 비지니스에만 취중되어 로그 작성은 개발자의 자율적인 부분이 됩니다. 하지만 개발 일정이 항상 부족한 개발자는 적당히 혹은 습관적으로 로그를 작성하게 됩니다. 여기서 발생하는 차이가 개발자의 능력이 될 수도 있지만 이를 최소화하고 더 좋은 시스템을 만들 때 필요한 고민들을 다음과 같이 정리해보았습니다.

로그 목적과 요구사항

기획에서 전달되는 요구사항이 아닌 개발자로서 제공해야되는 최소한의 로그의 목적은 다음과 같습니다.

  • 서비스 동작 상태 파악
  • 장애 파악 & 알람

위 목적으로 작성된 로그를 분석하면 서비스 지표의 확인, 트랙잭션, 성능 등 다양한 정보를 확인할 수 있습니다.

로그 어떻게 적을까?

로그의 목적을 이루기 위해서 로그를 작성해야 합니다. 그럼 어떻게 작성할지 고민해보겠습니다.

방법 1. stdout, stderr

로그를 작성하는 가장 쉬운 방법은 stdout으로 출력하는 것입니다. 어떤 언어를 배우더라도 처음 작성하는 구문은 Hello world!을 stdout으로 출력하는 방법을 배웁니다.

// 방법 1. stdout
System.out.println("Hello, World!")

12 Factor 에서는 로그를 이벤트 스트림으로 처리합니다. stdout, stderr로 출력하고 배포 환경에 따라 별도의 로그 저장소를 활용합니다. SaaS(Service as a Service) 환경에서 로컬에 저장되는 로그 파일은 초기화 될 수 있기 때문에 이러한 방법을 제안하고 있습니다.

방법 2. logging library

많은 개발자들이 알고 사용하는 방법입니다. 로깅 라이브러리(slf4j, logback, log4j, nlog 등)을 이용하여 로그를 작성합니다.

// 방법 2. logging library - slf4j & logback
@Component
class WebRouter(private val articleRouteHandler: ArticleRouteHandler) {
    private val logger = LoggerFactory.getLogger(this::class.java)

    @Bean
    fun router() = router {
        "/api".nest {
            "/articles".nest {
                POST("", articleRouteHandler::createArticle)
            }
        }
    }.filter { request, next ->
        logger.debug("Route ${request.methodName()} ${request.uri()}")

        next.handle(request)
                .switchIfEmpty { Mono.error(IllegalStateException("Response is empty")) }
                .onErrorResume { responseError(it) }
    }

    ...

이런 로깅 라이브러리의 강점은 로그 작성 인터페이스를 추상화 log.info(), error() 한다는 점과 로그 출력 방식에 대한 확장성입니다. Appender에 대한 설정만으로 다양한 로그 저장소(파일, Sentry, ElasticSearch 등)로 저장할 수 있는 것은 큰 장점입니다.

logging library를 이용한 로그 작성을 추천

logging library는 방법 1. stdout 까지 ConsoleAppender로 지원이 가능하고 확장성, 설정이 쉽기 때문에 대부분의 경우 훨씬 유용합니다.

  • 요구사항에 따른 Appender 개발 및 등록 가능
  • 다양한 Appender & Encoder 활용 가능
  • 타 로그 시스템/저장소와 통합의 편리성

사내 인프라, 상황에 따라 필요한 요구사항은 Appender를 직접 만들어 등록하면 되기 때문에 요구사항에 대한 대응도 유용합니다. 그리고 대부분의 라이브러리가 지원하는 클래스, 파일 라인, 인코딩 패턴, 컨텍스트 상태(MDC(Mapped Diagnostic Context 활용)를 활용한다면 로깅에 관련된 요구사항은 대부분 해결할 수 있습니다.

그리고 로깅 라이브러리는 대중적인 로그 시스템인 Sentry, ElasticSearch, Splunk 등과 연동하기 쉽도록 Appender, 플러그인, 로그 파서 등이 제공되기 때문에 통합이 편리합니다.

로그 무엇을 적을까?

이제는 로그에서 가장 “중요”한 무슨 로그를 작성할 것인가에 대한 고민입니다. 사실 앞에서 언급한 내용들은 많은 사람들이 이미 알고 잘 사용하고 있는 부분입니다. 하지만 어떤 로그를 언제 작성할 것인가는 많은 생각이 필요합니다.

각 서비스, 회사에 따라 다를 수 있지만 꼭 고민해야 할 요소들에 대한 생각입니다.

로그 레벨

먼저 로그 레벨에 대한 고민입니다.

레벨 설명
FATAL The FATAL level designates very severe error events that will presumably lead the application to abort.
ERROR The ERROR level designates error events that might still allow the application to continue running.
WARN The WARN level designates potentially harmful situations.
INFO The INFO level designates informational messages that highlight the progress of the application at coarse-grained level.
DEBUG The DEBUG Level designates fine-grained informational events that are most useful to debug an application.
TRACE The TRACE Level designates finer-grained informational events than the DEBUG

참고4 Apache log4j Level API doc

위의 설명은 Apache log4j의 로그 레벨에 관한 설명입니다. 하지만 실제로 로그 작성 시에 사용할 레벨로는 구분이 모호하고 그 수가 많습니다. 그리고 많은 개발자들이 프로젝트 초반/중반/후반 모두 명확한 레벨의 기준이 없다보니 점점 로그 레벨의 모호해지는 경우가 많습니다. 그래서 간단한 로그 작성 제안을 정리해보았습니다.

간단 로그 레벨(ERROR, INFO) 작성 제안

간략 정리 (tl;rd)

  • FATAL: X
  • ERROR: 의도하지 않은 오류 발생 (즉시 알림 필요 - 문자, 카카오톡, 텔레그램 등)
  • WARN: X
  • INFO: 서비스 동작 상태
  • DEBUG: 개발자 필요 (Dev 존에서만 사용)
  • TRACE: X

최소한으로 명확한 목적을 가지는 레벨은 ERROR, INFO 입니다. 로그 레벨을 잘 활용하기 어렵다면 차라리 ERROR, INFO로만 구분하여 간결하고 의도가 있는 로그를 작성합니다. 그 외의 로그 레벨은 선택적으로 활용 가능하지만 명확하게 도출이 되지 않는다면 ERROR, INFO만 사용하는 것을 추천합니다.

ERROR 의도하지 않은 오류를 명시적으로 표현

ERROR의 경우 종료까지 가지 않지만 의도하지 않은 경우, FATAL 레벨의 경우 어플리케이션을 종료 상태로 만들 수 있는 경우입니다. 하지만 이 두 레벨이 가지는 공통적인 목적은 ‘의도하지 않은’ 입니다. 그리고 이미 종료가 되는 시점의 FATAL 로그는 다양한 원인으로 작성되지 않을 확률이 다분합니다. 그래서 FATAL이 반드시 필요한 경우가 아니라면 의도하지 않은 오류는 모두 ERROR 적길 추천합니다.

예를 들어 사용자의 프로필을 반환하는 서비스에서 DB에 장애가 발생하여 ConnectionException 을 발생합니다. 이 경우에는 어떤 레벨로 로그를 작성해야할까요?

// UserService.kt (Kotlin)
try {
    val user = userRepository.findById(userId)
    user.modifyMobile(mobile)
    userRepository.save(user)
} catch (e: ConnectionException) {
    log.error("Fail to find a userDB is disconnected (userId: $userId)")
    throw InvalidUserServiceException(ErrorCode.DB_CONNECTION_ERROR, e)
}

위 예제는 의도하지 않은 ConnectionException 오류를 ERROR 레벨로 작성하여 개발, 운영자에게 빠르게 알람을 전달하고 InvalidUserServiceException 라는 오류로 변환하여 상위 계층에서 의도된 오류 처리를 수행하도록 구현하였습니다.

ERROR 의도하지 않은 모든 오류

  • ERROR 레벨의 로그는 예상/의도하지 않은 오류를 핸들링하는 시점에 사용
  • 로그 확인 시 ERROR 위주로 확인한다면 의도하지 않은 경우만 존재하기 때문에 빠르게 장애 원인 파악이 가능

INFO 시스템 동작을 표현

INFO 레벨은 ERROR에 비해 좀 더 모호합니다. 여러 가지 목적으로 사용하기 때문인데 그 중 가장 중요한 목적은 다음과 같습니다.

  1. 서비스 시나리오
  2. 요구사항

서비스 시나리오는 서비스의 목적을 달성을 성공적으로 하는지 분석 및 확인(지표 생성)하는 용도입니다. 그리고 요구사항은 예를 들어 외부 API 호출을 로그로 관리하여 대략적인 통계로 활용한다면 작성할 수 있는 로그입니다.

사실 이러한 경우는 정확하게 트랜잭션을 관리하며 DB에 기록하는 것이 더 좋다고 생각합니다. 어디까지나 중요하지 않는 대략적 통계라고 생각해주세요.

ERROR와 반대로 명확한 의도가 있는 로그들이 모두 INFO 레벨입니다.

예를 들어 서비스 동작에 관한 예로 사용자의 상태(등록, 휴면, 해지)를 얻는 API가 있습니다. 이 경우 사용자 서비스의 findById 만으로 구현을 한다면 다음과 같습니다.

// UserController.kt (Kotlin)
return try {
    val user = userService.findById(userId)
    log.info("User is ${user.status} status (userId: $userId)")
    return user.status
} catch (e: NonexistentUserException) {
    log.info("User is not exist (userId: $userId)")
    return UserStatus.NOT_REGISTERED
}

사용자가 존재하지 않기 경우 사용자 서비스에서는 NonexistentUserException 을 발생합니다. NonexistentUserException은 이미 findById 함수를 사용할 때부터 의도적으로 UserStatus.NOT_REGISTERED 상태를 반환하기 위해 사용합니다.

Exception이 발생하는 경우 무의식적으로 ERROR 레벨을 사용하는 경우가 있는데 시나리오 상 의도된 Exception이라면 ERROR 레벨로 작성할 이유가 전혀 없습니다. Exception을 오류라고 생각하면 그럴 수도 있지만 Exception은 ‘예외’이기 때문에 의도한 경우에는 INFO로 적는 것이 더 좋다고 생각합니다.

위 로그를 통해서 미가입 사용자가 몇 번이나 조회를 하였는지, 가입 중인 사용자라면 현재 상태가 어떤 값을 가졌는지 확인이 가능합니다.

INFO 서비스, 도메인의 동작, 요구사항을 표현

  • 서비스, 도메인의 시나리오 상태 (동작 확인 용도)
  • 서비스, 도메인의 지표 (지표 측정 용도)

그 외 로그 레벨

INFO, ERROR 로그 레벨은 철저하게 ‘의도’를 가졌나 여부로 구분을 하였습니다. 그 외 로그 레벨은 개발자의 필요에 따라 사용 가능하지만 다음의 요소들을 고민하여 사용하면 좋을 듯 합니다.

  • FATAL: 이미 너는 죽어 있다. 시스템 경우에 따라 작성된다면 결국 FATAL, ERROR, INFO 모두 확인해야하는데 필요할까?
  • WARN: 동작에는 문제 없지만 이상 발생 가능한 경우, 측정이나 생각이 가능할까? WARN일 때도 알람을 제공해야할까?
  • DEBUG/TRACE: 당신이 적는 로그가 의미가 있나요? TDD로 개발했다면 더욱 더 의미가 있을까요?

로그 작성 시 주의사항

  • 로그 파일/DB 생명 주기 & 저장소 용량
  • 개인정보
  • 시스템 주요 정보 (시스템 보안, 계정 정보)

로그의 생명 주기입니다. 로그가 저장되는 저장소의 용량, 파일 혹은 DB라도 삭제는 언제할 것인지? 이러한 계획을 명확하게 수립하고 운영해야 디스크 용량 부족과 같은 갑작스러운 장애를 방지할 수 있습니다.

그리고 몇 회사를 다녀보며 로그에 너무 쉽게 개인정보, 시스템 계정 정보가 작성되는 것을 많이 보았습니다. 이는 개인정보법을 위배할 수도 보안적 취약점을 가지기 때문에 꼭 주의할 필요가 있습니다. 로그 생명 주기는 DevOps, Infra 관리를 시스템적으로 하기 때문에 사전에 충분히 감지 및 대처할 수 있지만 민감정보가 로그에 작성되는 것은 철저히 개발자의 실수입니다.

로그 분석 & 활용을 돕는 도구

  • Sentry
  • ElasticSearch + Kibana
  • Splunk

회사마다 좋은 로그 분석 도구를 가지고 있겠지만 기본적으로 위 세가지 도구를 많이 활용합니다. 스타트업에서 처음으로 시작하는 경우 ElasticSearch + Kibana + X-Pack 으로도 로그 수집, 저장, 분석, 알림까지 지원이 가능하기 때문에 추천합니다. 충분한 인프라가 제공되는 회사의 경우에는 사내 제품을 적극적으로 활용하면 되겠습니다.


  1. 참고[1] Let’s talk about logging - Dave Cheney http://dave.cheney.net/2015/11/05/lets-talk-about-logging [return]
  2. 참고[2] What’s so bad about the stdlib’s log package? https://forum.golangbridge.org/t/whats-so-bad-about-the-stdlibs-log-package/1435/3 [return]
  3. 참고[3] 12 factor - Logs http://12factor.net/logs [return]
  4. 참고[4] Apache log4j Level API doc https://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/Level.html [return]