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

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
feature-distributed-lock-contract
ca-skeleton
error
ca-skeleton
testing
spring-integration
jdbc-lock-registry
lifecycle
testcontainers
2026-06-13 resolved

error: spring-integration-defaultlockrepository-aftersingletons-null-template-2026-06-13

Layer: raw/errors/ — 작업 중 마주친 단일 실패·트러블슈팅 기록. 원본은 raw에 영구 보관한다.

Parent / 부모

증상 / 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 발생.
  • 재현 가능 여부: alwaysafterSingletonsInstantiated() 를 명시 호출하지 않으면.

재현 절차 / 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 확인 경로: JdbcLockRegistryDefaultLockRepositoryafterSingletonsInstantiated()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 컨텍스트 밖에서는 자동으로 호출되지 않는다. 직접 호출하지 않으면 readCommittedTransactionTemplatenull 인 채로 남는다.

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