
Configuration
지난 글에서 봤던 모든 LoggerContext는 각각 활성화된 Configuration을 가지고 있습니다.
모든 Appender, Layout, Filter, Logger의 설정을 모델링하며, StrSubstitutor 등의 참조를 포함하고 있습니다.
- Appender: 로그 출력 대상 (예: 파일, 콘솔, 원격 서버 등)
- Layout: 로그 메시지의 형식 정의
- Filter: 로그 출력 여부를 결정하는 조건
- Logger: 로그를 기록하는 주체
- StrSubstitutor: 변수 치환 기능을 제공하는 유틸리티 클래스 (예: ${env:HOME} 같은 표현을 실제 값으로 치환)
Log4j Core의 Configuration은 일반적으로 애플리케이션 초기화 시에 수행됩니다.
권장되는 방식은 1) 설정 파일을 읽는 것이지만, 2) 코드 기반으로 설정하는 것도 가능합니다.
이 중에서 가장 많이 사용되는 방식인 설정 파일로부터 어떻게 불러오고 객체를 생성하는지 알아보겠습니다.
설정 파일
설정 파일이란, 보통 많이 알려진 방식인 xml, yml 등의 파일을 말합니다.
새로운 LoggerContext가 초기화될 때, 로깅 구현의 중심이 되는 Log4j Core는 해당 컨텍스트에 이름(context name)을 부여하고, 다음의 classpath 경로들을 순서대로 스캔하여 설정 파일을 찾습니다:
- log4j2-test<contextName>.<extension> 형식의 파일
- log4j2-test.<extension> 형식의 파일
- log4j2<contextName>.<extension> 형식의 파일
- log4j2.<extension> 형식의 파일
contextName
런타임 환경에서 파생된 이름:
- 독립 실행형 Java SE 애플리케이션의 경우: 무작위 식별자
- 웹 애플리케이션의 경우: 애플리케이션 디스크립터에서 유래된 식별자
extension
ConfigurationFactory가 지원하는 설정 파일 확장자, 어떤 확장자를 먼저 찾을지는 관련 ConfigurationFactory의 우선순서에 따라 결정됩니다.
지원되는 확장자 (Order 내림차순 순서대로 확인):
- XML (5)
- JSON (6)
- YAML (7)
- Properties (8)
설정 파일을 찾지 못하면, Log4j Core는 DefaultConfiguration을 사용하며, 이 경우 status logger가 경고 메시지를 출력합니다.
기본 설정은 log4j2.level(Default는 Error)보다 덜 심각한 메시지를 모두 콘솔에 출력합니다.
이제 Configuration이 어떻게 코드로 구현되어 있는지 보겠습니다. Configuration 역시 인터페이스이며 LifeCycle을 상속받고 있습니다. LifeCycle은 이전 글에서 LoggerContext를 보면서 한번 다루었습니다.
public interface Configuration extends Filterable {
String CONTEXT_PROPERTIES = "ContextProperties";
String getName();
LoggerConfig getLoggerConfig(String name);
Map<String, Appender> getAppenders();
void addAppender(final Appender appender);
Map<String, LoggerConfig> getLoggers();
void addLoggerAppender(Logger logger, Appender appender);
void addLoggerFilter(Logger logger, Filter filter);
void setLoggerAdditive(Logger logger, boolean additive);
void addLogger(final String name, final LoggerConfig loggerConfig);
void removeLogger(final String name);
List<String> getPluginPackages();
Map<String, String> getProperties();
LoggerConfig getRootLogger();
void addListener(ConfigurationListener listener);
void removeListener(ConfigurationListener listener);
...
}
저희가 설정 파일에 넣어주는 Appender와 Logger 등을 관리하는 필수 메서드들을 정의하고 있습니다.
대부분(아마도 모든)의 구현체들은 해당 인터페이스를 바로 구현하지 않고, AbstractConfiguration을 상속받아 구현합니다.
public abstract class AbstractConfiguration extends AbstractFilterable implements Configuration {
private static final int BUF_SIZE = 16384;
protected Node rootNode;
protected final List<ConfigurationListener> listeners = new CopyOnWriteArrayList<>();
protected final List<String> pluginPackages = new ArrayList<>();
protected PluginManager pluginManager;
protected boolean isShutdownHookEnabled = true;
protected long shutdownTimeoutMillis;
protected ScriptManager scriptManager;
private Advertiser advertiser = new DefaultAdvertiser();
private Node advertiserNode;
private Object advertisement;
private String name;
private ConcurrentMap<String, Appender> appenders = new ConcurrentHashMap<>();
private ConcurrentMap<String, LoggerConfig> loggerConfigs = new ConcurrentHashMap<>();
private List<CustomLevelConfig> customLevels = Collections.emptyList();
private Set<MonitorResource> monitorResources = Collections.emptySet();
private final ConcurrentMap<String, String> propertyMap = new ConcurrentHashMap<>();
private final Interpolator tempLookup = new Interpolator(propertyMap);
private final StrSubstitutor runtimeStrSubstitutor = new RuntimeStrSubstitutor(tempLookup);
private final StrSubstitutor configurationStrSubstitutor = new ConfigurationStrSubstitutor(runtimeStrSubstitutor);
private LoggerConfig root = new LoggerConfig();
private final ConcurrentMap<String, Object> componentMap = new ConcurrentHashMap<>();
private final ConfigurationSource configurationSource;
private final ConfigurationScheduler configurationScheduler = new ConfigurationScheduler();
private final WatchManager watchManager = new WatchManager(configurationScheduler);
private AsyncLoggerConfigDisruptor asyncLoggerConfigDisruptor;
private AsyncWaitStrategyFactory asyncWaitStrategyFactory;
private NanoClock nanoClock = new DummyNanoClock();
private final WeakReference<LoggerContext> loggerContext;
...
}
변수의 개수부터 어마어마합니다.
AbstractConfiguration를 상속하는 Configuration들을 보면 위에서 보았던 확장자들의 Configuration 클래스가 보입니다.

