JPA slice와 Testcontainers PostgreSQL
H2 같은 인메모리 데이터베이스는 빠르지만 PostgreSQL의 UUID, enum, 인덱스, 잠금, SQL 방언을 완전히 대신하지 못합니다. 운영 DB가 PostgreSQL이라면 저장 어댑터의 핵심 계약도 실제 PostgreSQL 프로세스에서 확인해야 합니다.
Testcontainers는 테스트마다 수동으로 포트를 정하고 DB를 설치하는 대신, 격리된 컨테이너를 시작하고 종료합니다. 이 테스트는 도메인 규칙이 아니라 Flyway 스키마와 JPA 매핑이 같은 테이블 계약을 공유하는지를 묻습니다.
DB 방언과 저장 제약은 인메모리 대역으로 보장할 수 없다
@Container가 PostgreSQL 생명주기를 테스트에 묶고, @ServiceConnection은 컨테이너의 동적 JDBC URL과 자격 증명을 Boot의 DataSource 설정으로 전달합니다. 애플리케이션은 그 연결에 Flyway migration을 적용한 뒤 같은 스키마로 JPA 작업을 수행합니다.
@AutoConfigureTestDatabase(replace = NONE)을 빠뜨리면 테스트 slice가 컨테이너 DataSource를 내장 DB로 교체할 수 있습니다. 테스트가 성공하더라도 실제 방언을 검증하지 못한 상태가 되므로 연결 대상을 명시합니다.
ServiceConnection으로 컨테이너를 연결한다
필요한 테스트 모듈을 추가하고, 저장 후 즉시 flush해 SQL과 제약 조건을 테스트 메서드 안에서 실행시킵니다. flush를 미루면 실패가 테스트 종료 시점으로 밀려 어느 행위가 원인이었는지 흐려질 수 있습니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>package com.andongmin.studylog.session;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.LocalDate;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.postgresql.PostgreSQLContainer;
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class StudySessionRepositoryTest {
@Container
@ServiceConnection
static final PostgreSQLContainer postgres =
new PostgreSQLContainer("postgres:18-alpine");
@Autowired
StudySessionJpaRepository repository;
@Test
void storesSessionThroughFlywaySchema() {
UUID id = UUID.randomUUID();
repository.saveAndFlush(new StudySessionEntity(id, "Spring", "JPA",
LocalDate.of(2026, 7, 13), 90));
assertThat(repository.findById(id)).isPresent();
}
}Docker 연결 전에 실패하면 컨테이너 실행 환경을, migration 단계에서 실패하면 SQL 이력과 checksum을, insert에서 실패하면 Entity–column 매핑과 DB 제약을 확인합니다. 한 테스트 안에서도 멈춘 단계가 조사 범위를 알려 줍니다.
migration과 JPA 매핑이 같은 스키마를 쓰는지 확인한다
테스트 로그에서 PostgreSQL 컨테이너가 시작되고 Flyway가 migration을 적용한 뒤 insert와 select가 성공하는 순서를 확인합니다.
./mvnw -q -Dtest=StudySessionRepositoryTest testTestcontainers 2의 PostgreSQL 18에서 Flyway migration과 JPA insert/select가 모두 성공해야 합니다.컨테이너 테스트는 값비싼 경계에만 사용한다
순수 규칙은 단위 테스트에 남기고 SQL, 제약, 격리 수준처럼 실제 DB가 답해야 하는 질문만 컨테이너로 확인합니다.
저장 어댑터까지 독립적으로 검증했으니 마지막에는 실제 서버에서 보안, REST, GraphQL, JPA가 연결되는 사용자 흐름을 한 번만 통합 검증합니다.