Logger를 static final로 선언하는 이유
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으로 공유하는 것이 합리적