매일 업데이트
2023-07-04 10:04 15 min

자바 개발자를 위한 가이드

소프트웨어 개발 과정에서 핵심적인 요소 중 하나는 효율적인 로깅 시스템 구축입니다. Java 개발 환경에서는 다양한 로깅 프레임워크가 존재하지만, 사용 편의성과 더불어 뛰어난 성능, 확장성, 그리고 사용자 맞춤 기능을 제공하는 프레임워크를 선택하는 것이 중요합니다. Log4j2는 이러한 모든 조건을 만족시키는 무료 Java 로깅 라이브러리입니다.

Log4j2를 애플리케이션에 통합함으로써 고급 필터링, Java 8 람다 표현식 지원, 속성 조회, 그리고 사용자 지정 로그 수준 설정과 같은 다양한 기능을 활용할 수 있습니다. 이제 프로젝트에 Log4j2를 추가하는 방법과, 이를 통해 개발 효율성을 높이는 데 도움이 되는 여러 기능들을 자세히 알아보겠습니다.

Log4j2란 무엇인가?

로깅은 시스템 운영 과정에서 발생하는 다양한 정보들을 '로그'라는 형태로 기록하고, 이를 분석 및 참조하는 과정을 의미합니다. 이러한 로그는 애플리케이션 코드 디버깅에 매우 유용하게 활용되며, 코드의 흐름을 파악하고, 운영 환경에서의 문제나 오류를 해결하는 데 필수적인 역할을 합니다.

더 나아가, 로그는 감사 목적으로도 활용될 수 있습니다. 예를 들어, 알림 메시지가 사용자에게 성공적으로 전송되었는지 여부를 추적하는 데 사용될 수 있습니다.

Log4j2는 Java 로깅 라이브러리 중에서도 가장 널리 사용되는 것 중 하나이며, 이전 버전인 Log4j의 개선된 후속 버전입니다. Apache Software Foundation과 Apache Logging Services에서 개발한 이 라이브러리는 Apache 라이선스 2.0에 따라 배포되는 무료 오픈 소스 소프트웨어입니다.

Log4j2는 Log4j의 견고한 토대 위에 구축되었으며, System.out.println()과 같은 단순한 출력문 대신 로거를 사용하는 것이 훨씬 효과적입니다. 로거를 사용하면 로그 메시지를 세밀하게 제어할 수 있으며, 디버거를 사용할 수 없는 운영 환경에서는 적절한 로깅이 매우 중요합니다.

프로젝트에 Log4j2를 추가하는 방법

Java 프로젝트에 Log4j2를 추가하는 방법은 여러 가지가 있으며, Log4j2의 모든 기능을 온전히 활용하기 위해서는 Java 8 이상 버전을 사용하는 것이 권장됩니다.

이제 요구사항에 따라 Log4j2를 추가하는 다양한 방법을 살펴보겠습니다.

Apache Maven을 사용하는 프로젝트에 Log4j2 추가

Apache Maven을 빌드 시스템으로 사용하는 프로젝트에서는 pom.xml 파일에 Log4j2 의존성을 추가해야 합니다.

<dependencies>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-api</artifactId>
      <version>2.20.0</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
      <version>2.20.0</version>
    </dependency>
  </dependencies>

Log4j2는 각기 다른 아티팩트 간의 버전 관리를 쉽게 하기 위해 BOM(Bill of Material) pom.xml 파일을 제공합니다. 의존성 관리에 이 BOM을 추가하면, 각각의 버전을 일일이 지정할 필요가 없습니다.

<!-- BOM을 dependencyManagement에 추가 -->
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-bom</artifactId>
      <version>2.20.0</version>
      <scope>import</scope>
      <type>pom</type>
    </dependency>
  </dependencies>
</dependencyManagement>

<!-- BOM을 추가한 후에는 버전 정보가 필요 없음 -->
<dependencies>
  <dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
  </dependency>
  <dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
  </dependency>
</dependencies>

Apache Gradle을 사용하는 프로젝트에 Log4j2 추가

Apache Gradle을 빌드 도구로 사용하는 경우, build.gradle 파일에 Log4j2 의존성을 추가할 수 있습니다.

dependencies {
  implementation 'org.apache.logging.log4j:log4j-api:2.20.0'
  implementation 'org.apache.logging.log4j:log4j-core:2.20.0'
}

