Table of Contents
개발을 하게되면 특별한 경우를 제외하고 남이 짜놓은 코드를 리팩터링하거나 기능을 추가하는 식으로 진행됩니다. 그래서 프로젝트를 처음 시작하면서 생각해야할 것들을 제대로 공부하지 못하는 경우가 많은 것 같습니다.
이번 글에서는 가장 기본적이면서도 중요한 요소인 로깅에 대해서 정리할 필요성을 느끼게 되었습니다.
로그란?
로그는 간단하게 말해서 연속된 데이터의 기록이라고 할 수 있습니다.
일반적으로 처음 프로그래밍을 배울 때는 보통 System.out.print
같은 언어에서 제공해주는 메소드를 사용해보셨을 것입니다. 프로그램이 실행되면서 콘솔에 무엇인가가 출력되는데, 이런 것들이 로그가 될 수 있습니다.
Logging Framework
System.out.print
를 사용하여 디버깅을 할 수 있지만, 만약 어플리케이션의 사이즈가 커지게 되면, 이런 방식이 얼마나 비효율적인지를 느끼실 수 있을 것입니다.
Logging Framework가 없다면?
public void test() {
System.out.println("test() 메소드 실행");
try {
someMethod();
} catch (Exception e) {
System.out.println(e.message);
e.printStackTrace()
}
}
위 코드는 간단하게 메서드에서 println
를 이용하여 메소드 내부의 로깅을 하는 예제입니다.
어플리케이션의 사이즈가 커지면 어떻게 될까요?
위와 같은 로깅 코드를 가지고 있는 메소드들이 많아질 것이고, 로깅을 위해 시간
, 패키지의 위치
, 메소드 명
등의 내용까지 붙여지게되면 코드량도 많아지고 관리하기도 어려워질 것 입니다.
이 어플리케이션을 필요로하는 사람에게 판매를 하게된다면 어떨까요?
소비자에게는 어플리케이션의 콘솔창으로부터 로그 메시지가 발생하는 것을 원치 않는 상황이 생길 것입니다. 만약 이 어플리케이션에 문제가 생겨 디버깅을 해야하는 상황이 생기면 우리는 또 다시 System.out.print
를 사용할 수 밖에 없는 상황에 놓이게 됩니다.
그 외에도 한 가지 생각해봐야 하는 것이 있습니다.
필요할 수도 있는 추적용 데이터는 어떻게 저장할까요?
예를 들어 IP 주소
, 디바이스 버전
과 같은 정보들은 나중에 데이터 추적을 하거나 디버깅을 위해서 로그를 남길 필요가 생길 수 있습니다.
System.out.print
를 통해서 남겨진 결과는 콘솔에 남지만, 콘솔의 메모리에 언제까지고 저장하고 있을 수 없어 위와 같은 정보들을 파일에 저장할 필요가 생기게 됩니다.
- 비즈니스로직에서 로그성 코드를 분리
- 상황에 따라 유연하게 대처할 수 있도록 로그를 레벨로 분리하여 노출 및 관리
- 특정한 로그는 정해진 파일에 저장
간단하게 정리하면 위와 같은 요구사항이 생겨, Logging Framework가 등장하게 되었습니다.
Slf4j
스프링의 Logging Framework에서 가장 유명한 라이브러리가 바로 slf4j(Simple Logging Facade For Java) 입니다.
slf4j
는 다양한 자바 로깅 시스템을 사용할 수 있도록 해주는 파사드 패턴의 인터페이스라고 생각하시면 될 것 같습니다.
위 그림과 같이 slf4j를 사용하면 logback
, log4j
, log4j2
와 같은 구현체를 어플리케이션에 영향 없이 손쉽게 교체할 수 있습니다.
스프링 로그 설정 예제
지금부터는 스프링 프로젝트를 하나 만들면서 로그 설정하는 방법을 자세하게 다뤄보도록 하겠습니다.
- OS: MacOS
- IDE: Intellij
- Project: Spring initializr
의존성 추가
의존성의 경우, 기본적으로 spring-boot-starter-web
과 개발 편의를 위한 lombok
을 추가해주었습니다.
spring-boot-starter-web
에는 기본적으로 logback
로깅 모듈이 포함되어 있지만, 성능 상 log4j2
가 logback
보다 우수하기 때문에 이를 제외하고 log4j2
를 추가해주는 작업을 해줄 것입니다.
// ... 생략
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-log4j2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
+ configurations {
+ all {
+ exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
+ }
+ }
// ... 생략
위 코드처럼 log4j2
의존성을 추가해주고, spring-boot-starter-logging
을 제외시켜 주었습니다.
log4j2 설정
log4j2를 설정하는 방식에는 다양한 방법들이 있지만, xml
설정방식을 제외한 나머지 설정에는 Jackson
등의 추가적인 라이브러리가 필요한 경우가 있습니다.
이 예제에서는 xml
설정 방식으로 진행하겠습니다.
패키지 구조
main
├── java
│ └── com
│ └── deeplify
│ └── tutorial
│ └── logging
│ └── LoggingApplication.java
└── resources
├── application.yml
+ ├── log4j2.xml // 설정 파일 추가
├── static
└── templates
간단한 콘솔 로그 설정
<?xml version="1.0" encoding="UTF-8" ?>
<Configuration status="INFO">
<Properties>
<Property name="LOG_PATTERN">%d{HH:mm:ss.SSSZ} [%t] %-5level %logger{36} - %msg%n</Property>
</Properties>
<Appenders>
<Console name="ConsoleLog" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="ConsoleLog" />
<AppenderRef ref="FileLog" />
</Root>
</Loggers>
</Configuration>
하나 하나씩 무엇을 의미하는지 살펴보도록 하겠습니다.
- Configuration: 로그 설정을 위한 최상위 요소
- status 속성: Log4j2 내부의 동작에 대한 로깅 레벨을 설정 (log4j 내부 문제를 해결하기 위한 용도의 로깅이 필요한 경우 사용)
- Properties: 하단 설정에 사용할 변수들을 정의
- name: 위 예제에서 name=”LOG_PATTERN”으로 설정하여
LOG_PATTERN
이라는 변수를 정의- Appenders: 로그가 출력되는 위치
- Console: 콘솔에 출력될 로그 설정
- name: 어펜더의 이름
- target: 로그 타겟 (default: SYSTEM_OUT)
- PatternLayout: 로그의 패턴을 설정
- Loggers: 로깅 작업의 주체로 각 패키지 별로 다양한 설정을 할 수 있음
- Root: 모든 패키지에 대한 로깅을 하기 위한 일반적인 로그 정책 설정 (한 개만 설정할 수 있음)
- AppenderRef: 상단에 설정한 Appender를 참조
출력 예제
@Slf4j
@SpringBootApplication
public class LoggingApplication {
public static void main(String[] args) {
SpringApplication.run(LoggingApplication.class, args);
log.trace("Hi I'm {} log", "TRACE");
log.debug("Hi I'm {} log", "DEBUG");
log.info("Hi I'm {} log", "INFO");
log.warn("Hi I'm {} log", "WARN");
log.error("Hi I'm {} log", "ERROR");
}
}
설정을 해준 상태로 위와 같이 LoggingApplication.java
위치에 로그를 찍어보기 위해 코드를 작성하고, 실행해보았습니다.
20:22:20.831 [main] INFO com.deeplify.tutorial.logging.LoggingApplication - Hi I'm INFO log
20:22:20.831 [main] INFO com.deeplify.tutorial.logging.LoggingApplication - Hi I'm INFO log
20:22:20.832 [main] WARN com.deeplify.tutorial.logging.LoggingApplication - Hi I'm WARN log
20:22:20.832 [main] WARN com.deeplify.tutorial.logging.LoggingApplication - Hi I'm WARN log
20:22:20.832 [main] ERROR com.deeplify.tutorial.logging.LoggingApplication - Hi I'm ERROR log
20:22:20.832 [main] ERROR com.deeplify.tutorial.logging.LoggingApplication - Hi I'm ERROR log
레벨에 따른 출력
일반적으로 레벨을 위와 같이 구성되어 있습니다. 오른쪽으로 갈수록 꼭 출력해야 하는 중요한 레벨이라고 생각하시면 될 것 같습니다.
운영환경에서는 특별한 경우를 제외하고 INFO
로 설정하는 경우가 많고, 개발 환경에서는 TRACE
또는 DEBUG
로 설정하여 좀 더 자세한 로그를 확인할 수 있도록 합니다.
로그가 파일로 출력되도록 하는 설정 예제
<?xml version="1.0" encoding="UTF-8" ?>
<Configuration status="INFO">
<Properties>
<Property name="LOG_PATTERN">%d{HH:mm:ss.SSSZ} [%t] %-5level %logger{36} - %msg%n</Property>
</Properties>
<Appenders>
<Console name="ConsoleLog" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
</Console>
<RollingFile name="FileLog"
fileName="./logs/spring.log"
filePattern="./logs/spring-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8" />
<Policies>
<TimeBasedTriggeringPolicy interval="1" />
<SizeBasedTriggeringPolicy size="10000KB" />
</Policies>
<DefaultRolloverStrategy max="20" fileIndex="min" />
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="ConsoleLog" />
<AppenderRef ref="FileLog" />
</Root>
</Loggers>
</Configuration>
- RollingFile: 조건에 따라 파일에 로그를 출력하도록 설정
- name: 어펜더의 이름
- fileName: 경로를 포함한 파일 이름
- filePattern: 롤링 조건에 따른 경로를 포함한 파일 이름 패턴
- Policies: 파일 롤링 정책
- TimeBasedTriggeringPolicy: 1일 단위(interval=1)로 새로운 파일에 로그를 기록
- SizeBasedTriggeringPolicy: 파일 사이즈를 기준으로 용량이 넘칠 경우 다음 파일을 생성하여 기록
- DefaultRolloverStrategy: 파일 용량 초과 시 생성될 수 있는 파일의 최대 개수 설정
로그를 파일로 저장하면, 로그 파일의 양도 관리를 해주어야합니다. 위의 복잡해보일 수 있는 설정들은 로그파일의 최대 용량 설정과 날짜별로 로그파일을 관리하기 위한 것들입니다.
파일 출력 예제
.
+ ├── logs
+ │ └── spring.log // 로그파일 생성
└── src
├── main
// ... 생략
20:25:20.831 [main] INFO com.deeplify.tutorial.logging.LoggingApplication - Hi I'm INFO log
20:25:20.832 [main] WARN com.deeplify.tutorial.logging.LoggingApplication - Hi I'm WARN log
20:25:20.832 [main] ERROR com.deeplify.tutorial.logging.LoggingApplication - Hi I'm ERROR log
어플리케이션을 실행하면 위와 같은 위치에 로그 폴더와 파일이 생기는 것을 확인하실 수 있고, (폴더는 제가 임의로 생성했습니다.) 파일 내부에 로그 기록들이 남아 있는 것도 확인하실 수 있습니다.
패키지별 로그 설정
<Loggers>
<Root level="info">
<AppenderRef ref="ConsoleLog" />
<AppenderRef ref="FileLog" />
</Root>
<Logger name="com.deeplify.tutorial.logging" level="debug">
<AppenderRef ref="ConsoleLog" />
</Logger>
</Loggers>
- Logger: 패키지 단위로 로그 설정
- name: 이름에 패키지 경로를 설정
- level: 해당 패키지의 로그 레벨 설정
위와 같은 방식으로 패키지 별로 로그를 관리할 수 있도록 하는 설정도 제공하고 있습니다. 자신의 어플리케이션에서 상황에 따라 적절하게 이용하면 좋을 것 같습니다.
패키지별 로그 예제
+ 20:26:20.731 [main] DEBUG com.deeplify.tutorial.logging.LoggingApplication - Hi I'm DEBUG log
20:26:20.731 [main] INFO com.deeplify.tutorial.logging.LoggingApplication - Hi I'm INFO log
20:26:20.732 [main] WARN com.deeplify.tutorial.logging.LoggingApplication - Hi I'm WARN log
20:26:20.732 [main] ERROR com.deeplify.tutorial.logging.LoggingApplication - Hi I'm ERROR log
실행 후, 콘솔창의 결과를 확인해보면 위와 같이 DEBUG
레벨의 로그가 남겨져 있는 것을 확인할 수 있습니다.
예제에서 보여드린 코드는 위 프로젝트에서 확인하실 수 있습니다.
참고 문서
맺음
간단하게 스프링에서 logging Framework를 이용한 로깅 설정하는 방법에 대해서 알아보았습니다. 혹시 궁금하신 점이나 이상한 점이 있으면 댓글 부탁드리겠습니다.
감사합니다.