--- title: error / spring-integration-defaultlockrepository-aftersingletons-null-template-2026-06-13 source_type: error-note status: raw related_branches: [feature-distributed-lock-contract] related_projects: [ca-skeleton] tags: [error, ca-skeleton, testing, spring-integration, jdbc-lock-registry, lifecycle, testcontainers] created: 2026-06-13 status_label: 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 - 에러 메시지 (원문): ```text 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 ```java 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` 를 사용할 때는 다음 순서로 명시 초기화: ```java 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 ```java // setTransactionManager 추가만으로는 해결 안 됨: repo.setTransactionManager(new DataSourceTransactionManager(dataSource)); repo.afterPropertiesSet(); repo.start(); // afterSingletonsInstantiated 누락 — 여전히 NPE ``` `setTransactionManager()` 는 `afterPropertiesSet()` 의 null 체크를 통과하는 데 필요하지만 `readCommittedTransactionTemplate` 생성과는 무관하다. 해결의 핵심은 `afterSingletonsInstantiated()` 호출이다.