고성능 JPA & Hibernate - Intergration Testing With Docker
H2 대신 tmpfs?
Jul 07, 2025
고성능 JPA & Hibernate (High-Performance Java Persistence)를 수강하고 개인적으로 정리한 글 입니다.
데이터 접근 로직은 비즈니스 로직만큼이나 중요하며, 이를 철저히 검증하는 것은 안정적인 서비스 운영에 필수적입니다. 관계형 데이터베이스(RDBMS)에서 테스트를 실행하는 것은 속도와 복잡성 때문에 어려움이 따르며, 이러한 문제를 해결하기 위해 H2 같은 인메모리 데이터베이스가 널리 사용되어 왔습니다.
하지만 인메모리 데이터베이스는 개발/테스트 환경과 운영 환경 간의 간극을 유발할 수 있습니다. 예를 들어, H2는 SQL 문법이나 기능 면에서 실제 운영 DB(예: PostgreSQL)와 미묘한 차이가 있을 수 있으며, 이는 테스트에서는 통과했지만 실제 운영 환경에서 실패하는 예측 불가능한 상황으로 이어질 수 있습니다.
이러한 딜레마를 해결하고 개발/테스트 환경과 운영 환경 간의 격차를 최소화하면서도 고성능 테스트를 수행하기 위한 효과적인 방법이 바로 Docker와
tmpfs
를 활용한 통합 테스트입니다.Docker와 tmpfs
를 사용해야 하는 이유

