Java

Logger를 static final로 선언하는 이유

dev_SiWoo 2025. 10. 2. 10:50

1. 문제 제기

흔히 보는 Logger 선언 패턴

@Component
public class SignlDataParser {
    private static final Logger LOG = LoggerFactory.getLogger(SignlDataParser.class);
}

 

의문점

- Spring `@Component`는 기본적으로 싱글톤
- 싱글톤이면 인스턴스가 1개만 생성됨
- 그럼 굳이 `static`으로 선언할 필요가 있을까?

 

// 이렇게 해도 되지 않나?
@Component
public class SignlDataParser {
    private final Logger log = LoggerFactory.getLogger(getClass());
}

 

 

 

2. LoggerFactory의 캐싱 메커니즘

2.1 내부 구조
LoggerFactory는 내부적으로 `ConcurrentHashMap`을 사용해 Logger를 캐싱함

// Logback의 실제 구현 (단순화)
public class LoggerContext {
    // Logger 인스턴스를 이름별로 캐싱
    private ConcurrentHashMap<String, Logger> loggerCache = new ConcurrentHashMap<>();

    public Logger getLogger(String name) {
        // 1. 캐시에서 먼저 찾기
        Logger logger = loggerCache.get(name);

        if (logger != null) {
            return logger;  // 캐시 히트
        }

        // 2. 없으면 새로 생성 후 캐시에 저장
        synchronized (this) {
            logger = new Logger(name);
            loggerCache.put(name, logger);
        }

        return logger;
    }
}

 

2.2 캐싱의 효과

같은 클래스를 여러 번 호출해도 Logger 인스턴스는 1개만 생성됨

 

Logger log1 = LoggerFactory.getLogger("MyClass");
Logger log2 = LoggerFactory.getLogger("MyClass");
Logger log3 = LoggerFactory.getLogger("MyClass");

System.out.println(log1 == log2);  // true
System.out.println(log2 == log3);  // true

 

3. static vs non-static 비교

3.1 메모리 관점

Case 1: static final (싱글톤 환경)

@Component  // 인스턴스 1개만 생성
public class Parser {
    private static final Logger LOG = LoggerFactory.getLogger(Parser.class);
}

메모리 상태:
- Logger 인스턴스: 1개
- 저장 위치: Method Area

 

 

Case 2: non-static final (싱글톤 환경)

@Component  // 인스턴스 1개만 생성
public class Parser {
    private final Logger log = LoggerFactory.getLogger(getClass());
}

 

메모리 상태:
- Logger 인스턴스: 1개 (LoggerFactory 캐시에 저장)
- 저장 위치: Heap 영역 (인스턴스 필드 참조)

결론: 싱글톤 환경에서는 메모리상으로는 차이가 거의 없음

 

 

3.2 메서드 호출 오버헤드

`getLogger()` 메서드를 호출하면 내부적으로 다음 작업이 수행됨

public Logger getLogger(String name) {
    // 1. HashMap 조회
    Logger logger = loggerCache.get(name);  // O(1)이지만 비용 존재

    // 2. null 체크
    if (logger != null) {
        return logger;
    }

    // 3. 동기화 블록 (첫 호출 시)
    synchronized (this) {
        // ...
    }
}

 

캐시 히트라도 다음 비용이 발생:
- 메서드 호출 스택 프레임 생성
- HashMap 조회 연산
- null 체크 분기
- 메서드 리턴

static final은 이 모든 과정을 스킵

 

 

3.3 바이트코드 레벨에서의 비교

static final 바이트코드

// Java 코드
private static final Logger LOG = LoggerFactory.getLogger(Parser.class);

// 컴파일된 바이트코드 (단순화)
static {
    // <clinit> 메서드 (클래스 초기화)
    0: ldc           #1  // class Parser
    2: invokestatic  #2  // LoggerFactory.getLogger
    5: putstatic     #3  // Parser.LOG
    8: return
}
// 클래스 로딩 시 1번만 실행되고 끝

 

 

non-static final 바이트코드

// Java 코드
private final Logger log = LoggerFactory.getLogger(getClass());

// 컴파일된 바이트코드 (단순화)
public Parser() {
    // <init> 메서드 (인스턴스 초기화)
    0: aload_0
    1: invokespecial #1  // Object.<init>
    4: aload_0
    5: invokevirtual #2  // getClass()
    8: invokestatic  #3  // LoggerFactory.getLogger
   11: putfield      #4  // Parser.log
   14: return
}
// new Parser() 할 때마다 실행됨

 

 

4. 정리

1. 성능 : 불필요한 메서드 호출 제거


2. 의미 : Logger는 클래스의 도구, 인스턴스 상태 아님

       모든 인스턴스가 같은 Logger 사용
         - 인스턴스마다 다른 Logger 필요 없음
         - static으로 공유하는 것이 합리적