본문 바로가기

Development/Java

[JAVA] 효과적인 Logging 14가지 가이드

728x90

실제 운영 상태에서 디버깅, 모니터링 및 사고 대응은 필수적인 요소입니다.

로그로 사용하는 라이브러리는 SLF4J, log4j, log4j2, logback 등 다양하게 있습니다.

Spring에서는 기본으로 logback을 쓰는것으로 알고 있습니다.

 

모범적인 사례들을 따르면 SLF4J와 Logback을 활용하여 애플리케이션 관리 및 사고 해결을 위한 전략적으로 사용할 수 있습니다.

이러한 가이드라인을 사용할경우 문제 해결 속도가 빨라지고, 시스템 동작에 대한 더 깊은 이해가 이루지며 애플리케이션 안정성과 성능을 위한 견고한 기반이 구축될 것입니다.

 

이러한 효과적인 로깅의 주는 이점으로는 다음과 같습니다.

  • 관측성  향상: 로그는 애플리케이션 동작에 대한 자세한 기록을 제공하므로 시스템 작동 방식을 더 쉽게 이해하고 잠쟂거인 문제를 식별 가능합니다.
  • 더 빠른 문제 해결: 잘 구성되고 정보가 풍부한 로그를 통해 개발자는 문제의 근본 원인을 신속하게 파악하고 효율적으로 해결할 수 있습니다.
  • 향상된 인시던트 응답: 로그는 인시던트 응답 중에 매우 유용하며, 문제가 발생하거나 발생하는 동안 발생하는 이벤트에 대한 시간 순으로 설명할 수 있습니다.
  • 규정 준수및 보안: 로그는 규정 준수의 증거가 될 수 있으며 보안 위반이나 의심스러운 활동을 식별하는 데 도움이 됩니다.

SLF4J와 Logback 선택

SLF4J(Java의 단순 로깅 Facade)는 다양한 로깅 프레임워크에서 로깅을 위한 일관된 API를 제공하는 로깅 Facade입니다. Logback은 다양한 기능과 널리 사용되고 있는 로깅 프레임워크입니다. SLF4J를 Logback과 결합하면 유연성과 성능을 활용할 수 있습니다.

SLF4J 및 Logback을 활용하여 효과적으로 사용하기 위한 14가지 사례를 다루고 있습니다. 운영을 보다 안정적이고 유지 관리가 가능하며 유용한 정보를 제공하는 로깅을 만드는데 도움일 될 것입니다.

1. SLF4J를 로깅 Facade로 사용

좋은 예)
기본 로깅 라이브러리 구현에서 로깅 아키텍처를 분리하려면 애플리케이션의 로깅 Facade로 SLF4J를 선택합니다. 이 추상화를 사용하면 주요 코드 변경 없이 다양한 로깅 프레임워크(logback, log4j, log4j2 등)을 전환할 수 있습니다.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyClass {
    private static final Logger logger = LoggerFactory.getLogger(MyClass.class);
    // ...
}

 

나쁜 예)

애플리케이션 코드에서 특정 로깅 프레임워크 구현을 하드코딩으로 구현할 경우 로깅 프레임워크를 변경할 시 코드를 변경해야 하는 부분에서 비용이 추가됩니다.

import org.apache.log4j.Logger;

public class MyClass {
    private static final Logger logger = Logger.getLogger(MyClass.class);
    // ...
}

 

좋은 예)의 경우 SLF4J의 Facade 아키텍처로 인하여 로깅 프레임워크를 변경하더라도 동일한 코드를 유지할 수 있어 로깅 프레임워크 변경 시 나쁜 예)보다 비용을 아낄 수 있는 수 있습니다.

2. 효율적인 로깅을 위한 로그백 구성

좋은 예)

향상된 성능과 유연성을 위해 Logback 구성을 외부화하고 PatternLayout을 사용합니다. 각 환경별(개발, 스테이징, 프로덕션 등) 다양한 구성을 정의하여 로그의 자세한 내용과 세부 사항을 관리하기에 용이 합니다.

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="debug">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

 

나쁜 예)

오래되었거나 성능이 떨어지는 레이아웃 클래스와 코드의 하드코딩 구성 설정을 사용하면 다양한 환경에 적응하기 어려울 수 있습니다.

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <!-- 권장되지 않는 레이아웃 구성 -->
        </layout>
    </appender>
    <!-- ... -->
</configuration>

3. 적절한 로그 레벨 사용

좋은 예)

메시지의 중요성과 의도를 전달하기 위해 올바른 레벨로 기록합니다. 일반적인 이벤트에는 INFO를 개발 중 자세한 정보에는 DEBUG, 주의가 필요한 심각한 문제에 관해서는 ERROR를 사용합니다.

logger.info("Application has started.");
logger.debug("The value of X is {}", x);
logger.error("Unable to process the request.", e);

 

