본문 바로가기
spring

[SpringBoot] logback xml을 java configuration으로 변환하기

by moonsiri 2024. 11. 4.
728x90
반응형

Spring Boot 프로젝트에서는 일반적으로 로깅 설정을 위해 logback-spring.xml 파일을 사용합니다. (XML 기반 설정)

Java 기반 설정의 장점(유연성과 정적 타입 검사)을 고려하여 로깅 설정을 Java Configuration으로 변환하겠습니다.

 

기존 XML 기반 Logback 설정

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
    <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss} - %msg%n"/>
    <property name="LOG_FILE" value="/var/logs/myapp.log"/>
    
    <!-- 콘솔 Appender 설정 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <!-- 파일 Appender 설정 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_FILE}</file>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_FILE}-%d{yyyy-MM-dd}.zip</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
    </appender>

    <!-- 로깅 레벨 설정 -->
    <logger name="com.example" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </logger>
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

 

Java Configuration으로 변환

Java 기반 Appender 구성

Appender 설정을 XML 대신 Java Configuration으로 전환합니다.

ConsoleAppender, RollingFileAppender를 Java 코드로 설정할 수 있게 하고, 여러 프로파일에 따른 Appender를 동적으로 설정하는 데도 유리합니다.

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import org.slf4j.LoggerFactory;

public abstract class BaseLogger {

	private final LoggerContext logCtx = (LoggerContext) LoggerFactory.getILoggerFactory();
	private Appender<ILoggingEvent> appender;

	protected BaseLogger() {
	}

	protected void initializeAppender(Appender<ILoggingEvent> appender) {
		if (this.appender == null) {
			this.appender = appender;
		}
	}

	protected LoggerContext getContext() {
		return logCtx;
	}

	public Appender<ILoggingEvent> retrieveAppender() {
		if (appender == null) {
			throw new IllegalStateException("Appender has not been initialized.");
		}
		return appender;
	}
}
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.ConsoleAppender;
import com.biz.config.logback.properties.LogProperties;

public class ConsoleLogger extends BaseLogger {

	private ConsoleLogger(LogProperties logProperties) {
		getContext().reset();
		initializeAppender(getLogAppender(logProperties));
	}

	public static Appender<ILoggingEvent> getAppender(LogProperties logProperties) {
		ConsoleLogger config = new ConsoleLogger(logProperties);
		return config.retrieveAppender();
	}

	private ConsoleAppender<ILoggingEvent> getLogAppender(LogProperties logProperties) {
		PatternLayoutEncoder consoleLogEncoder = createLogEncoder(logProperties.getPattern());
		ConsoleAppender<ILoggingEvent> logConsoleAppender = createLogAppender(consoleLogEncoder);
		logConsoleAppender.start();

		return logConsoleAppender;
	}

	private PatternLayoutEncoder createLogEncoder(String pattern) {
		PatternLayoutEncoder encoder = new PatternLayoutEncoder();
		encoder.setContext(getContext());
		encoder.setPattern(pattern);
		encoder.start();
		return encoder;
	}

	private ConsoleAppender<ILoggingEvent> createLogAppender(PatternLayoutEncoder consoleLogEncoder) {
		ConsoleAppender<ILoggingEvent> logConsoleAppender = new ConsoleAppender<>();
		logConsoleAppender.setName("STDOUT");
		logConsoleAppender.setContext(getContext());
		logConsoleAppender.setEncoder(consoleLogEncoder);

		return logConsoleAppender;
	}
}
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.RollingPolicy;
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
import com.biz.config.logback.properties.LogProperties;

public class RollingFileLogger extends BaseLogger {

	private RollingFileLogger(LogProperties logProperties) {
		initializeAppender(getLogAppender(logProperties));
	}

	public static Appender<ILoggingEvent> getAppender(LogProperties logProperties) {
		RollingFileLogger config = new RollingFileLogger(logProperties);
		return config.retrieveAppender();
	}

	private RollingFileAppender<ILoggingEvent> getLogAppender(LogProperties logProperties) {
		PatternLayoutEncoder rollingLogEncoder = createLogEncoder(logProperties.getPattern());
		RollingFileAppender<ILoggingEvent> rollingFileAppender = createLogAppender(logProperties.getFile(), rollingLogEncoder);
		TimeBasedRollingPolicy<RollingPolicy> rollingPolicy = createLogRollingPolicy(logProperties.getFile(), rollingFileAppender);
		rollingFileAppender.setRollingPolicy(rollingPolicy);
		rollingFileAppender.start();

		return rollingFileAppender;
	}

	private TimeBasedRollingPolicy<RollingPolicy> createLogRollingPolicy(String logFilePath, RollingFileAppender<ILoggingEvent> fileAppender) {
		TimeBasedRollingPolicy<RollingPolicy> policy = new TimeBasedRollingPolicy<>();
		policy.setContext(getContext());
		policy.setParent(fileAppender);
		policy.setFileNamePattern(logFilePath + "_%d{yyyy-MM-dd}.zip");
		policy.setMaxHistory(5);
		policy.start();
		return policy;
	}

	private PatternLayoutEncoder createLogEncoder(String pattern) {
		PatternLayoutEncoder encoder = new PatternLayoutEncoder();
		encoder.setContext(getContext());
		encoder.setPattern(pattern);
		encoder.start();
		return encoder;
	}

	private RollingFileAppender<ILoggingEvent> createLogAppender(String logFilePath, PatternLayoutEncoder rollingLogEncoder) {
		RollingFileAppender<ILoggingEvent> logRollingAppender = new RollingFileAppender<>();
		logRollingAppender.setContext(getContext());
		logRollingAppender.setFile(logFilePath);
		logRollingAppender.setEncoder(rollingLogEncoder);

		return logRollingAppender;
	}

}
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "log")
public class LogProperties {

