@Slf4j의 동작 방식

mangoo·2024년 1월 28일
0

SLF4J란?

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 변수가 선언된 상태라고 가정하고 설명한다.


Logback, Log4j2 의존성이 존재하는 경우

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() 메서드를 호출한다.

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)로 나누어 살펴보자.

(1) findServiceProviders()

아래 코드는 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 파일에 포함된 인터페이스의 구현을 찾고, 여러 구현체들의 목록을 확인하고 가져오는데 사용할 수 있다.

(2) reportMultipleBindingAmbiguity()

// 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를 리턴하고, 아래의 여러 경고 메시지들이 콘솔에 출력되게 된다.

따라서, 두 개의 로깅 의존성을 추가한 상태에서 애플리케이션을 띄우면 아래와 같은 경고 메시지가 출력된다.

0개의 댓글