Logging 관련 라이브러리는 다양한데, SLF4J는는 로깅 추상 레이어를 제공해 이런 라이브러리들을 하나의 통일된 방식으로 사용할 수 있는 방법을 제공한다. SLF4J는 로깅에 대한 추상 레이어를 제공하는 인터페이스의 모음으로, Facade 패턴이다.
Spring에서는 SLF4J를 사용하려면 두 가지 방식이 존재한다. Logger 변수를 직접 선언하거나 @Slf4j
애노테이션을 사용하는 방식이다.
// 직접 선언하는 방식
public class MangoService {
private static final Log logger = LogFactory.getLog(MangoService.class);
}
// 애노테이션을 사용하는 방식
@Slf4j
public class MangoService {
private static final Log logger = LogFactory.getLog(MangoService.class);
}
여기서 롬복 애노테이션을 사용하는 경우 클래스 파일(.class
)을 확인해보면 직접 선언하는 방식과 차이가 없다는 것을 알 수 있다. 따라서, 내부 동작은 Logger 변수가 선언된 상태라고 가정하고 설명한다.
spring-boot-starter-web
에는 spring-boot-starter-logging
의존성이 포함되어 있다. 해당 의존성에는 Logback이 설정되어 있는데, Log4j2 의존성을 추가로 설정한 경우이다.
맨 처음 애플리케이션을 기동하면 진입 지점이 SpringApplication 클래스이다. 해당 클래스에는 logger 정적 변수가 선언되어 있는데 이를 초기화하는 작업이 이루어진다.
// SpringApplication.java
public class SpringApplication {
private static final Log logger = LogFactory.getLog(SpringApplication.class);
...
}
// LogFactory.java
public static Log getLog(String name) {
return LogAdapter.createLog(name);
}
// LoggerAdapter.java
private static class Slf4jAdapter {
public static Log createLocationAwareLog(String name) {
Logger logger = LoggerFactory.getLogger(name); // 여기!
return (logger instanceof LocationAwareLogger locationAwareLogger ?
new Slf4jLocationAwareLog(locationAwareLogger) : new Slf4jLog<>(logger));
}
public static Log createLog(String name) {
return new Slf4jLog<>(LoggerFactory.getLogger(name)); // 여기!
}
}
코드를 타고 들어가다 보면 LoggerAdapter의 메서드에서 LoggerFactory 클래스의 정적 메서드인 getLogger()
를 호출하는 것을 알 수 있다.
// LoggerFactory.java
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
ILoggerFactory
인터페이스 타입의 인스턴스를 가져오는 getILoggerFactory()
메서드를 호출한다.
// LoggerFactory.java
public static ILoggerFactory getILoggerFactory() {
return getProvider().getLoggerFactory();
}
static SLF4JServiceProvider getProvider() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization();
}
}
}
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITIALIZATION:
return PROVIDER;
case NOP_FALLBACK_INITIALIZATION:
return NOP_FALLBACK_SERVICE_PROVIDER;
...
}
처음 애플리케이션이 기동될 때는 초기화된 상태가 아니기 때문에 performInitialization()
을 호출하고, 이는 내부적으로 bind()
메서드를 호출한다.
private final static void bind() {
// (1)
List<SLF4JServiceProvider> providersList = findServiceProviders();
// (2)
reportMultipleBindingAmbiguity(providersList);
...
}
여기는 (1)과 (2)로 나누어 살펴보자.
아래 코드는 ServiceLoader를 사용해 SLF4JServiceProvider 인터페이스를 구현한 구현체들(Logback, Log4j2)을 찾아 리턴해 적절한 구현을 선택하도록 하는 코드이다.
static List<SLF4JServiceProvider> findServiceProviders() {
List<SLF4JServiceProvider> providerList = new ArrayList<>();
...
ServiceLoader<SLF4JServiceProvider> serviceLoader = getServiceLoader(classLoaderOfLoggerFactory);
Iterator<SLF4JServiceProvider> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
safelyInstantiate(providerList, iterator);
}
return providerList;
}
private static ServiceLoader<SLF4JServiceProvider> getServiceLoader(final ClassLoader classLoaderOfLoggerFactory) {
ServiceLoader<SLF4JServiceProvider> serviceLoader;
SecurityManager securityManager = System.getSecurityManager();
if(securityManager == null) {
// 여기!
serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class, classLoaderOfLoggerFactory);
} else {
final PrivilegedAction<ServiceLoader<SLF4JServiceProvider>> action = () -> ServiceLoader.load(SLF4JServiceProvider.class, classLoaderOfLoggerFactory);
serviceLoader = AccessController.doPrivileged(action);
}
return serviceLoader;
}
SLF4JServiceProvider 타입의 ServiceLoader를 가져오면 safelyInstantiate()
메서드를 호출한다.
private static void safelyInstantiate(List<SLF4JServiceProvider> providerList, Iterator<SLF4JServiceProvider> iterator) {
try {
SLF4JServiceProvider provider = iterator.next();
providerList.add(provider);
} catch (ServiceConfigurationError e) {
Util.report("A SLF4J service provider failed to instantiate:\n" + e.getMessage());
}
}
메서드의 끝에서 providerList를 확인해보면 아래와 같다. 예상대로 Logback, Log4j2를 모두 찾아 들고 있는 것을 알 수 있다.
ServiceLoader는 jar 파일에 포함된 인터페이스의 구현을 찾고, 여러 구현체들의 목록을 확인하고 가져오는데 사용할 수 있다.
// LoggerFactory.java
private static void reportMultipleBindingAmbiguity(List<SLF4JServiceProvider> providerList) {
if (isAmbiguousProviderList(providerList)) {
Util.report("Class path contains multiple SLF4J providers.");
for (SLF4JServiceProvider provider : providerList) {
Util.report("Found provider [" + provider + "]");
}
Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
}
}
// Util.java
static final public void report(String msg) {
System.err.println("SLF4J: " + msg);
}
두 개의 의존성이 존재하기 때문에 isAmbiguousProviderList()
는 true를 리턴하고, 아래의 여러 경고 메시지들이 콘솔에 출력되게 된다.
따라서, 두 개의 로깅 의존성을 추가한 상태에서 애플리케이션을 띄우면 아래와 같은 경고 메시지가 출력된다.