이런 Configuration 클래스를 만들어내려면 개발자가 작성한 설정 파일들을 읽어와서 파싱(Parsing)을 거쳐야 합니다.
이때 파싱을 하는 핵심 코드는 ConfigurationFactory와 그 하위 구현 클래스들 내부에 있습니다.
각 포맷별로 파싱을 담당하는 클래스가 plugin으로 구현되어 자동으로 로드됩니다.
설정 포맷 파싱 클래스 (ConfigurationFactory 구현체) 내부에서 쓰는 파서
| XML | XmlConfigurationFactory | XmlConfiguration → DocumentBuilder (javax.xml) |
| JSON | JsonConfigurationFactory | JsonConfiguration → Jackson (com.fasterxml.jackson) |
| YAML | YamlConfigurationFactory | YamlConfiguration → Jackson + SnakeYAML |
| Properties | PropertiesConfigurationFactory | PropertiesConfiguration → java.util.Properties |
이 중에서도 우선순위가 가장 높은 Properties를 살펴보겠습니다.
@Plugin(name = "PropertiesConfigurationFactory", category = ConfigurationFactory.CATEGORY)
@Order(8) // 1
public class PropertiesConfigurationFactory extends ConfigurationFactory {
@Override
protected String[] getSupportedTypes() {
return new String[] {".properties"};
}
// 2
@Override
public PropertiesConfiguration getConfiguration(
final LoggerContext loggerContext, final ConfigurationSource source) {
final Properties properties = new Properties();
try (final InputStream configStream = source.getInputStream()) {
properties.load(configStream); // 3
} catch (final IOException ioe) {
throw new ConfigurationException("Unable to load " + source.toString(), ioe);
}
return new PropertiesConfigurationBuilder()
.setConfigurationSource(source)
.setRootProperties(properties)
.setLoggerContext(loggerContext)
.build();
}
}
1. 우선순위는 @Order 애노테이션을 통해 인식합니다.
2. Properties file로부터 PropertiesConfiguration을 생성하는 부분입니다. Properties는 java.util의 클래스이며 매개변수로 받은 ConfigurationSource의 InputStream을 통해서 load 합니다. 그 후 Builder를 통해서 최종적으로 Configuration을 생성하고 반환합니다.
3. configStream으로부터 .properties 파일을 읽어와서, 파일 내의 키-값 쌍을 Properties 객체에 파싱하여 저장합니다. 즉, 파일의 내용을 자바에서 사용할 수 있는 형태로 변환하는 과정입니다. 좀 더 안쪽을 들여다보면 비로소 Properties.java의 static class인 LineReader(실제 파싱이 일어나는 부분)를 사용하는 것을 볼 수 있습니다.
public synchronized void load(InputStream inStream) throws IOException {
Objects.requireNonNull(inStream, "inStream parameter is null");
load0(new LineReader(inStream));
}
객체지향의 OCP
YAML, JSON, XML도 역시 파싱 되어 읽어 들이게 되지만 보시다시피 내부에서 쓰는 파서와 구현 방식은 서로 종속되어 있지 않고 자유롭게 되어 있습니다.
만약에 새로운 파일도 파싱 되어야 한다면 ConfigurationFactory를 상속받는 클래스를 만들기만 하면 기존 코드를 수정하지 않고 기능을 추가할 수 있습니다.
이는 OOP의 SOLID 원칙 중 하나인 Open Closed Principle을 잘 지킨 예시 중 하나가 됩니다.
Configuration을 생성하는 'Factory' 역할을 하는 다양한 구현체들이 있으며, 다형성을 활용함으로써 어떤 구현체가 사용될지는 런타임에 결정되기 때문에, 설정 파일 형식(XML, YAML, JSON 등)에 따라 유연하게 대응할 수 있는 구조입니다.
Reference
'나의 생각' 카테고리의 다른 글
| Log4j2 아키텍처 분석하기 - 1 (2) | 2025.06.26 |
|---|---|
| Log의 필요성과 주의점 (1) | 2025.06.24 |