--- 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 쓰기 경합으로 위양성 실패 — 검증 명령은 직렬화할 것.