- 운영 환경과의 높은 일치성 (Production Parity):
- Docker: Docker를 사용하면 실제 운영 환경에서 사용할 것과 동일한 버전의 데이터베이스(PostgreSQL) 컨테이너를 테스트 환경에 구축할 수 있습니다. 이는 H2와 같은 인메모리 DB 사용 시 발생할 수 있는 SQL 문법, 함수 동작, 데이터 타입 등의 불일치 문제를 원천적으로 해결하여 "테스트는 성공했는데 운영에서 에러"나는 상황을 방지합니다.
tmpfs
:tmpfs
(Temporary File System)는 리눅스의 RAM 기반 파일 시스템으로, 데이터베이스의 데이터 디렉터리(e.g., /var/lib/postgresql/data
)를 메모리에 직접 매핑합니다. 이를 통해 데이터베이스의 모든 I/O 작업이 디스크가 아닌 초고속 RAM에서 이루어지므로, 일반적인 디스크 기반 DB의 I/O 병목 현상을 제거하고 인메모리 DB에 버금가는, 혹은 그 이상의 속도를 구현할 수 있습니다.
- 탁월한 성능 (H2를 능가하는):
tmpfs
는 실제 DB 엔진을 사용하면서도 디스크 I/O를 없애기 때문에, 순수한 데이터베이스 연산 성능을 극대화합니다. 특히 대량 데이터 처리 시 그 효율성이 더욱 두드러집니다.
성능 비교
여기서 기본적인 환경을 구성하여, 아래의 간단한 통합 테스트를 통해 H2와 tmpfs의 차이를 확인해보겠습니다.
H2 Code
@SpringBootTest
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
"spring.datasource.driver-class-name=org.h2.Driver",
"spring.datasource.username=sa",
"spring.datasource.password=",
"spring.jpa.hibernate.ddl-auto=create-drop",
"spring.jpa.database-platform=org.hibernate.dialect.H2Dialect",
// 배치 설정 강화
"spring.jpa.properties.hibernate.jdbc.batch_size=500",
"spring.jpa.properties.hibernate.order_inserts=true",
"spring.jpa.properties.hibernate.order_updates=true",
"spring.jpa.properties.hibernate.jdbc.batch_versioned_data=true",
// H2용 ID 생성 전략 오버라이드
"spring.jpa.properties.hibernate.id.new_generator_mappings=false"
})
@DisplayName("H2 인메모리 데이터베이스 통합테스트")
public class H2IntegrationTest {
private static final Logger log = LoggerFactory.getLogger(H2IntegrationTest.class); // ★ 클래스명 수정
@PersistenceContext
private EntityManager entityManager;
@Autowired
private PostRepository postRepository;
@Autowired
private PostCommentRepository commentRepository;
@Test
@DisplayName("H2 인메모리 데이터베이스 연결 테스트")
void testH2InMemoryConnection() {
log.info("H2-INMEMORY 연결 테스트 시작");
SQLStatementCountValidator.reset();
// 간단한 연결 테스트
long count = postRepository.count();
assertThat(count).isEqualTo(0);
log.info("탐색된 카운트 : {}", count);
}
@Test
@Transactional
@DisplayName("Post CRUD TEST")
void testInsertAndSelect() {
log.info("CRUD Test 시작");
Post post = Post
.builder()
.title("CRUD Title")
.content("CRUD Content")
.createdDate(LocalDateTime.now())
.build();
Post savedPost = postRepository.save(post);
assertThat(savedPost.getId()).isNotNull();
entityManager.flush();
Optional<Post> getPostById = postRepository.findById(savedPost.getId());
assertThat(getPostById.isPresent()).isTrue();
assertThat(getPostById.get().getTitle()).isEqualTo(post.getTitle());
entityManager.flush();
Post post2 = getPostById.get();
post2.setTitle("Update");
postRepository.save(post2);
Optional<Post> getPostById2 = postRepository.findById(post2.getId());
assertThat(getPostById2.isPresent()).isTrue();
assertThat(getPostById2.get().getTitle()).isEqualTo(post2.getTitle());
entityManager.flush();
postRepository.delete(post);
Optional<Post> getPostById3 = postRepository.findById(post.getId());
assertThat(getPostById3.isPresent()).isFalse();
entityManager.flush();
log.info("CRUD Test 종료");
}
@Test
@Transactional
@DisplayName("H2 성능 테스트 - 대량 데이터 처리")
void testH2PerformanceWithBulkData() {
log.info("H2-INMEMORY Bulk 처리 시작");
long startTime = System.currentTimeMillis();
List<Post> postList = new ArrayList<>(500);
// 대량 데이터 생성 (500개 포스트, 각각 3개 댓글)
for (int i = 1; i <= 50000; i++) {
Post post = Post.builder()
.title("H2 성능 테스트 포스트 " + i)
.content("H2 인메모리 데이터베이스 성능 테스트용 내용 " + i)
.createdDate(LocalDateTime.now())
.build();
PostComment comment = PostComment.builder()
.review("포스트 " + i + "의 댓글 " + i)
.build();
post.addComment(comment);
postList.add(post);
}
postRepository.saveAll(postList);
entityManager.flush();
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// 결과 검증
long totalPosts = postRepository.count();
long totalComments = commentRepository.count();
assertThat(totalPosts).isEqualTo(50000);
assertThat(totalComments).isEqualTo(50000);
log.info("H2-INMEMORY 포스트 수: {}", totalPosts);
log.info("H2-INMEMORY 댓글 수: {}", totalComments);
log.info("H2-INMEMORY 총 처리 시간: {}ms", duration);
long searchStartTime = System.currentTimeMillis();
List<Post> searchResults = postRepository.findByTitleContaining("성능 테스트");
entityManager.flush();
long searchEndTime = System.currentTimeMillis();
long searchDuration = searchEndTime - searchStartTime;
assertThat(searchResults).hasSize(50000);
log.info("H2-INMEMORY 대량 검색 테스트 완료: {} 건 조회 (검색 시간: {}ms)", searchResults.size(), searchDuration);
log.info("H2-INMEMORY 성능 테스트 완료");
}
}
tmpts(Postgresql) Code
@SpringBootTest
@Testcontainers
@DisplayName("tmpfs를 활용한 PostgreSQL 통합테스트")
class TmpfsPostgreSQLIntegrationTest {
private static final Logger log = LoggerFactory.getLogger(TmpfsPostgreSQLIntegrationTest.class);
@PersistenceContext
private EntityManager entityManager;
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
.withTmpFs(Map.of("/var/lib/postgresql/data", "rw,noexec,nosuid,size=1g")); // tmpfs 설정
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
registry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop");
registry.add("spring.jpa.properties.hibernate.jdbc.batch_size", () -> 500);
registry.add("spring.jpa.properties.hibernate.order_inserts", () -> true);
registry.add("spring.jpa.properties.hibernate.order_updates", () -> true);
registry.add("spring.jpa.properties.hibernate.jdbc.batch_versioned_data", () -> true);
}
@Autowired
private PostRepository postRepository;
@Autowired
private PostCommentRepository commentRepository;
@Autowired
private TagRepository tagRepository;
@Test
@DisplayName("tmpfs PostgreSQL 컨테이너 연결 테스트")
void testTmpfsPostgreSQLConnection() {
assertThat(postgres.isRunning()).isTrue();
log.info("[TMPFS-POSTGRESQL] 컨테이너가 성공적으로 실행 중입니다");
}
@Test
@Transactional
@DisplayName("tmpts 성능 테스트 - 대량 데이터 처리")
void testTmpfsPerformanceWithBulkData() {
log.info("tmpts Postgresql Bulk 처리 시작");
long startTime = System.currentTimeMillis();
List<Post> postList = new ArrayList<>(50000);
// 대량 데이터 생성 (500개 포스트, 각각 3개 댓글)
for (int i = 1; i <= 50000; i++) {
Post post = Post.builder()
.title("H2 성능 테스트 포스트 " + i)
.content("H2 인메모리 데이터베이스 성능 테스트용 내용 " + i)
.createdDate(LocalDateTime.now())
.build();
PostComment comment = PostComment.builder()
.review("포스트 " + i + "의 댓글 " + i)
.build();
post.addComment(comment);
postList.add(post);
}
postRepository.saveAll(postList);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// 결과 검증
long totalPosts = postRepository.count();
long totalComments = commentRepository.count();
assertThat(totalPosts).isEqualTo(50000);
assertThat(totalComments).isEqualTo(50000);
log.info("tmpts Postgresql 포스트 수: {}", totalPosts);
log.info("tmpts Postgresql 댓글 수: {}", totalComments);
log.info("tmpts Postgresql 총 처리 시간: {}ms", duration);
long searchStartTime = System.currentTimeMillis();
List<Post> searchResults = postRepository.findByTitleContaining("성능 테스트");
long searchEndTime = System.currentTimeMillis();
long searchDuration = searchEndTime - searchStartTime;
assertThat(searchResults).hasSize(50000);
log.info("tmpts Postgresql 대량 검색 테스트 완료: {} 건 조회 (검색 시간: {}ms)", searchResults.size(), searchDuration);
log.info("tmpts Postgresql 성능 테스트 완료");
}
}
데이터베이스 종류 | 테스트 데이터 양 (포스트/댓글) | 총 처리 시간 | 포스트 당 평균 처리 시간 |
H2 (인메모리 DB) | 5,000 / 5,000 (총 1만 건) | 1078ms | 약 0.2156ms/post |
H2 (인메모리 DB) | 50,000 / 50,000 (총 10만 건) | 10498ms | 약 0.210ms/post |
PostgreSQL ( tmpfs ) | 5,000 / 5,000 (총 1만 건) | 210ms | 약 0.042ms/post |
PostgreSQL ( tmpfs ) | 50,000 / 50,000 (총 10만 건) | 988ms | 약 0.01976ms/post |
결과
1만 건 데이터 처리 시,
tmpfs
를 사용한 PostgreSQL이 H2 인메모리 DB보다 약 5배 이상 (0.2156ms → 0.042ms) 빠른 성능을 보였습니다. 규모가 커질수록 더욱 효율적이었는데, 특히 10만 건의 대량 데이터 처리 시, tmpfs
PostgreSQL의 포스트 당 평균 처리 시간은 0.01976ms로, 1만 건 처리 시(0.042ms)보다 약 2배 이상 더 빨라지는 경향을 보였습니다. 이는 대량 배치 인서트에서 tmpfs
와 실제 PostgreSQL 엔진의 조합이 고정적 오버헤드를 더 줄일 수 있고 배치 효율이 더 좋아짐을 의미합니다.반면 H2 인메모리는 데이터 규모가 10배 증가해도 포스트 당 평균 처리 시간이 큰 변화 없이 유지되어, 특정 시점 이후로는 효율성 개선이 제한적임을 시사합니다.
Docker와
tmpfs
를 활용한 통합 테스트는 운영 환경과의 높은 일치성을 보장하면서도, 인메모리 DB를 넘어서는 효율을 보여줍니다. 수 많은 통합 테스트를 실행할 떄 실행 시간이 주는 생산성 저하부분을 생각한다면 인메모리 DB대신 tmpfs나 Docker를 활용한 방법이 좋은 대안이 될 수 있습니다.한계
이렇게 실제 db 엔진을 기반으로 실행하는 테스트는 특정 환경에서의 실제적인 성능을 검증하기에 용이할 수 있지만, 다음과 같은 한계를 가지고 있습니다.
- 디스크 I/O를 고민해야 합니다.
- 운영 환경의 데이터베이스는 일반적으로 SSD나 HDD와 같은 영구 저장장치에서 작동하며, 디스크 I/O는 실제 성능의 가장 큰 병목 중 하나입니다.
tmpfs
는 이 디스크 I/O를 완전히 제거하여 최고 성능을 보여주지만, 이는 실제 운영 환경의 I/O 지연을 반영하지 않습니다. - 따라서 이 테스트는 "디스크 I/O가 전혀 없을 때의 최적 성능"을 보여줄 뿐, 실제 디스크 환경에서의 성능과는 차이가 있을 수 있습니다.
- 네트워크 지연을 고민해야 합니다.
Testcontainers
는 로컬 환경에서 Docker 컨테이너를 실행하므로, 애플리케이션과 DB 간의 네트워크 지연이 거의 없습니다. 운영 환경에서는 애플리케이션 서버와 DB 서버가 물리적으로 분리되어 있어 훨씬 큰 네트워크 지연이 발생할 수 있습니다.
- 동시성
- 이 테스트는 아마도 높은 확률로 제한된 동시성아래에서 실행될 확률이 높습니다. 실제 환경에서는 많은 사용자와 트랜잭션 이 동시에 발생하며 Lock이나 경쟁상태가 발생할 수 있습니다. tmpfs같은 도구를 사용한다고 하더라도, 동시성 환경에 대한 충분한 고민이 필요합니다.
Docker와 tmpfs를 활용한 통합 테스트는 최적화된 데이터 접근이 실제 데이터베이스 엔진에서 발휘할 수 있는 최대 효용을 측정하는 데 좋습니다. 개발 단계에서 데이터 접근 효율성을 올리고, H2와 같은 인메모리 DB 사용 시의 기능적 불일치 문제를 해결하는 데 매우 유리합니다.
하지만 운영 환경의 실제 성능을 종합적으로 고려하면, 디스크 I/O, 네트워크 지연, 높은 동시성, 다양한 워크로드 등을 고려한 별도의 성능 테스트 환경(스테이징 또는 전용 성능 테스트 서버)에서 부하 테스트(Load Test)를 수행하는 것이 권장됩니다.
즉, 이상적인 환경에서 얼마나 빠르게 작동할 수 있는가 혹은 H2와 같은 인메모리 DB를 사용한 통합테스트의 속도가 너무나도 오래걸려 생산성이 저하됐을 때 이를 검증하거나 개선하기 위하여 사용하는데 적합합니다.
예를들어, 운영환경에서 코드가 초당 몇 건의 요청을 처리할 수 있는지 같은 건 실제 메트릭을 기반으로 추론하여 추가적인 부하 테스트를 실행하는 것이 좋습니다.
Share article