Files
llm-wiki/raw/errors/testcontainers-two-context-shared-datasource-close-2026-06-11.md

50 lines
3.2 KiB
Markdown

---
title: error / testcontainers-two-context-shared-datasource-close-2026-06-11
source_type: error-note
status: raw
related_branches: [feature-domain-event-outbox-contract]
related_projects: [ca-skeleton]
tags: [error, ca-skeleton, testing, testcontainers, spring, datasource, outbox]
created: 2026-06-11
status_label: resolved
---
# error: testcontainers-two-context-shared-datasource-close-2026-06-11
> Layer: `raw/errors/` — 작업 중 마주친 단일 실패·트러블슈팅 기록. 원본은 raw에 영구 보관한다.
## Parent / 부모
- [[raw/branch-notes/feature-domain-event-outbox-contract]] — `OutboxPublisherLeaderElectionContractTest`(2개 Spring context + 1000 rows SKIP LOCKED claim 계약 테스트) 인프라 작성 중 발생.
## 증상 / Symptom
- 에러 메시지 (원문 그대로):
```text
java.sql.SQLException: HikariDataSource HikariDataSource (HikariPool-1) has been closed.
```
- 발생 컨텍스트: `cd src && ./gradlew :app-bootstrap:test --tests '*Outbox*'` — 하나의 PostgreSQL Testcontainers 컨테이너를 공유하는 **두 개의 `AnnotationConfigApplicationContext`** 중 첫 번째를 `close()` 하자 두 번째 context 의 쿼리가 전부 실패.
- 재현 가능 여부: `always` (공유 DataSource 를 bean 으로 등록한 두 context 중 하나라도 닫으면).
## 재현 절차 / Reproduction
1. Testcontainers PG 컨테이너 1개에서 `HikariDataSource` 1개를 만들고, 이를 두 개의 `AnnotationConfigApplicationContext` 에 `registerBean(DataSource.class, () -> sharedDs)` 로 등록.
2. 두 context 로 동시 작업 후 첫 번째 context 를 `close()`.
3. 기대: 외부에서 생성한 DataSource 는 context 가 소유하지 않으므로 살아 있어야 함.
4. 실제: Spring 이 bean 의 추론된 destroy method(`close`)를 호출해 공유 풀이 닫힘 → 두 번째 context 의 모든 쿼리 실패.
## 원인 / Root cause
- Spring 의 `registerBean` 기본 동작은 **inferred destroy method** — bean 이 `close()`/`shutdown()` 을 가지면 context close 시 자동 호출한다. 외부 소유(externally-owned) 자원이라는 사실을 Spring 은 모른다.
## 해결 / Resolution
- DataSource bean definition 에 `beanDefinition.setDestroyMethodName("")` 을 지정해 destroy 추론을 끈다 (소유권은 테스트 support 클래스가 유지, 마지막에 직접 close).
- 같은 맥락에서 `LocalContainerEntityManagerFactoryBean` 대신 직접 `EntityManagerFactory` 를 등록하고 `ContextClosedEvent` listener 로 EMF 만 정리.
- 적용 위치: ca-tmpl `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/outbox/OutboxContainerTestSupport.java` (`actually-implemented`, `locally-verified` — 전체 `./gradlew check` 836/836 green).
## 동반 발견 (같은 테스트 인프라에서)
- 고정 Clock(t0) 으로 relay 를 돌리면서 row 는 `Instant.now()` 로 insert → `next_attempt_at <= :now` 술어가 전부 false 가 되어 published=0. 해결: row 와 relay 가 같은 t0 기반, relay clock 은 `t0.plusSeconds(1)` 버퍼.
- 병렬 Gradle 실행 2개가 같은 모듈 테스트를 돌리면 JUnit XML report 쓰기 경합으로 위양성 실패 — 검증 명령은 직렬화할 것.