Files
llm-wiki/raw/errors/spring-integration-defaultlockrepository-aftersingletons-null-template-2026-06-13.md

101 lines
5.2 KiB
Markdown

---
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()` 호출이다.