🌏 인트로
서비스를 실제로 개발하다 보면 단위 테스트(Unit Test)보다 훨씬 현실적인 End-to-End(E2E) 테스트를 작성할 일이 많습니다.
E2E 테스트는 실제 API를 호출하고, 데이터베이스와의 연동, 인증 절차, 외부 요청 등까지 통합적으로 검증하는 테스트입니다.
✅ E2E 테스트 예시
(인수테스트입니다)
@Test
void 고객이_자신의_리워드를_조회한다() {
String ownerAccessToken = 카페_사장_회원_가입_요청하고_액세스_토큰_반환(OWNER_CREATE_REQUEST);
Long savedCafeId = 카페_생성_요청하고_아이디_반환(ownerAccessToken, CAFE_CREATE_REQUEST);
String customerToken = 가입_고객_회원_가입_요청하고_액세스_토큰_반환(...);
Long customerId = authTokensGenerator.extractMemberId(customerToken);
Long couponId = 쿠폰_생성_요청하고_아이디_반환(ownerAccessToken, new CouponCreateRequest(savedCafeId), customerId);
쿠폰에_스탬프를_적립_요청(ownerAccessToken, customerId, couponId, new StampCreateRequest(10));
ExtractableResponse<Response> response = 리워드_목록_조회(customerToken);
VisitorRewardsFindResponse result = response.body().as(VisitorRewardsFindResponse.class);
assertAll(
() -> assertThat(response.statusCode()).isEqualTo(OK.value()),
() -> assertThat(result.getRewards()).isNotEmpty()
);
}
이 테스트는 실제로 API를 호출하고 DB에 데이터를 쌓습니다. 문제는 이런 테스트가 DB 상태를 오염시킨다는 점입니다.
✅ DB 오염의 문제점: 비결정적 테스트
테스트가 실행될수록 데이터가 계속 누적되며, 다음 테스트가 기존 데이터에 영향을 받는 비결정적 테스트(Non-deterministic Test)로 변질됩니다.
테스트는 ‘언제 실행하더라도 같은 결과’를 내야 하는데, DB가 오염되면 테스트를 신뢰할 수 없게 됩니다ㅠㅠ
🌏 테스트 데이터 격리
✅ 모든 테스트는 깨끗한 DB에서 시작해야 한다
End-to-End 테스트에서는 테스트 전후에 항상 DB를 초기화해야 합니다.
단순히 @Transactional을 사용하면 테스트가 끝난 뒤 자동으로 롤백되어 DB가 깨끗해지지만, 이 방식은 @SpringBootTest(webEnvironment = RANDOM_PORT) 환경에서는 통하지 않습니다.
왜냐하면 이 경우에는 실제로 서버를 띄워서 API 요청을 보내기 때문에, 요청이 테스트 메서드의 트랜잭션 경계 밖에서 수행됩니다.
테스트 코드가 아무리 @Transactional로 감싸져 있어도 HTTP 요청을 통해 실행된 비즈니스 로직은 별도의 트랜잭션에서 커밋되어 버리기 때문에 단순 롤백으로는 DB를 되돌릴 수 없습니다.
물론 테스트마다 스프링 컨테이너를 통째로 다시 띄우는 방법(@DirtiesContext 등)도 있긴 하지만, 테스트 한 번 실행할 때마다 애플리케이션 컨텍스트가 새로 만들어지고, 빈 초기화, DI 주입, 서버 부팅까지 전부 다시 일어나기 때문에 너무 비효율적입니당,, 테스트 속도가 수 초 단위로 느려지고, 대규모 테스트에서는 사실상 불가능에 가깝습니다.
따라서 테스트 전후에 항상 DB를 직접 초기화해야 합니다
이 포스팅에서는 쉽게 가져다 쓸 수 있는 초기화 템플릿을 공유하려고 합니당
🌏 DataCleaner
@Profile("test")
@Component
public class DataCleaner {
private static final String FOREIGN_KEY_CHECK_FORMAT = "SET FOREIGN_KEY_CHECKS %d";
private static final String TRUNCATE_FORMAT = "TRUNCATE TABLE %s";
private final List<String> tableNames = new ArrayList<>();
@PersistenceContext
private EntityManager entityManager;
@PostConstruct
public void findDatabaseTableNames() {
List<Object[]> tableInfos = entityManager.createNativeQuery("SHOW TABLES").getResultList();
for (Object[] tableInfo : tableInfos) {
tableNames.add((String) tableInfo[0]);
}
}
@Transactional
public void clear() {
entityManager.clear();
truncate();
}
private void truncate() {
entityManager.createNativeQuery(String.format(FOREIGN_KEY_CHECK_FORMAT, 0)).executeUpdate();
for (String tableName : tableNames) {
entityManager.createNativeQuery(String.format(TRUNCATE_FORMAT, tableName)).executeUpdate();
}
entityManager.createNativeQuery(String.format(FOREIGN_KEY_CHECK_FORMAT, 1)).executeUpdate();
}
}
- 테이블 이름 로드
SHOW TABLES로 현재 DB 내의 모든 테이블 이름을 읽어옵니다. - 외래 키 제약 비활성화각 테이블을
TRUNCATE로 비웁니다.SET FOREIGN_KEY_CHECKS = 0으로 FK 제약을 끄고, - 다시 활성화
SET FOREIGN_KEY_CHECKS = 1으로 복구합니다.
이렇게 하면 모든 데이터가 완전히 초기화된 상태로 다음 테스트가 시작됩니다.
🌏 DataClearExtension
하지만 매 테스트마다 dataCleaner.clear()를 수동으로 호출하면 불편하니 자동으로 실행해주는 JUnit 5 Extension을 추가합니다.
public class DataClearExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
DataCleaner dataCleaner = SpringExtension.getApplicationContext(context)
.getBean(DataCleaner.class);
dataCleaner.clear();
}
}
이 클래스를 @ExtendWith로 등록하면 테스트 시작 전에 항상 DataCleaner.clear()가 자동으로 호출됩니다.
🌏 테스트 템플릿
최종적으로 모든 E2E 테스트는 다음과 같은 템플릿을 상속받습니다.
@ExtendWith(DataClearExtension.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class AcceptanceTest {
@LocalServerPort
private int port;
@BeforeEach
void setup() {
RestAssured.port = port;
}
@AfterEach
void tearDown() {
cleaner.clear();
}
}
여기서 @ExtendWith(DataClearExtension.class) 덕분에 각 테스트마다 DB가 자동으로 정리됩니다.
🌏 사용 예시
이제 단순히 AcceptanceTest를 상속받기만 하면 됩니다.
class VisitorRewardFindAcceptanceTest extends AcceptanceTest {
@Test
void 고객이_자신의_리워드를_조회한다() {
// 실제 API 호출 및 검증
}
@Test
void 리워드가_없으면_빈_배열을_반환한다() {
// 또 다른 시나리오
}
}
각 테스트가 끝날 때마다 자동으로 데이터가 비워지므로 서로 영향을 주지 않고, 테스트 실행 순서에도 의존하지 않습니다.


도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!
'PROJECT > Stamp Crush' 카테고리의 다른 글
| [우테코] 무중단 서비스 환경에서의 안전한 스키마 변경과 데이터 마이그레이션 전략: backward-compatible schema (1) | 2025.08.19 |
|---|---|
| [우테코] 스탬프크러쉬를 마무리하며 (2) | 2023.12.04 |
| [우테코] JWT 방식에서 로그아웃, Refresh Token 만들기(2): 구현을 해보자! (0) | 2023.11.08 |
| [우테코] 무중단 배포 자동화(2): 배포서버에 Github Actions self-hosted runners, Nginx, Docker 설정 (2) | 2023.10.28 |
| [우테코] 무중단 배포 자동화(1): Github Actions workflow 생성, Secrets 설정 (2) | 2023.10.27 |