5.2 KiB
title, source_type, status, related_branches, related_projects, tags, created, status_label
| title | source_type | status | related_branches | related_projects | tags | created | status_label | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| error / spring-integration-defaultlockrepository-aftersingletons-null-template-2026-06-13 | error-note | raw |
|
|
|
2026-06-13 | resolved |
error: spring-integration-defaultlockrepository-aftersingletons-null-template-2026-06-13
Layer:
raw/errors/— 작업 중 마주친 단일 실패·트러블슈팅 기록. 원본은 raw에 영구 보관한다.
Parent / 부모
- raw/branch-notes/feature-distributed-lock-contract —
DistributedLockProviderContractTest(D3 mutual exclusion + D5 lease expiry Testcontainers 계약 테스트) 작성 중 발생.
증상 / Symptom
- 에러 메시지 (원문):
org.springframework.dao.CannotAcquireLockException: Cannot acquire lock; nested exception is java.lang.NullPointerException: Cannot invoke "org.springframework.transaction.support.TransactionTemplate.execute( org.springframework.transaction.support.TransactionCallbackWithoutResult)" because "this.readCommittedTransactionTemplate" is null - 발생 컨텍스트:
DistributedLockProviderContractTest— Spring 컨텍스트 없이DefaultLockRepository를 직접 인스턴스화하여 두 개의JdbcLockRegistry(두 앱 인스턴스 시뮬레이션)를 만들어 Testcontainers PG DataSource 에 연결. 첫 번째 registry 의tryLock()호출 시 NPE 발생. - 재현 가능 여부:
always—afterSingletonsInstantiated()를 명시 호출하지 않으면.
재현 절차 / Reproduction
DefaultLockRepository repo = new DefaultLockRepository(dataSource);
repo.setTimeToLive((int) ttl.toMillis());
repo.setCheckDatabaseOnStart(false);
repo.setTransactionManager(new DataSourceTransactionManager(dataSource));
repo.afterPropertiesSet();
// afterSingletonsInstantiated() 누락
repo.start();
JdbcLockRegistry registry = new JdbcLockRegistry(repo);
Lock lock = registry.obtain("test-key");
lock.tryLock(1, TimeUnit.SECONDS); // ← NullPointerException here
원인 / Root cause
DefaultLockRepository 는 두 개의 lifecycle 인터페이스를 구현한다:
| 인터페이스 | 메서드 | 구현 내용 |
|---|---|---|
InitializingBean |
afterPropertiesSet() |
필드 null 체크, JdbcTemplate 생성 |
SmartInitializingSingleton |
afterSingletonsInstantiated() |
readCommittedTransactionTemplate 생성 |
Spring 컨텍스트 내부에서는 모든 singleton bean 이 instantiate 된 뒤 컨테이너가 자동으로 SmartInitializingSingleton.afterSingletonsInstantiated() 를 호출한다. 그러나 컨텍스트 없이 직접 인스턴스화할 때 afterSingletonsInstantiated() 는 호출되지 않는다.
결과적으로 readCommittedTransactionTemplate 필드가 null 로 남고, 첫 tryLock() 호출 시 NPE → CannotAcquireLockException 으로 래핑되어 던져진다.
Spring Integration 6.5 source 확인 경로: JdbcLockRegistry → DefaultLockRepository → afterSingletonsInstantiated() → this.readCommittedTransactionTemplate = new TransactionTemplate(...).
해결 / Resolution
Spring 컨텍스트 외부에서 DefaultLockRepository 를 사용할 때는 다음 순서로 명시 초기화:
private static DefaultLockRepository buildRepository(DataSource dataSource, Duration ttl) {
DefaultLockRepository repo = new DefaultLockRepository(dataSource);
repo.setTimeToLive((int) ttl.toMillis());
repo.setCheckDatabaseOnStart(false);
// 1. TransactionManager 먼저 설정 (afterPropertiesSet 에서 null 체크 통과용)
repo.setTransactionManager(new DataSourceTransactionManager(dataSource));
// 2. InitializingBean lifecycle
repo.afterPropertiesSet();
// 3. SmartInitializingSingleton lifecycle — readCommittedTransactionTemplate 생성
repo.afterSingletonsInstantiated();
// 4. Lifecycle.start() — Spring Integration SmartLifecycle
repo.start();
return repo;
}
핵심: afterSingletonsInstantiated() 는 Spring 컨텍스트 밖에서는 자동으로 호출되지 않는다. 직접 호출하지 않으면 readCommittedTransactionTemplate 이 null 인 채로 남는다.
유사 패턴 / Related patterns
SmartInitializingSingleton을 구현하는 다른 Spring 컴포넌트들도 동일한 위험을 가진다:DefaultMessageListenerContainer,KafkaListenerEndpointRegistry등. 컨텍스트 없이 직접 사용 시 항상afterSingletonsInstantiated()명시 호출 여부를 확인.SmartLifecycle.start()는 별도 —afterSingletonsInstantiated()이후에 호출해야 한다.
오답 / Anti-pattern tried
// setTransactionManager 추가만으로는 해결 안 됨:
repo.setTransactionManager(new DataSourceTransactionManager(dataSource));
repo.afterPropertiesSet();
repo.start(); // afterSingletonsInstantiated 누락 — 여전히 NPE
setTransactionManager() 는 afterPropertiesSet() 의 null 체크를 통과하는 데 필요하지만 readCommittedTransactionTemplate 생성과는 무관하다. 해결의 핵심은 afterSingletonsInstantiated() 호출이다.