1. 들어가며
이전 포스트에서 엔티티를 구현하는 과정을 거쳤다. 물론 22개의 ERD에 따라 모든 엔티티를 구현하지는 않았다. 우리가 기능단위로 개발을 진행하니 구현이 필요할 때 작업할 예정이다. 그보다 이번에는 DataJPA를 사용하는 Repository 계층을 구현하고 테스트를 작성하였던 과정과 그 과정에서 겪었던 문제들과 해결방법을 소개하려고 한다.
2. 기술 선정: 왜 Spring Data JPA인가?
프로젝트의 핵심은 유저와 디지털 카드 데이터를 효율적으로 관리하는 것이다. 이를 위해 Spring Data JPA를 선택했다.
JPA (Java Persistence API)란?
JPA는 자바 애플리케이션에서 관계형 데이터베이스(RDBMS)를 사용하는 방식을 정의한 인터페이스(표준 명세)다. 과거에는 SQL을 직접 하나하나 작성해야 했지만, JPA를 사용하면 자바 객체(Entity)와 DB 테이블을 매핑하여 객체 지향적으로 데이터를 관리할 수 있다.
Spring Data JPA를 사용하는 이유
순수 JPA만 사용해도 되지만, Spring Data JPA는 이를 한 단계 더 추상화하여 개발 생산성을 극대화해 준다.
- 반복 코드 제거: save(), findAll(), findById() 같은 기본적인 CRUD 메서드를 미리 구현해 두었다.
- 쿼리 메서드: findByEmail(String email)처럼 메서드 이름만 잘 지으면 알아서 SQL을 생성해 준다.
- 유지보수성: SQL이 코드에 섞이지 않아 비즈니스 로직에 집중할 수 있다.
이러한 장점 덕분에 UserRepository 인터페이스를 단 몇 줄로 구현할 수 있었다.
import com.creators.photocard.member.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByNickname(String nickname);
}
3. 첫 번째 난관: H2 없는 @DataJpaTest
Repository를 만들었으니, 제대로 동작하는지 테스트가 필요했다. Spring Boot는 JPA 컴포넌트만 빠르게 테스트할 수 있는 @DataJpaTest 어노테이션을 제공한다.
하지만 테스트를 실행하자마자 에러가 발생했다.
Failed to replace DataSource with an embedded database for tests.
문제의 원인
@DataJpaTest는 기본적으로 In-Memory Database(H2)를 사용하여 테스트를 수행하도록 설계되어 있다. 빠르고 간편하기 때문이다. 하지만 우리 프로젝트는 PostgreSQL을 메인 DB로 채택했고, build.gradle에 H2 라이브러리를 아예 추가하지 않았다.
스프링 부트 입장에서는 "테스트니까 H2로 갈아 끼우려고 했는데, 라이브러리가 없네?" 라며 에러를 뱉은 것이다.
4. 임시 해결책과 한계: 로컬 DB 연결 (Replace.NONE)
H2를 설치해서 해결할 수도 있었지만, "실제 운영 환경(PostgreSQL)과 다른 DB로 테스트하는 것이 의미가 있을까?" 하는 의문이 들었다. 문법 차이(Dialect)로 인해 테스트는 통과하고 배포 후 에러가 터지는 상황을 피하고 싶었다.
그래서 스프링에게 "내장 DB를 찾지 마"라고 설정을 변경했다.
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryTest { ... }
이렇게 하면 내 로컬 PC에 설치된 PostgreSQL에 접속하여 테스트를 수행한다. 일단 테스트는 성공했다.
하지만 이건 정답이 아니다
이 방식은 치명적인 단점이 있다.
- 환경 종속성: 내 컴퓨터에서는 되지만, DB가 설치되지 않은 팀원의 컴퓨터나 CI/CD 서버(Github Actions)에서는 테스트가 실패한다.
- 데이터 오염: 테스트 도중 실수로 데이터를 지우지 않으면, 로컬 개발 데이터와 섞여버릴 위험이 있다.
결국 "어디서 실행하든 동일한 환경을 제공하는" 방법이 필요했다.
5. Testcontainers 도입
이 문제를 해결하기 위해 Testcontainers를 도입했다. Testcontainers는 Java 코드에서 Docker 컨테이너를 제어할 수 있게 해주는 라이브러리다. 테스트 코드가 실행될 때 도커로 PostgreSQL을 띄우고, 테스트가 끝나면 자동으로 파괴한다.
package com.creators.photocard.member.repository;
import com.creators.photocard.global.config.JpaConfig;
import com.creators.photocard.member.entity.User; // User 엔티티 import (패키지명 확인 필요)
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Import;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@Testcontainers // 1. Testcontainers 기능 활성화
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 2. "내장 H2 쓰지 마! 내가 알아서 연결할게"
@Import(JpaConfig.class) // 3. Auditing(자동 시간 저장) 설정 불러오기
class UserRepositoryTest {
// 4. 도커 컨테이너 정의 (static으로 선언하여 테스트 간 공유)
// "postgres:16" 부분은 로컬에서 사용 중인 버전과 맞추는 게 좋습니다.
@Container
@ServiceConnection // 5. 핵심! 이 컨테이너의 접속 정보(URL, ID, PW)를 스프링에 자동 주입
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Autowired
UserRepository userRepository;
@Test
@DisplayName("Testcontainers가 정상 작동하여 데이터를 저장한다")
void saveAndFindTest() {
// given
User newUser = new User();
// 엔티티 필드 설정 (프로젝트 상황에 맞게 수정해주세요)
// newUser.setEmail("test@test.com");
// newUser.setName("tester");
// when
User savedUser = userRepository.save(newUser);
// then
assertThat(savedUser).isNotNull();
// Auditing이 잘 작동했다면 생성일이 null이 아니어야 함
// assertThat(savedUser.getCreatedAt()).isNotNull();
// 눈으로 확인 (콘솔 로그)
System.out.println(">>> 연결된 DB URL: " + postgres.getJdbcUrl());
}
}
위 코드는 TestContainers 적용 후 코드다.
의존성 설정 (Spring Boot 4.0.1 호환성 이슈)
설정 과정에서 꽤 애를 먹었다. 인터넷에 있는 일반적인 설정을 넣었더니 의존성을 찾지 못하는 문제가 발생했다. 원인은 Spring Boot 4.0.1 환경에서 일부 라이브러리 버전을 자동으로 매핑하지 못하는 것이었다.
검색을 통해, 아래와 같이 버전을 명시하는 방식으로 해결했다.
[Spring] TestContainers를 통한 테스트 환경 구축
Springboot 3.1.0 버전부터 spring-boot-testcontainers 모듈을 통해 TestContainers을 정식 지원하게 되었다.기존에는 프로덕션 데이터베이스에 독립적인 테스트 코드를 짜기 위해선 Embedded Database를 사용하거나
khrdev.tistory.com
위 블로그 내용 중 스프링 부트 4.0 버전에서의 설정에 대한 내용이 있었다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:testcontainers-junit-jupiter'
testImplementation 'org.testcontainers:testcontainers-postgresql'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
나도 버전을 찾지 못하는 문제가 있었는데 postgresql 앞에 testcontainers- 를 붙이니 문제가 없어졌다.
테스트 코드 적용 (@ServiceConnection)
Spring Boot 3.1부터 도입된 @ServiceConnection을 사용하면 application.yml에 DB URL을 적을 필요도 없이 자동으로 연결된다.
@Container
@ServiceConnection // 스프링이 알아서 이 컨테이너 정보를 DataSource로 사용함
static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:16");
# 4.0 버전 이후의 변경인지 PostgreSQLContainer<?> 이렇게 쓰는 내용이 많았는데 이제는 PostgreSQLContainer 이렇게 제네릭을 없애도 되었다 그리고 new PostgreSQLContainer<> 도 new PostgreSQLContainer로 바꾸면 되었다.
이제 테스트를 실행하니 초록불이 뜬다!! 성공했다.
하지만 문제가 있다 느려도 너무 느리다. 컨테이너를 테스트마다 띄우는 것 같았다.
6. 성능 최적화: 싱글톤 컨테이너 (Singleton Pattern)
모든 것이 완벽해 보였지만, 테스트 파일이 늘어날수록 전체 테스트 시간이 급격히 느려지는 문제가 발생했다.
왜 느린가?
기본 설정대로라면, 각 테스트 클래스(UserTest, BoardTest...)가 실행될 때마다 새로운 PostgreSQL 컨테이너를 띄우고(Start) 내리는(Stop) 작업을 반복한다. 컨테이너 구동에 약 3~4초가 걸리는데, 테스트 클래스가 10개면 40초를 그냥 버리는 셈이다.
해결: 상속을 통한 컨테이너 공유
이 문제를 해결하기 위해 IntegrationTestSupport라는 추상 클래스를 만들고, 컨테이너를 static으로 선언하여 모든 테스트가 공유하게 만들었다.
일단 테스트 패키지 최 상단에 클래스를 만들었다.
// IntegrationTestSupport.java
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = Replace.NONE) // H2 사용 방지
public abstract class IntegrationTestSupport {
// static: 클래스 로딩 시점에 딱 한 번만 생성됨
@ServiceConnection
static final PostgreSQLContainer<?> POSTGRES_CONTAINER;
static {
POSTGRES_CONTAINER = new PostgreSQLContainer<>("postgres:16");
POSTGRES_CONTAINER.start(); // JVM 시작 시 1회 실행
}
}
이후 실제 테스트 코드는 위 클래스를 상속받기만 하면 된다.
class UserRepositoryTest extends IntegrationTestSupport {
// 설정 코드 없이 깔끔하게 비즈니스 검증만 수행
@Test
void saveTest() { ... }
}
이 적용을 통해 전체 테스트 실행 시간을 획기적으로 단축할 수 있었다. 그리고 테스트 시 생성된 컨테이너도 테스트가 끝나니 제거되는 것을 확인했다.

