101 lines
5.2 KiB
Markdown
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()` 호출이다.
|