밀리고 밀리다가 이제 쓰는 Test 개선기....ㅠ
기존 방식
통합 테스트와 레포지토리 테스트 시 H2 데이터베이스를 사용하여 테스트를 진행 했다.
데이터베이스를 사용한 테스트에서, 각각을 격리하기 위해 @DirtiesContext를 ClassMode.AFTER_EACH_TEST_METHOD 옵션으로 사용했다.
테스트 격리용 어노테이션을 생성하고 해당 기능이 필요한 테스트에 대해 선언을 한다.
아래는 레포 테스트와 통합 테스트용 어노테이션에 격리용 어노테이션을 적용하여 사용한 예시이다.
그런데 해당 옵션은 매 테스트 @DirtiesContext는 DataSource를 포함한 전체 ApplicationContext을 새로 생성한다.
초반 미션을 진행할 때에는 데이터베이스 격리가 필요한 테스트가 많지 않아 해당 방식을 사용해도 문제가 없었다. (샤라웃 투 맥)
문제 발생
하지만 미션을 진행하면서 통합, 레포 테스트가 67개, 전체 테스트 갯수가 160개를 넘어가다보니, 빌드 속도가 눈에 띄게 느려졌다.
해당 방식의 큰 문제는 격리가 필요한 테스트의 수만큼 ApplicationContext를 생성하는 것인데, 다시말해 전체 테스트 실행 시 67개의 ApplicationContext를 생성하는 것이기 때문에 매우 비효율적이었다.
따라서 테스트 격리에 대해 다른 방식을 @DirtiesContext가 아닌 다른 방식을 사용해보기로 했다.
방식 개선
이를 개선할 방식으로는 @Sql, @BeforeEach & @AfterEach, AbstractTestExecutionListener 3가지 방식을 고려했고
각 방식의 장단점은 다음과 같았다.
방식 | 특징 | 장점 | 단점 | 적합한 사용 상황 |
@DirtiesContext | 전체 ApplicationContext를 재생성 | 완벽한 격리 보장 | 매우 느림, 리소스 소모 큼 | 전체 애플리케이션 상태 변경이 필요한 경우 |
@Sql | SQL 스크립트로 DB 상태 초기화 | 빠름 | 테이블 추가, 삭제 시 sql 파일의 해당 테이블 제거 등 변경 필요 | 간단한 데이터 셋업이 필요한 경우 |
@BeforeEach, @AfterEach | 테스트 전후 코드로 상태 관리 | 반복적인 코드 작성 필요 | 복잡한 셋업/정리가 필요한 경우 | |
AbstractTestExecutionListener | 테스트 실행 주기에 맞춘 커스텀 로직 | 높은 유연성, 재사용성 | (다른 구현사항에 비해) 구현 복잡도 높음...? | 프로젝트 전반에 걸친 일관된 테스트 로직이 필요 한 경우 |
AbstractTestExecutionListener 선택 이유
@Sql과의 가장 큰 차이점은 @Sql를 사용할 때는 .sql 파일에서 ApplicationContext에 접근할 수 없기 때문에 드랍하려는 테이블마다 쿼리를 기입해주어야 한다.
AbstractTestExecutionListener은 ApplicationContext에 접근하여 현재 등록된 EntityManager 또는 JdbcTemplate 등을 사용해 테이블을 손수 작성하지 않아도 테이블 정보를 가져올 수 있다는 것이다.
요구사항에 의해 테이블이 추가, 삭제 되어도 격리하는 부분은 변경되지 않는 것이 유지보수에 맞다고 판단했다.
따라서 결론으로 가장 유지보수하기 좋고, 성능상으로도 문제가 없는 AbstractTestExecutionListener를 사용하기로 했다.
AbstractTestExecutionListener 구현
AbstractTestExecutionListener의 beforeTestMethod를 재정의하면, 테스트 메소드 실행 전 데이터베이스를 클린업하는 로직이 수행되도록 할 수 있다.
Jpa를 사용하지 않고 Jdbc 미션을 진행 중이기 때문에 현재의 테스트 컨텍스트에서 ApplicationContext에 접근하여 등록된 JdbcTemplate 빈을 가져왔다.
그 후 테이블 정보를 가져오고, 테이블을 순회하여 truncate 하는 로직으로 구성했다.
package roomescape.util;
import java.util.List;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;
public class DatabaseCleaner extends AbstractTestExecutionListener {
@Override
public void beforeTestMethod(TestContext testContext) {
JdbcTemplate jdbcTemplate = testContext.getApplicationContext().getBean(JdbcTemplate.class);
List<String> queries = getTruncateQueries(jdbcTemplate);
truncate(queries, jdbcTemplate);
}
private List<String> getTruncateQueries(final JdbcTemplate jdbcTemplate) {
return jdbcTemplate.queryForList( """
SELECT CONCAT('TRUNCATE TABLE ', TABLE_NAME, ' RESTART IDENTITY;')
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'PUBLIC';
""", String.class);
}
private void truncate(List<String> queries, JdbcTemplate jdbcTemplate) {
jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY FALSE;");
queries.forEach(jdbcTemplate::execute);
jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY TRUE;");
}
}
구현한 Listener를 등록하려면 아래처럼 @TestExecutionListeners를 선언해주면 된다.
이 부분은 우리가 만든 DatabaseCleaner 클래스를 테스트 실행 리스너로 등록한다는 의미이다.
mergeModes의 MERGE_WITH_DEFAULTS는 우리가 추가한 DatabaseCleaner를 기존 스프링의 기본 리스너들과 함께 사용하겠다는 의미이다.
(이 모드를 사용하지 않으면, 기본 스프링의 리스너들이 모두 무시되고 DatabaseCleaner만 사용되게 된다.)
속도 비교
이렇게 변경 전후를 보았으니 gradle의 profile 옵션을 통해 성능을 측정해보자.
@DirtiesContext 사용했을 때 전체 속도
AbstractTestExecutionListener 사용한 후 전체 속도
사진에서 볼 수 있듯이 전체 빌드 시간이 49.541s에서 12.148s로 4배 이상이 개선된 것을 확인할 수 있다.!!!!!!!!!!!!!!!!!!!
'우아한테크코스 6기 > 2단계' 카테고리의 다른 글
난 레벨 2 동안 어떤 경험들을 했을까? (0) | 2024.07.08 |
---|---|
gradle 살펴보기 (0) | 2024.06.23 |
JPQL new 연산은 지양해야 할까? JPQL은 어떻게 동작할까? (0) | 2024.06.04 |
[방탈출 사용자 예약] @Bean, @Component, 그리고 POJO (0) | 2024.05.14 |
영속성 entity 와 domain entity 분리해보기 (0) | 2024.05.12 |