나쁜 예)

동일한 수준에서 모든 것을 로깅하며 로그 파일에 잡음이 너무 많아 주요한 문제를 파악하기 어려울 수 있습니다.

logger.error("Application has started."); // 로그 수준의 잘못된 사용
logger.error("The value of X is " + x); // 비효율적인 문자열 연결
// ...

 

각 환경별 레벨 설정이 가능하여 환경별로 출력되는 로그를 분리하는 데도 적절한 로그 레벨을 사용함으로 개발에는 상세한 정보를 운영에는 필요한 정보만을 구분하여 출력할 수 있습니다.

4. 의미가 있는 메시지를 기록

좋은 예)

컨텍스트를 제공하기 위해 로그 메시지에 트랜잭션 또는 상관 관계 ID와 같은 관련 정보를 포함합니다. 이는 서비스 전반에 걸쳐 요청을 추적하는 분산 시스템에서 특히 유용합니다.

 

logger.info("Order {} has been processed successfully.", orderId);

 

나쁜 예)

이벤트나 문제를 이해하는데 충분한 컨텍스트를 제공하지 않는 모호하거나 일반적인 로그 메시지는 되도록 사용하지 않는 것이 좋습니다.

logger.info("Processed successfully."); // 제공된 컨텍스트가 없음

5. 동적 콘텐츠에 자리 표시자 사용

좋은 예)

자리 표시자를 활용하여 로그 수준이 비활성화되면 불필요한 문자열 연결을 방지하여 메모리와 CPU 주기(cycle)를 절약합니다.

logger.debug("User {} logged in at {}", username, LocalDateTime.now());

 

나쁜 예)

로그 문 내에서 문자열을 연결하는 것은 비효율적입니다.

logger.debug("User " + username + " logged in at " + LocalDateTime.now());

6. 스택 추적으로 예외 기록

좋은 예)

문제 진단을 위한 최대 컨텍스트를 제공하려면 항상 스택 추적을 포함한 전체 예외를 기록하십시오.

try {
   // 예외를 던지는 코드
} catch (Exception e) {
   logger.error("예기치 않은 오류가 발생했습니다", e);
}

 

나쁜 예)

스택 추적 없이 예외 메시지만 로깅하면 중요한 진단 정보가 누락될 수 있습니다.

try {
    // 예외를 던지는 코드
} catch (Exception e) {
    logger.error("예기치 않은 오류가 발생했습니다: " + e.getMessage());
}

7. 성능을 위해 비동기 로깅 사용

좋은 예)

로깅 작업을 별도의 스레드로 오프로드하여 애플리케이션 성능을 향상시키는 비동기 로깅을 구현합니다.

<configuration>
    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="FILE" />
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>application.log</file>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="ASYNC" />
    </root>
</configuration>

 

나쁜 예)

로그 관련 대기 시간의 가능성을 고려하지 않고 성능이 중요한 경로에 동기식 로깅을 수행합니다.

logger.info("A time-sensitive operation has completed.");

8. 적절한 세분성으로 로그인

좋은 예)

로깅이 너무 많은 것과 너무 적은 것 사이에서 균형을 유지해야 합니다. 애플리케이션으 ㅣ특정 요구 사항을 기반으로 적절한 세분성으로 기록합니다. 로그를 복잡하게 만들고 중요한 정보를 식별하기 어렵게 만드는 과도한 로깅을 피해야 합니다.

public void processOrder(Order order) {

    logger.info("Processing order: {}", order.getId());

    // 디버깅 목적으로 보다 세분화된 로깅
    logger.debug("Order details: {}", order);

    // order 처리
    orderService.save(order);

    logger.info("Order processed successfully");
}

 

나쁜 예)

프로덕션에서 높은 세분성으로 과도한 로깅은 성능 문제 및 로그 범람으로 이어질 수 있습니다.

public void processOrder(Order order) {

    logger.trace("Entering processOrder method");
    logger.debug("Received order: {}", order);
    logger.info("Processing order: {}", order.getId());

    // 주문 처리의 모든 단계 기록
    logger.debug("Step 1: Validating order");
    // ...
    logger.debug("Step 2: Calculating total amount");
    // ...
    logger.debug("Step 3: Updating inventory");
    // ...

    logger.info("Order processed successfully");
    logger.trace("Exiting processOrder method");
}

9. 로그 파일 모니터링 및 회전

좋은 예)

로그가 과도한 디스크 공간을 소비하지 않도록 크기 또는 시간을 기준으로 로그 파일 순환을 구성합니다. 용량에 가까워지면 경고를 트리거하도록 로그 파일에 대한 모니터링을 설정합니다.

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>logs/myapp-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
        <maxHistory>30</maxHistory>
        <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
            <maxFileSize>100MB</maxFileSize>
        </timeBasedFileNamingAndTriggeringPolicy>
    </rollingPolicy>
    <!-- ... -->
