로그(Log)란?
로그(Log) 남기기
어플리케이션을 운영할 때 작동 정보인 로그(Log)를 기록하는 행위를 해주어야 합니다. 로그를 기록하면 어플리케이션의 상태를 추적하고, 오류 인지 및 잠재적인 문제를 진단할 수 있습니다. 즉 로깅을 통해 로직의 흐름을 파악함으로써 서비스의 품질을 관리할 수 있기 때문에 로깅은 개발자들에게 필수적입니다.
그러나 로그를 무분별하게 기록하면 로그 파일의 볼륨이 너무 커져 문제를 야기할 수 있습니다. 따라서 예외가 발생하는 곳이나, 중요 기능이 실행되는 부분에 적절한 로깅을 남겨 효율적으로 처리하는 것이 중요합니다.
로그 레벨
로그 레벨은 로그 메시지의 중요도를 나타냅니다. 로그 레벨에는 총 6가지의 레벨이 있습니다. 로깅 레벨을 올바르게 선택하면 적절한 정보만 로그로 남기고 로그 파일의 볼륨을 관리할 수 있습니다.
- TRACE : 가장 세분화된 정보로 개발 단계에서 사용합니다.
- DEBUG : 디버깅할 정도로 세분화된 정보를 제공합니다. SQL 로깅을 할 수 있으며 역시 개발 단계에서 사용합니다.
- INFO : 어플리케이션 운영 상황 정보를 제공합니다.
- WARN : 프로그램 실행에는 문제가 없지만 잠재적인 오류가 있다는 경고 상태입니다.
- ERROR : 요청 처리 중 오류가 발생한 상태입니다.
- FATAL : 아주 심각한 오류가 발생한 상태입니다.
로그는 선택한 레벨 부터 높은 레벨의 로그까지 찍힙니다.
위 코드에서 낮은 레벨인 TRACE부터 높은 레벨인 ERROR까지 로그를 남겨보려 했을 때, 위 결과와 같이 INFO부터 ERROR까지만 로그가 찍힌 것을 확인할 수 있습니다. 그 이유는 기본 로그 레벨이 INFO로 설정 되어있기 때문입니다.
로그 레벨을 변경하는 것도 가능한데, 이는 application.properties(yml) 파일에서 변경이 가능합니다.
아래와 같이 root로 전체 로깅 레벨을 설정할 수 있고, 패키지 별로 로그 레벨 설정이 가능합니다. 로그 레벨의 우선 순위는 안 쪽에 있을수록 높습니다.
아래는 com.dutmdcjf.test.controller 내에 있는 Controller들의 로그 레벨을 trace로 설정한 예제입니다. TestController의 로그 레벨이 trace가 됨을 확인할 수 있습니다.
logging:
level:
root: info
com.dutmdcjf.test: debug
com.dutmdcjf.test.controller: trace
로그 남기는 방법
Spring Boot에서 spring-boot-starter-logging은 spring-boot-starter-web안에 포함되어 있기 때문에 다른 의존을 추가하지 않고 사용할 수 있습니다. 로그를 사용하는 법은 두 가지 방식이 있는데 두 방식은 위 예제에서 사용했습니다 :)
1) LoggerFactory
LoggerFactory에서 가져오는 방식입니다. 해당 코드에서는 static을 선언하지 않았지만, 실제로 사용할 때는 static을 사용해주어야합니다. Logger 인스턴스를 여러 번 만들 필요가 없습니다. 때문에 Logge를 static으로 선언함으로써 맨 처음 클래스 로딩 시 한 번만 생성되고, 메모리 사용량과 객체 생성 비용이 절감됩니다.
2) @Slf4j
롬복(Lombok)에서 제공하는 어노테이션 중 하나로, 이 어노테이션을 사용하면 Logger 객체를 간편하게 생성할 수 있습니다.
log4j2
log4j vs Logback vs log4j2
log4j, logback, log4j2는 SLF4J의 구현체들입니다. 이 로그 메시지는 로그백을 통해 처리됩니다.
즉, @Slf4j를 통해 Logger 객체를 간편하게 생성하고, 이렇게 생성된 Logger 객체는 SLF4J의 인터페이스를 구현한 로깅 시스템(Logback, log4j2)을 사용하여 로그를 기록합니다.
그럼 log4j와 logback, log4j2 중 무엇을 사용해야할까요?
일단 나온 순서는 다음과 같습니다.
Apache log4j는 초기에 개발된 Java 로깅 프레임워크 중 하나입니다. Java에서 가장 많이 사용되던 로깅 라이브러리였지만, 후속 버전으로 Logback이 나오면서 더 이상 사용되지 않게되었습니다.
logback은 log4j보다 더 많은 appender 및 필터를 제공하게 되어 사람들은 Logback을 많이 사용하게 되었고 log4j는 2015년에 더 이상 개발되지 않게되었습니다.
그 후로 log4j의 후속이자 최신 버전으로 log4j2가 나오게 되었습니다. 이는 비동기 로깅, Loom 백그라운드 로깅과 같은 현대적인 기능을 지원하고 기존 log4j 및 logback 구성을 쉽게 이전할 수 있는 기능을 제공합니다.
logback vs log4j2
log4j2는 logback보다 뛰어난 로깅 성능을 제공합니다. 특히 비동기 로깅과 Loom 백그라운드 로깅을 통해 처리량을 향상시키기 때문에, log4j2는 Multi Thread 환경에서 비동기 로거의 경우 다른 로깅 프레임워크보다 뛰어난 성능을 보입니다.
log4j2 의존성 추가
Spring Boot는 `spring-boot-starter-web`을 추가하면 기본적으로 `spring-boot-starter-logging`이 포함되어 있어, logback을 사용하도록 되어있습니다.
log4j2를 사용하기 위해선 `spring-boot-start-web`안에 포함되어 있는 `spring-boot-starter-logging`을 제외해주어야 logback 라이브러리를 제외할 수 있습니다.
configurations {
configureEach {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
}
`spring-boot-starter-logging`을 제외해주었다면, log4j2를 추가해주어야 합니다.
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
log4j2 설정 파일 읽는 순서
스프링 부트에서 로깅 관련 설정을 읽어 들이는 순서는 다음과 같습니다.
1.resources 디렉터리 아래에 log4j2.xml 파일이 있으면 해당 파일의 설정을 읽습니다.
2. log4j2.xml 파일이 없는 경우 .properties(.yml) 파일을 읽습니다.
3. 위 두 파일이 모두 있는 경우 log4j2.xml을 적용시키고, .properties(.yml) 파일 설정을 덮습니다.
💡log4j2.xml과 yml 설정 파일
Logback 설정 파일을 생성할 때 log4j2.xml과 yml파일 중 무엇을 사용하는 것이 좋을까요?
1. XML 파일:
Logback에서 기본적으로 사용되는 설정 형식입니다. XML 파일을 통해 다양한 설정을 유연하고 구체적인 설정을 할 수 있습니다. XML 파일은 문법이 복잡하고 가독성이 떨어진다는 단점이 있습니다.
2. YAML 파일:
YAML 파일은 XML에 비해 문법이 간결하고 가독성이 좋아 설정 파일을 작성하기 편리합니다. 그러나 XML 파일보다는 설정 옵션이나 구성이 제한적일 수 있다는 단점이 있습니다.
너무 복잡하지 않은 간단한 설정인 경우 YAML 파일을 써도 아무런 문제가 없습니다. 더 세세한 커스텀들을 하고 싶다면 XML파일로 작성하는 것이 유리합니다.
아래 설명들은 log4j2에서 기본적으로 사용되는 설정 방식인 XML에 대해서 설명하고, YAML 파일 형식으로 설정하는 코드는 가볍게만 작성하도록 하겠습니다.
log4j2.xml 파일 작성
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO" scan="true" scanPeriod="1 hour">
<Properties>
<Property name="DIR_PATH">logs</Property>
<Property name="LOG_PATTERN">[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] %-5level${PID:-} [%15.15thread] %logger{36} - %msg%n</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}"/>
</Console>
<RollingFile name="RollingFile" fileName="${DIR_PATH}/mylog.log" filePattern="logs/mylog-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
</Policies>
<DefaultRolloverStrategy>
<Delete basePath="${DIR_PATH}">
<IfAccumulatedFileCount exceeds="30"/>
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
</Appenders>
<Loggers>
<Logger name="com.dutmdcjf.test" level="info" />
<Logger name="com.dutmdcjf.test.controller" level="debug" />
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Root>
</Loggers>
</Configuration>
Appender: 로그 출력 방식 지정
Appender는 로그 이벤트가 어디로 출력되는지를 정의합니다. 로그 이벤트는 콘솔, 파일, 데이터베이스 등 다양한 대상으로 출력될 수 있습니다.
기존 log4j와 Logback과의 다른점은 Appender의 종류를 class의 속성값(예: ConsoleAppender)으로 구분하였지만, log4j2에서는 태그(예: )로 구분합니다.
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO" scan="true" scanPeriod="1 hour">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] %-5level${PID:-} [%15.15thread] %logger{36} - %msg%n"/>
</Console>
<File name="File" fileName="logs/mylog.log">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] %-5level${PID:-} [%15.15thread] %logger{36} - %msg%n"/>
</File>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>
<Appenders> 태그 내에 Appender들을 작성해줍니다. 그리고 <Loggers> 태그 내에 로그 이벤트를 수신하고 처리할 로거들을 정의해줍니다. Logger에 대한 설명은 아래 자세히 하겠습니다.
Appender 선언
<Console>: 콘솔에 출력
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] %-5level${PID:-} [%15.15thread] %logger{36} - %msg%n"/>
</Console>
<File>: 파일에 출력
<File name="File" fileName="logs/mylog.log">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] %-5level${PID:-} [%15.15thread] %logger{36} - %msg%n"/>
</File>
<JDBC>: RDB Table에 출력
MyConnectionFactory.java 클래스를 작성하여, DB 연결을 해주어야합니다.
<JDBC name="db" tableName="db_log">
<ConnectionFactory class="com.dutmdcjf.MyConnectionFactory" method="getDatabaseConnection" />
<Column name="EVENT_ID" literal="LOGGING.APPLICATION_LOG_SEQUENCE.NEXTVAL" />
<Column name="EVENT_DATE" isEventTimestamp="true" />
<Column name="LEVEL" pattern="%level" />
<Column name="LOGGER" pattern="%logger" />
<Column name="MESSAGE" pattern="%message" />
<Column name="THROWABLE" pattern="%ex{full}" />
</JDBC>
<Socket>: 원격 호스트 및 포트로 로그를 전송
<Socket name="Socket" host="localhost" port="4560" protocol="TCP">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] %-5level${PID:-} [%15.15thread] %logger{36} - %msg%n"/>
</Socket>
<Async>: 비동기적으로 다른 Appender로 전달
<Async name="Async">
<AppenderRef ref="Console"/>
</Async>
<RollingFile>: 크기 또는 시간 기반으로 파일을 롤링하여 출력
<RollingFile name="RollingFile" fileName="${DIR_PATH}/mylog.log" filePattern="logs/mylog-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
</Policies>
<DefaultRolloverStrategy>
<Delete basePath="${DIR_PATH}">
<IfAccumulatedFileCount exceeds="30"/>
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
- filePattern:
Rolling시 저장될 파일 이름입니다.
yyyy-MM-dd, yyyy-MM-dd-hh, yyyy-MM-dd-hh-mm 이런식으로 설정해주게 된다면 각각 일마다, 시간마다, 분마다 파일명이 정해져 저장됩니다. - TimeBasedTriggeringPolicy:
파일이 저장될 시간 간격을 정합니다. interval="1"은 기본값입니다. 만약 yyyy-MM-dd로 파일명을 지정해준다면 해당 파일은 1일 간격 로그를 모아 저장할 것입니다. yyyy-MM-dd-hh-mm으로 파일명을 지정하게 되면 파일은 1분 간격으로 로그가 모아 저장됩니다. - SizeBasedTriggeringPolicy:
파일의 크기로 지정하여 일정 용량이 모이면 파일로 저장하는 것도 가능합니다. - DefaultRolloverStrategy:
모을 파일의 개수를 지정할 수 있는 전략입니다. 위 코드는 파일의 개수가 30개 초과시 가장 오래된 파일을 제거하며 최대 파일의 개수를 30개 유지시켜줍니다.
Logger: 처리할 로그 정의
<Loggers>
<Logger name="com.dutmdcjf.test" level="info" />
<Logger name="com.dutmdcjf.test.controller" level="debug" />
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Root>
</Loggers>
log4j2 설정 파일 읽는 순서에 따라 log4j2.xml을 먼저 읽고, 그 후에 application.yml을 읽습니다.
log4j2.xml에서 `com.dutmdcjf.test.controller`에서 로그 레벨을 `debug`로 설정했지만 위에서 설정한 application.yml에서는 로그레벨을 `trace`로 설정했기 때문에 해당 로그는 trace 로그 레벨로 적용되어 나옵니다.
- <Loggers>: 로그 이벤트를 수신하고 처리할 로거들을 정의하는 곳
- <Root>: 어플리케이션 전반적으로 적용되는 로깅 설정을 정의합니다.
- <Logger>:
특정 패키지 또는 클래스를 위한 로거를 정의합니다. `name` 속성은 패키지 또는 클래스를 지정하는 옵션입니다.
additivity는 로깅 이벤트가 상위 로거에서 하위 로거로 전파되는 방식을 결정하는 속성입니다. true인 경우 상위 로거에 정의된 Appender와 하위 로거에 정의된 Appender가 동시에 사용되고, false인 경우 하위 로거로 전파되지 않습니다.
Layout: 로그 이벤트 포맷 설정
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] %-5level${PID:-} [%15.15thread] %logger{36}.%M(%F:%L) - %msg%n"/>
- d, data: 로깅 이벤트의 일자 및 시간 출력, 위와 같이 패턴으로 출력 포맷 설정 가능
- r, relatvie: 로그 처리시간 (milliseconds 단위)
- p, level: 로그 레벨 출력
- t, thread: 로깅 이벤트 발생한 스레드명 출력
- c, logger: 로그 이벤트를 발생시킨 클래스의 이름
- M, method: 로깅 이벤트가 발생한 메소드명 출력
- F, file: 로깅 이벤트가 발생한 클래스의 파일명 출력
- L, line: 로깅 이벤트가 발생한 라인 번호 출력
- l, location: 로깅 이벤트가 발생한 `클래스의 풀네임명.메소드명(파일명:라인번호)` 출력
- m, msg: 로그문에서 전달된 메시지를 출력
- n: 줄바꿈
Property: 정적인 값 정의
아래와 같이 name으로 정적인 값을 정의하고, 이를 ${DIR_PATH}, ${LOG_PATTERN} 이런식으로 값을 참조하여 사용할 수 있습니다.
<Properties>
<Property name="DIR_PATH">logs</Property>
<Property name="LOG_PATTERN">[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] %-5level${PID:-} [%15.15thread] %logger{36} - %msg%n</Property>
</Properties>
Console 색 입히기
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout disableAnsi="false" pattern="${LOG_PATTERN}"/>
</Console>
`disableAnsi="false"`로 하여 ANSI 이스케이프 시퀀스를 비활성화해주어야 합니다.
ANSI 이스케이프 시퀀스는 터미널에서 색상, 스타일 및 기타 서식을 지정하는데 사용됩니다. 이를 false로 설정하면 ANSI 이스케이프 시퀀스가 활성화되어 터미널에서 색싱 및 서식이 지원됩니다.
<Property name="LOG_PATTERN">[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] %highlight{%-5level${PID:-}} [%15.15thread] %logger{36}.%M(%F:%L) - %msg%n</Property>
로그레벨 부분에 하이라이트 설정을 하여 색을 입힌 코드입니다. 자세한 설명과 상세 커스텀은 Apache의 logging 사이트를 확인하면 됩니다.
'Spring Boot' 카테고리의 다른 글
Spring Boot 캐시 Cache 구현하기 (spring-context-support? redis?) (0) | 2024.02.05 |
---|---|
Spring Boot Custom Validation 어노테이션 만들기 (0) | 2023.12.13 |
Spring Boot Jsoup을 통한 웹 크롤링 (0) | 2023.12.12 |
Spring Boot Validation @NotNull, @NotEmpty, @NotBlank 차이점 (0) | 2023.10.24 |
SpringBoot에서 JUnit5로 효율적인 단위 테스트 작성하기, Assertions로 값 검증하기 (0) | 2023.10.04 |