서론
스프링 부트 환경에서 profile별로 로깅 방식과 레벨을 설정해야 했다. 이전에 쓰던 프로젝트의 logback.xml 소스를 받아 그대로 사용해도 됐지만, logback.xml에서 profile마다 로그 레벨과 appender를 지정하는 게 내 눈에는 복잡해 보였고 좀 더 좋은 방법이 없을까 찾아보게 되었다.
SpringBoot Logback
spring-boot-starter에는 기본적으로 slf4j 인터페이스를 구현한 logback을 사용한다. 사용자는 slf4j를 인터페이스를 사용하는 덕분에, log 구현 라이브러리를 손쉽게 교체할 수 있다. log4j, logback, log4j2가 있고 후자일수록 성능이 더 좋다고 하지만 스프링 부트가 기본적으로 사용하는 logback을 사용하기로 했다.
스프링 부트를 웬만한 설정 파일은 기본값을 가지고 있다. logback도 마찬가지로 default 설정이 있어서 굳이 로그 설정을 안 해주더라도 콘솔에서 로그를 볼 수 있는 것이다. 그리고 간단한 설정 정도는 property 혹은 yml 파일로 적용할 수 있다.
logging:
pattern:
console: "%-5level %d{yyyy-MM-dd HH:mm:ss}[%thread] %logger[%method:%line] - %msg%n"
file: "%-4relative [%thread] %-5level %logger{35} - %msg%n"
logback:
rollingpolicy:
file-name-pattern: "${LOG_FILE}.%d{yyyy-MM-dd}_%i.zip"
file:
name: logs
max-history: 7
max-size: 5KB
total-size-cap: 1MB
level:
com.blog.tistory: debug
로그 패턴 레이아웃에 관해서는 logback docs를 참조해서 취향껏 만들면 된다.
여기서 profile별로 로깅 정책을 다르게 적용할 필요성이 생겼다. 하나의 yml에 profile을 분리해서 작성했다.
---
spring.profiles: loc
logging:
pattern:
console: "%-5level %d{yyyy-MM-dd HH:mm:ss}[%thread] %logger[%method:%line] - %msg%n"
level:
root: info
com.blog.tistory: debug
---
spring.profiles: dev
logging:
pattern:
console: "%-5level %d{yyyy-MM-dd HH:mm:ss}[%thread] %logger[%method:%line] - %msg%n"
file: "%-4relative [%thread] %-5level %logger{35} - %msg%n"
logback:
rollingpolicy:
file-name-pattern: "${LOG_FILE}.%d{yyyy-MM-dd}_%i.zip"
file:
name: logs
max-history: 7
max-size: 5KB
total-size-cap: 1MB
level:
root: info
com.blog.tistory: debug
---
spring.profiles: stg
logging:
pattern:
file: "%-4relative [%thread] %-5level %logger{35} - %msg%n"
logback:
rollingpolicy:
file-name-pattern: "${LOG_FILE}.%d{yyyy-MM-dd}_%i.zip"
file:
name: logs
max-history: 7
max-size: 5KB
total-size-cap: 1MB
level:
root: error
---
spring.profiles: prd
logging:
pattern:
file: "%-4relative [%thread] %-5level %logger{35} - %msg%n"
logback:
rollingpolicy:
file-name-pattern: "${LOG_FILE}.%d{yyyy-MM-dd}_%i.zip"
file:
name: logs
max-history: 7
max-size: 5KB
total-size-cap: 1MB
level:
root: error
Log Module
위의 예시를 보면 로깅 패턴 등 여러 설정이 중복되는 것을 볼 수 있다. 물론 환경별로 공통적인 설정은 문서 최상단에 정의해 해결할 수 있지만, 완전하게 해결할 수는 없다. 중복적인 내용을 어떻게 깔끔하게 처리할 수 있을까 찾아보다가 profile을 모듈처럼 만들 수 있다는 걸 알았다.
로그방식을 기준으로 콘솔, 파일, 메시지 어펜더 총 3개의 모듈을 만들겠다.
로그레벨을 기준으로 low, high 총 2개의 모듈을 만들겠다.
logback-spring.xml
application.yml이 아닌, logback-spring.xml에 작성한다. 생성과 사용을 분리하기 위해 logback-spring.xml에서 모듈을 생성하고 yml 설정 파일에서 사용하는 기준으로 분리했다.
참고로 logback.xml으로 작성하면 spring boot가 가동되기 전에 읽으므로, 프로파일 확장 기능을 사용하려면 logback-spring.xml으로 작성하라고 스프링 공식문서에서 권장하고 있다.
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<property name="logPath" value="C://logs" />
<property name="fileName" value="test-log" />
<property name="maxHistory" value="7" />
<property name="maxFileSize" value="5KB" />
<property name="totalSizeCap" value="1GB" />
<property name="consolePattern" value="%-5level %d{yyyy-MM-dd HH:mm:ss}[%thread] %logger[%method:%line] - %msg%n"/>
<property name="filePattern" value="%-4relative [%thread] %-5level %logger{35} - %msg%n"/>
<property name="slackPattern" value="%d{yyyy-MM-dd HH:mm:ss}:%-3relative][%thread] %-5level %logger{35} - %msg%n"/>
<property name="slackHookUri" value="%%%%%%%%%YOUR SLACK WEBHOOK URI%%%%%%%%%"/>
<!-- Log Appender Module -->
<springProfile name="console-logging">
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${consolePattern}</pattern>
</encoder>
</appender>
</springProfile>
<!-- file Appender Module -->
<springProfile name="file-logging">
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logPath}//${fileName}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${logPath}//LOG_%d{yyyy-MM-dd}_${fileName}.log.%i.zip</fileNamePattern>
<maxHistory>${maxHistory}</maxHistory>
<maxFileSize>${maxFileSize}</maxFileSize>
<totalSizeCap>${totalSizeCap}</totalSizeCap>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${filePattern}</pattern>
</encoder>
</appender>
</springProfile>
<!-- Message Appender Module -->
<springProfile name="message-logging">
<appender name="slack" class="com.github.maricn.logback.SlackAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${slackPattern}</pattern>
</layout>
<webhookUri>${slackHookUri}</webhookUri>
<username>bbbic</username>
<iconEmoji>:stuck_out_tongue_winking_eye:</iconEmoji>
<colorCoding>true</colorCoding>
</appender>
</springProfile>
<!-- Log Level Module -->
<springProfile name="high-level-logging">
<logger name="root" level="error" />
<logger name="org.springframework" level="error" />
<logger name="com.blog.tistory" level="warn" />
</springProfile>
<springProfile name="low-level-logging">
<logger name="root" level="info" />
<logger name="org.springframework" level="info" />
<logger name="com.blog.tistory" level="debug" />
</springProfile>
<root>
<springProfile name="console-logging">
<appender-ref ref="console" />
</springProfile>
<springProfile name="file-logging">
<appender-ref ref="file" />
</springProfile>
<springProfile name="message-logging">
<appender-ref ref="slack" />
</springProfile>
</root>
</configuration>
xml에서 `<springProfile>` name 속성에 정의된 이름으로 profile이 등록되고 이를 하나의 모듈처럼 사용할 수 있다.
위에서는 총 5개의 모듈을 생성했다.
- console-logging
- file-logging
- message-logging
- high-level-logging
- log-level-logging
application.yml
server:
port: 8080
spring.config.use-legacy-processing: true
---
spring.profiles: loc
spring:
profiles:
include:
- console-logging
- low-level-logging
---
spring.profiles: dev
spring:
profiles:
include:
- file-logging
- low-level-logging
---
spring.profiles: stg
spring:
profiles:
include:
- file-logging
- high-level-logging
---
spring.profiles: prd
spring:
profiles:
include:
- message-logging
- file-logging
- high-level-logging
생성된 모듈을 application.yml에서 조합해서 사용하면 된다. 이런 미리 여러 개의 모듈을 관리하면, 변화에 좀 더 유연한 구조를 가질 수 있다.
스프링부트 2.4버전부터 profile 정의방법이 일부 변경됐다. 이전 방식으로 사용하고 싶으면 `spring.config.use-legacy-processing`을 true로 작성해야 한다.
레거시 방식을 사용하고 싶지 않다면 property 작성법을 일부 변경해야한다.
package com.blog.tistory;
import java.util.Arrays;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import lombok.extern.slf4j.Slf4j;
@SpringBootApplication
@Slf4j
public class ExcelApplication {
public static void main(String[] args) {
SpringApplication.run(ExcelApplication.class, args).close();
}
@Bean
@Profile("!prd")
public CommandLineRunner runProfile(Environment env) {
return args -> {
Arrays.stream(env.getActiveProfiles()).forEach(profile -> {
log.debug("current profile :: {}", profile);
});
};
}
@Bean
@Profile("prd")
public CommandLineRunner runPrdProfile() {
return args -> {
try {
throw new RuntimeException("테스트 에러 발생");
} catch (Exception e) {
log.debug("prd runtime error. this must be ignored");
log.error("prd runtime error", e);
}
};
}
}
테스트하기 위해 간단하게 loc와 prd profile을 테스트해봤다. loc에서는 debug 에러가 콘솔에서 잘 나오는 것을 볼 수 있고, prd에서는 error 로그만 파일과 메시지로 찍히는 것을 확인할 수 있다.
결론
spring profile을 단순하게 환경단위로만 정의하는 것을 넘어서, 유의미한 객체 단위로 만들어 조합해서 사용할 수 있다는 것을 배울 수 있었다. 또한, logback-{profile}.xml로 나눠져 있던 것을 하나의 logback에서 profile을 객체로 생성해서 관리한다는 점에서 좀 더 좋은 코드라고 생각했다.
추가적으로, 코드 리뷰를 받을 때 하나의 파일에 모든 환경별 설정을 하는 것은 보안적인 측면에서 문제가 생길 수도 있다고 했다. 보통 보안이 중요한 프로젝트는 특정 환경의 파일이 해킹당하면 전체 환경정보까지 해킹당하는 것을 방지하기 위해 profile별로 환경 파일을 분리한다고 한다. 그리고는 빌드툴로 특정 파일명 패턴은 제외한다거나(exclude: application-${profile).yml) 혹은 CI/CD단계에서 jenkins로 현재 profile과 관련 없는 파일은 삭제해 배포하는 방식으로 관리한다고 한다. 즉, 보안이 중요한 프로젝트는 하나로 합치는 것보단 여러 개로 관리하는 것이 조금 번거롭더라도 옳은 방식이라는 것이다.
보안을 위해서 `application-${profile).yml`으로 나눠서 작성하면 더 좋다.
'Backend > Java' 카테고리의 다른 글
Tika로 파일 MIME 타입 검사 (0) | 2021.07.24 |
---|---|
YAML) @PropertySource에서 EnvironmentPostProcessor까지 (0) | 2021.07.03 |
Java Stream과 Multi Thread (2) | 2021.04.17 |
JDBC query vs queryForObject (0) | 2021.03.30 |
Spring Security JWT Token (0) | 2021.03.22 |