</appender>

 

나쁜 예)

로그 파일이 무한정 커지도록 두면 디스크 공간이 고갈되고 잠재적인 시스템 오류가 발생할 수 있습니다.

10. 민감한 정보의 보안

좋은 예)

중요한 데이터가 로그에 기록되기 전에 수정하거나 해시하려면 로깅 프레임워크에 필터 또는 사용자 지정 변환기를 구현합니다.

log.info("Processing payment with card: {}", maskCreditCard(creditCardNumber));

public String maskCreditCard(String creditCardNumber) {
    int length = creditCardNumber.length();
    if (length < 4) return "Invalid number";
    return "****-****-****-" + creditCardNumber.substring(length - 4);
}

 

나쁜 예)

비밀번호, API 키, 신용 카드 또는 개인 식별 정보(PII)와 같은 민감한 정보를 로깅합니다.

log.info("Processing payment with card: {}", creditCardNumber);

11. 구조화된 로깅

좋은 예)

JSON과 같이 기계가 읽을 수 있는 형식으로 로그를 출력하는 구조화된 로깅을 채택하여 로그 관리 시스템에서 더 나은 검색 및 인덱싱을 용이하게 합니다.

<configuration>
    <appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp>
                    <timeZone>UTC</timeZone>
                </timestamp>
                <version />
                <logLevel />
                <threadName />
                <loggerName />
                <message />
                <context />
                <stackTrace />
            </providers>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="JSON_CONSOLE" />
    </root>
</configuration>

 

JSON 형식으로 인쇄되는 로그 메시지의 예를 살펴보겠습니다.

logger.info("Order has been processed");

 

위 로그 메시지의 출력은 다음과 같이 인쇄됩니다.

{"@timestamp":"2024-03-26T15:52:00.789Z","@version":"1","message":"Order has been processed","logger_name":"Application","thread_name":"main","level":"INFO"}

 

나쁜 예)
프로그래밍 방식으로 구문 분석하고 분석하기 어려운 구조화되지 않은 로그 형식을 사용합니다.

12. 모니터링 도구와의 통합

좋은 예)

로깅을 모니터링 및 경고 도구와 연결하여 자동으로 이상 현상을 감지하고 관련 팀에 알립니다.

 

나쁜 예)

로그와 모니터링 시스템의 통합을 무시하면 문제 감지가 지연될 수 있습니다.

13. 로그 집계

좋은 예)

분산 환경에서는 중앙 집중식 로그 집계를 사용하여 여러 서비스에서 로그를 수집하고 이벤트 분석 및 상관 관계를 단순화합니다.

 

나쁜 예)

로그가 다양한 시스템에 분산되어 있으면 문제 해결 프로세스가 복잡해집니다.

14. 스마트 로깅

여기에는 AOP를 사용하여 스마트 로깅을 구현하는 데 대한 훌륭한 콘텐츠가 있습니다.
https://logback.qos.ch/manual/appenders.html

 

Chapter 4: Appenders

There is so much to tell about the Western country in that day that it is hard to know where to start. One thing sets off a hundred others. The problem is to decide which one to tell first. —JOHN STEINBECK, East of Eden Chapter 4: Appenders What is an Ap

logback.qos.ch

마무리

효과적인 로깅은 단순히 데이터를 캡쳐하는 것이 아니라 적절한 시기에 적절한 형식으로 적절한 데이터를 캡처하는 것입니다. 이러한 좋은 예에서 보여준것 같이 구현함으로써 개발자와운영 팀은 애플리케이션 관리 및 사고 해결을 위한 전략적 리소스로 전화이 가능합니다.

이러한 지침을 채택함으로 관찰성이 향상디ㅗ고, 문제 해결이 빨라지며 시스템 동작에 대한 이해도가 높아지고, 애플리케이션의 안정성과 선능을 위한 견고한 기반이 마련될 것입니다.

 

 

 

 

애플리케이션을 구축함에 있어 서비스에만 집중하다 보면 로깅에 대한 부분을 소홀이 할 때가 많습니다. 구축당시 잘 되었던 것들도 운영을 하면서 문제점을 찾을 때 많은 도움을 줄 수 있는 로그는 잘 만들면 재현 없이 문제를 찾을 수 있지만 그렇지 못한 애플리케이션은 동일한 사고(에러)를 찾기 수많은 시도를 해야만 할 때가 있습니다.

또한 분산 시스템에 대한 로깅은 경험이 많지 않을 경우 놓치는 부분이 생각보다 많을 수 있습니다.

이런 14가지 효과적인 로깅 방법을 숙지하고 몸에 익힌다면 소홀해질 수 밖에 없는 상황에서 조금이라도 트래킹에 있어 도움이 될 것이라 생각해 봅니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

참고: https://foojay.io/today/effective-java-logging/

728x90
반응형