Gradle 버전 5.0 이상에서는 일관된 의존성 버전을 유지하기 위해 Log4j2 Maven BOM을 가져올 수 있습니다. build.gradle 파일에 다음 내용을 추가하면 됩니다.

dependencies {
  implementation platform('org.apache.logging.log4j:log4j-bom:2.20.0')

  implementation 'org.apache.logging.log4j:log4j-api'
  runtimeOnly 'org.apache.logging.log4j:log4j-core'
}

Gradle 버전 2.8부터 4.10까지는 Maven BOM을 직접 가져오는 기능이 없으므로, 의존성 관리 플러그인을 추가해야 합니다.

plugins {
  id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

dependencyManagement {
  imports {
    mavenBom 'org.apache.logging.log4j:log4j-bom:2.20.0'
  }
}

dependencies {
  implementation 'org.apache.logging.log4j:log4j-api'
  runtimeOnly 'org.apache.logging.log4j:log4j-core'
}

빌드 도구 없이 독립 실행형 애플리케이션에 Log4j2 추가

빌드 도구를 사용하지 않는 프로젝트의 경우, 공식 Log4j2 다운로드 페이지에서 필요한 Log4j2 아티팩트 버전을 다운로드할 수 있습니다.

다운로드 후에는 애플리케이션의 클래스 경로에 다음 JAR 파일들이 포함되어 있는지 확인해야 합니다.

  • log4j-api-2.20.0.jar
  • log4j-core-2.20.0.jar

Log4j2의 구성 요소

Log4j2의 기능을 제대로 이해하고 활용하기 위해서는 내부 구조를 파악하는 것이 중요합니다. Log4j2는 여러 구성 요소들로 이루어져 있으며, 각 구성 요소의 역할을 자세히 살펴보겠습니다.

#1. LoggerContext

LoggerContext는 로깅 시스템의 핵심 단위로, 애플리케이션에서 요청한 모든 로거들을 관리합니다. 또한, 로깅 설정에 대한 참조를 저장하는 역할도 합니다.

#2. 구성 (Configuration)

Configuration에는 로깅 시스템에 필요한 모든 설정 정보가 포함되어 있습니다. 여기에는 로거, 어펜더, 필터 등이 포함됩니다. Log4j2는 XML, JSON, YAML과 같은 다양한 파일 형식을 지원하며, Log4j2 API를 통해 프로그래밍 방식으로 구성을 정의할 수도 있습니다.

구성 파일의 변경 사항은 자동으로 감지되어 적용되기 때문에, 애플리케이션을 재시작할 필요가 없습니다.

#3. 로거 (Logger)

Logger는 Log4j2 시스템의 주요 구성 요소입니다. 애플리케이션 코드 내에서 LogManager.getLogger()를 사용하여 로거를 가져온 후, 로그 메시지를 생성하는 데 사용됩니다. 로그 메시지는 디버그, 정보, 경고, 오류, 치명적 등 다양한 심각도 수준으로 생성할 수 있습니다.

#4. LoggerConfig

LoggerConfig는 특정 로거의 동작을 정의합니다. 로깅 수준 설정, 어펜더 지정, 필터 적용 등 특정 로거에서 생성된 이벤트를 어떻게 처리할지를 결정합니다.

#5. 필터 (Filter)

Filter를 사용하면 Log4j2에서 특정 조건에 따라 로그 이벤트를 선택적으로 처리할 수 있습니다. 필터는 로거 또는 어펜더에 적용될 수 있으며, 특정 기준에 부합하는 로그 이벤트만 처리되도록 제어할 수 있습니다. 이를 통해 로깅 동작을 미세 조정하여 필요한 로그만 처리할 수 있습니다.

#6. 어펜더 (Appender)

Appender는 로그 메시지의 최종 목적지를 결정합니다. 로거는 여러 개의 어펜더를 가질 수 있으며, 로그 이벤트는 해당 로거에 지정된 모든 어펜더로 전송됩니다. Log4j2는 다양한 종류의 어펜더를 미리 제공하며, 예를 들어 ConsoleAppender는 메시지를 콘솔에 출력하고, FileAppender는 메시지를 파일에 출력합니다. 각 어펜더는 최종 로그 메시지의 형식을 정의하는 자체 레이아웃을 가지고 있습니다.

#7. 레이아웃 (Layout)

Layout은 Log4j2에서 최종 로그 메시지의 형식을 정의하는 데 사용됩니다. 레이아웃은 어펜더와 연결되며, 어펜더가 출력 대상을 결정하는 반면 레이아웃은 메시지를 어떻게 출력할지를 정의합니다.

Log4j2의 주요 기능 5가지

Log4j2는 다양한 기능을 제공하며, 이것이 다른 Java 로깅 프레임워크와 차별화되는 중요한 점입니다. 비동기 로거, Java 8 람다 표현식 지원 등 Log4j2는 여러 장점을 제공합니다. 이 프레임워크의 몇 가지 주목할 만한 기능들을 살펴보겠습니다.

#1. 플러그인을 사용한 기능 확장

Log4j 1.x 버전에서는 기능을 확장하기 위해 코드 수정이 많이 필요했지만, Log4j2는 플러그인 시스템을 도입하여 이러한 문제를 해결했습니다.

클래스에 @Plugin 어노테이션을 사용하여 새로운 플러그인을 선언할 수 있습니다. 플러그인을 통해 필터, 어펜더와 같은 독자적인 구성 요소를 만들거나, 타사 구성 요소를 라이브러리에 쉽게 추가할 수 있습니다.

#2. Java 8 람다 표현식 지원

Log4j2 버전 2.4부터 Java 8 람다 표현식에 대한 지원이 도입되었습니다. 람다 표현식을 사용하면 로깅 로직을 인라인으로 정의할 수 있어 여러 줄의 검사나 익명 내부 클래스 사용을 줄일 수 있습니다. 또한, 비용이 많이 드는 메서드가 불필요하게 실행되지 않도록 할 수 있어, 코드 가독성을 높이고 시스템 오버헤드를 줄이는 데 도움이 됩니다.

디버그 레벨이 활성화되었을 때만 비용이 많이 드는 작업의 결과를 로깅하는 예시를 보겠습니다. 람다를 지원하기 전에는 다음과 같이 코드를 작성해야 했습니다.

if (logger.isDebugEnabled()) {
      logger.debug("The output of the given operation is: {}", expensiveOperation());
  }

이러한 사용 사례가 많아지면 불필요한 조건부 검사가 증가합니다. 하지만 Log4j2에서는 동일한 작업을 다음과 같이 수행할 수 있습니다.

logger.debug("The output of the given operation is: {}", () -> expensiveOperation()

expensiveOperation() 메서드는 디버그 레벨이 활성화된 경우에만 실행되므로, 명시적인 확인이 필요 없습니다.

#3. 비동기 로거

모든 로깅 이벤트는 I/O 작업이므로 시스템 오버헤드를 증가시킬 수 있습니다. 이를 완화하기 위해 Log4j2는 애플리케이션 스레드와 분리된 별도의 스레드에서 실행되는 비동기 로거를 도입했습니다. 비동기 로거를 사용하면, 호출 스레드는 logger.log() 메서드 호출 후 즉시 제어권을 다시 얻을 수 있습니다.

이러한 비동기 방식은 로깅 이벤트가 완료될 때까지 기다리지 않고 애플리케이션 로직을 계속 실행할 수 있도록 해줍니다. 이로 인해 더 높은 로깅 처리량을 얻을 수 있으며, 모든 로거를 비동기 방식으로 만들거나, 동기 및 비동기 방식을 혼합하여 사용할 수 있습니다.

#4. 가비지 없는 로깅

Java의 가비지 컬렉션은 애플리케이션에서 사용하지 않는 객체를 자동으로 정리하는 프로세스입니다. 이 작업은 수동으로 처리할 필요는 없지만, 가비지 컬렉션 자체에도 오버헤드가 발생합니다.

만약 애플리케이션이 단시간에 너무 많은 객체를 생성하면, 가비지 컬렉션 프로세스가 시스템 리소스를 많이 소모하게 될 수 있습니다. 이전 버전의 Log4j를 포함한 일부 로깅 라이브러리들은 로깅 과정에서 많은 임시 객체를 생성하여 가비지 컬렉션에 대한 부담을 증가시켜 시스템 성능에 부정적인 영향을 줄 수 있습니다.

Log4j2는 버전 2.6부터 "가비지 없는" 모드로 작동합니다. 이것이 기본 동작이며, 객체를 재사용하여 임시 객체 생성을 크게 줄였습니다.

다음 이미지는 Log4j2 버전 2.6이 Log4j2 버전 2.5에 비해 불필요한 객체 생성을 어떻게 줄이는지 보여줍니다.

Log4j2 버전 2.5에서는 로깅 과정 중 많은 임시 객체가 생성됩니다. 출처: apache.org

Log4j2 버전 2.6에서는 로깅 과정 중 생성되는 임시 객체가 없습니다. 출처: apache.org

#5. 조회 (Lookups)

Log4j2에서 조회 기능을 사용하면 로그에 컨텍스트 정보를 추가할 수 있습니다. 이를 통해 시스템 속성, 환경 변수, 사용자 정의 값 등 다양한 소스의 데이터를 로그에 포함시킬 수 있어, 로그를 더욱 유용하게 활용할 수 있습니다.

예를 들어, 모든 로그 라인에 사용자의 세션 ID를 기록하고 싶다고 가정해 보겠습니다. 이렇게 하면 세션 ID에 해당하는 모든 로그를 쉽게 검색할 수 있습니다.

세션 ID를 명시적으로 추가하는 것은 유지보수가 어렵고, 값 추가를 잊어버릴 경우 중요한 정보를 잃을 수 있습니다.

logger.info("The user data has been fetched for session id {}", sessionId);
  ...
  logger.info("The transaction has been processed for session id {}", sessionId);
  ...
  logger.info("Request has been successfully processed for session id {}", sessionId);

컨텍스트 맵 조회를 사용하면 더 효율적으로 처리할 수 있습니다. 세션 ID를 애플리케이션 코드의 스레드 컨텍스트에 추가하고, Log4j2 설정 내에서 해당 값을 사용할 수 있습니다. 따라서 로그 메시지에서 세션 ID를 명시적으로 언급할 필요가 없어집니다.

ThreadContext.put("sessionId", sessionId);

값을 추가한 후에는 키워드 ctx를 사용하여 조회를 통해 해당 값을 사용할 수 있습니다.

<File name="Application" fileName="application.log">
    <PatternLayout>
      <pattern>%d %p %c{1.} [%t] $${ctx:sessionId} %m%n</pattern>
    </PatternLayout>
  </File>

Log4j2에서 사용자 지정 로그 수준 생성 방법

Log4j2의 로그 수준은 심각도 또는 중요도에 따라 로그 이벤트를 분류하는 데 사용됩니다. 애플리케이션 코드에서 메시지를 기록할 때 로그 수준을 제어할 수 있습니다.

예를 들어, logger.debug()는 DEBUG 수준을 추가하고, logger.error()는 ERROR 수준을 추가합니다. 최종 출력에 표시되는 메시지는 구성 파일에서 설정한 로그 수준에 따라 결정됩니다.

Log4j2의 미리 설정된 로그 수준과 해당 값은 다음과 같습니다.

OFF(0) < FATAL(100) < ERROR(200) < WARN(300) < INFO(400) < DEBUG(500) < TRACE(600) < ALL(MAX)

로그 수준을 특정 수준으로 설정하면, 해당 수준과 그 이상의 수준(값이 더 작은)의 로그 라인이 출력됩니다. 나머지 로그는 무시됩니다.

예를 들어, 로그 수준을 WARN으로 설정하면 WARN, ERROR, FATAL 메시지가 표시됩니다. 다른 수준의 모든 로그 라인은 무시됩니다. 이는 다른 환경에서 동일한 코드를 실행할 때 유용합니다.

개발 환경에서는 로그 수준을 INFO 또는 DEBUG로 설정하여 더 많은 로그를 확인할 수 있습니다. 운영 환경에서는 ERROR로 설정하여 문제 발생 시 중요한 로그만 확인하는 것이 좋습니다.

Log4j2에서는 미리 설정된 로그 수준 외에도 자신만의 사용자 정의 로그 수준을 추가할 수 있습니다. 사용자 정의 로그 수준을 추가하고 사용하는 방법을 살펴보겠습니다.

#1. 구성 파일을 사용하여 사용자 정의 로그 수준 추가

구성 파일에서 사용자 정의 로그 수준을 선언하여 추가할 수 있습니다.

다음 예제에서는 NOTICE라는 사용자 정의 로그 수준을 값 450으로 정의했습니다. 이는 INFO(값 400)와 DEBUG(값 500) 사이에 위치하게 됩니다. 즉, 로그 수준을 NOTICE로 설정하면 INFO 메시지는 기록되지만 DEBUG 메시지는 무시됩니다.

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
  <CustomLevels>
    <CustomLevel name="NOTICE" intLevel="450" />
  </CustomLevels>

  <Appenders>
    <File name="MyFile" fileName="logs/app.log">
      <PatternLayout pattern="%d %-7level %logger{36} - %msg%n"/>
    </File>
  </Appenders>
  <Loggers>
    <Root level="trace">
      <AppenderRef ref="MyFile" level="NOTICE" />
    </Root>
  </Loggers>
</Configuration>

#2. 코드에서 사용자 정의 로그 수준 추가

구성 파일에서 선언하는 것 외에도, 코드에서 직접 사용자 정의 로그 수준을 정의할 수 있습니다.

final Level VERBOSE = Level.forName("VERBOSE", 550);

이 코드는 VERBOSE라는 새로운 로그 수준을 생성합니다. 이 로그 수준은 DEBUG(값 500)와 TRACE(값 600) 사이에 위치합니다. 로거를 VERBOSE 수준으로 설정하면, DEBUG를 포함하여 VERBOSE 이상의 모든 로그 메시지가 기록되지만, TRACE 메시지는 무시됩니다.

#3. 코드에서 사용자 정의 로그 수준 사용

사용자 정의 로그 수준을 사용하기 전에, 먼저 구성 파일이나 코드에서 선언해야 합니다. 선언 후에는 자유롭게 사용할 수 있습니다.

다음 코드는 NOTICE라는 사용자 정의 수준을 선언하고 사용하는 방법을 보여줍니다.

final Level NOTICE = Level.forName("NOTICE", 550);

final Logger logger = LogManager.getLogger();
logger.log(NOTICE, "a notice level message");

이 방법은 새로운 수준으로 메시지를 생성하지만, 매번 수준을 명시적으로 전달하는 것이 번거로울 수 있습니다. Log4j2는 확장된 로거를 만드는 데 도움이 되는 유틸리티를 제공합니다. 이 유틸리티를 사용하면, logger.debug() 또는 logger.error()와 유사하게 logger.notice()와 같은 사용자 정의 메서드를 사용할 수 있습니다.

다음 명령은 CustomLogger.java라는 Java 파일을 생성합니다. 이 파일에는 NOTICE 수준에 대한 메서드와 기존 로그 메서드가 포함되어 있습니다.

java -cp log4j-core-2.20.0.jar org.apache.logging.log4j.core.tools.ExtendedLoggerGenerator 
      com.example.CustomLogger NOTICE=450 > com/example/CustomLogger.java

파일이 생성되면, 코드에서 이 클래스를 사용하여 새로운 로거를 만들 수 있습니다. 이 로거는 사용자 정의 로그 수준에 대한 메서드를 포함하므로, 로거의 기능을 확장할 수 있습니다.

final Logger logger = CustomLogger.create(ValueFirstSmsSender.class);

  //이 새로운 메서드는 logger.debug()를 사용하는 것과 유사함
  logger.notice("a notice level message");

결론

Log4j2는 다양한 기능, 유연한 구성, 뛰어난 성능 개선 등을 제공하는 매우 강력한 Java 로깅 프레임워크입니다. 로그는 소프트웨어 개발 과정에서 매우 중요한 부분이므로, Log4j2와 같은 강력한 프레임워크를 사용하면 애플리케이션의 기능을 향상시킬 수 있습니다.

Log4j2의 유연성과 확장성을 통해 애플리케이션에서 발생하는 이벤트를 적절하게 캡처할 수 있으며, 로그를 디버깅 및 감사에 유용한 도구로 활용할 수 있습니다. 이러한 모든 기능과 개선 사항으로 인해 Log4j2는 다양한 소프트웨어 프로젝트에서 선호되는 선택이 되었습니다.

이러한 Java IDE 및 온라인 컴파일러에도 관심이 있을 수 있습니다.

저자
Korea

기술 트렌드와 실용적인 팁을 전하는 लेखक입니다.