	private String pattern;
	private String file;
}

 

LogChain 및 Log 클래스 구현

Java Configuration에서 체이닝을 통해 여러 로거와 Appender 설정을 손쉽게 할 수 있도록 Log 클래스와 LogChain 인터페이스를 구현했습니다.

이 Log 클래스를 사용하여 각 로깅 레벨별로 Appender와 Logger를 관리할 수 있습니다.

public interface LogChain {
}
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

public class Log implements LogChain {

	private final LoggerContext logCtx;

	private final List<LoggerConfig> loggerConfigs = new ArrayList<>();
	private final List<String> offLoggerNames = new ArrayList<>();

	public Log() {
		this.logCtx = (LoggerContext) LoggerFactory.getILoggerFactory();
	}

	public Log off(Consumer<OffBuilder> offConfig) {
		OffBuilder builder = new OffBuilder();
		offConfig.accept(builder);
		offLoggerNames.addAll(builder.loggerNames);
		return this;
	}

	public Log debug(Consumer<LogLevelBuilder> levelConfig) {
		LogLevelBuilder builder = new LogLevelBuilder(Level.DEBUG);
		levelConfig.accept(builder);
		loggerConfigs.addAll(builder.configs);
		return this;
	}

	public Log info(Consumer<LogLevelBuilder> levelConfig) {
		LogLevelBuilder builder = new LogLevelBuilder(Level.INFO);
		levelConfig.accept(builder);
		loggerConfigs.addAll(builder.configs);
		return this;
	}

	public Log warn(Consumer<LogLevelBuilder> levelConfig) {
		LogLevelBuilder builder = new LogLevelBuilder(Level.WARN);
		levelConfig.accept(builder);
		loggerConfigs.addAll(builder.configs);
		return this;
	}

	public Log error(Consumer<LogLevelBuilder> levelConfig) {
		LogLevelBuilder builder = new LogLevelBuilder(Level.ERROR);
		levelConfig.accept(builder);
		loggerConfigs.addAll(builder.configs);
		return this;
	}

	public LogChain build() {
		offLoggerNames.forEach(this::offLogger);

		loggerConfigs.forEach(config -> {
			Logger logger = logCtx.getLogger(config.name);
			logger.setAdditive(config.additive);
			logger.setLevel(config.level);

			for (Appender<ILoggingEvent> appender : config.appenders) {
				logger.addAppender(appender);
			}
		});

		return this;
	}

	private void offLogger(String loggerName) {
		Logger logger = logCtx.getLogger(loggerName);
		logger.setLevel(Level.OFF);
	}

	private static class LoggerConfig {
		private final String name;
		private final Level level;
		private final boolean additive;
		private final Appender<ILoggingEvent>[] appenders;

		@SafeVarargs
		public LoggerConfig(String name, Level level, boolean additive, Appender<ILoggingEvent>... appenders) {
			this.name = name;
			this.level = level;
			this.additive = additive;
			this.appenders = appenders;
		}
	}

	public static class OffBuilder {
		private final List<String> loggerNames = new ArrayList<>();

		public OffBuilder add(String loggerName) {
			loggerNames.add(loggerName);
			return this;
		}
	}

	public static class LogLevelBuilder {
		private final List<LoggerConfig> configs = new ArrayList<>();
		private final Level level;

		public LogLevelBuilder(Level level) {
			this.level = level;
		}

		@SafeVarargs
		public final LogLevelBuilder add(String loggerName, boolean additive, Appender<ILoggingEvent>... appender) {
			configs.add(new LoggerConfig(loggerName, level, additive, appender));
			return this;
		}

		@SafeVarargs
		public final LogLevelBuilder add(String loggerName, Appender<ILoggingEvent>... appender) {
			return add(loggerName, true, appender);
		}
	}
}
  • log.off(), log.debug(), log.info() 메서드 등을 통해 로깅 레벨과 각 로거 설정을 체이닝 방식으로 적용할 수 있습니다.
  • Appender를 여러개 지정할 수 있고, 각 로거는 개별적으로 additive 설정을 가질 수 있습니다.
  • 특정 패키지나 클래스의 로깅을 끄기 위해 OffBuilder를 구현하였고, 로깅 레벨별 설정을 제공하기 위해 LogLevelBuilter를 구현하였습니다.

 

변환코드 예시)

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Profile("local")
@Configuration
public class LogConfig {

    @Bean
    public LogChain logger(LogProperties logProperties) {
        Log log = new Log();

        // Off 로그 설정 - 특정 패키지나 클래스를 로깅에서 제외합니다.
        log.off(off -> off
            .add("io.lettuce.core")
            .add("com.zaxxer.hikari")
            .add("jdbc")
            .add("jdbc.connection")
            .add("jdbc.audit")
        );

        // Appender 구성 - ConsoleAppender를 예제로 추가합니다.
        Appender<ILoggingEvent> consoleAppender = ConsoleAppender.getAppender(logProperties);

        // Debug 로그 설정 - 다양한 로그 레벨을 체이닝 방식으로 설정할 수 있습니다.
        log.debug(debug -> debug
            .add("com.example", false, consoleAppender)
            .add("jdbc.sqltiming", false, consoleAppender)
            .add("jdbc.resultsettable", false, consoleAppender)
            .add("root", consoleAppender)
        );

        // Info 로그 설정
        log.info(info -> info
            .add("org", false, consoleAppender)
        );

        // log.build()를 통해 설정을 마치고 반환합니다.
        return log.build();
    }
}

Java Configuration은 프로파일 기반 로깅  설정에 매우 유연합니다. @Profile을 활용하여 특정 프로파일에 따른 LoggerAppender 구성을 쉽게 적용할 수 있습니다.

 

728x90
반응형

댓글