물론 컨테이너가 바로 제거되는 것이 아니니 여유를 가지고 기다리자
7. 결론
DataJPA는 강력한 도구지만, 이를 안정적으로 테스트하기 위해서는 환경 구축이 필수적이다.
- H2는 편하지만, 운영 환경과의 정합성을 보장하지 못한다.
- 로컬 DB 연결은 CI/CD 환경에서 문제가 된다.
- Testcontainers를 사용하면 환경에 구애받지 않는 격리된 테스트가 가능하다.
- Singleton 패턴을 적용해야 테스트 속도 저하를 막을 수 있다.
이제 "어떤 환경에서도 버튼 하나로 검증 가능한" 단단한 데이터 계층이 완성되었다. 다음은 이제 서비스 계층을 구현해 보겠다.
'사이드프로젝트' 카테고리의 다른 글
| [사이드 프로젝트] 디지털 굿즈 플랫폼 개발기 #6 - Spring Security + JWT + Redis로 구축하는 강력한 인증 시스템 (1편: 설계와 설정) (0) | 2026.01.19 |
|---|---|
| [사이드 프로젝트] 디지털 굿즈 플랫폼 개발기 #5 - Controller 구현과 입력값 유효성 검증 (0) | 2026.01.15 |
| [사이드 프로젝트] 디지털 굿즈 플랫폼 개발기 #4 - Service 계층 구현과 Mockito 단위 테스트 (0) | 2026.01.15 |
| [사이드 프로젝트] 디지털 굿즈 플랫폼 개발기 #2 - JPA 엔티티 설계와 기술적 디테일 (1) | 2026.01.14 |
| [사이드 프로젝트] 디지털 굿즈 플랫폼 개발기 #1 - 초기 DB 설계와 프로젝트 셋팅 (1) | 2026.01.09 |
