일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 테스트 속도개선
- gradle plugin만들기
- spring 테스트 성능
- gatling
- 스프링 gatling
- okhttp progress
- @MockBean 속도
- custom plugin
- 테스트 속도
- Springboot 테스트 속도
- 스프링 scouter
- 자바 가상스레드란
- 자바Thread
- junit 테스트 속도
- JDK21 가상스레드
- 성능테스트 모니터링
- okhttp sink
- gradle plugin이란
- 스프링 성능테스트
- spring socuter
- 스프링 모니터링
- gradle pl
- @DirtiesContext 속도
- spring gatling
- JAVA 가상스레드란
- okhttp upload progress
- spring 테스트 속도
- 자바 가상스레드
- gradle custom plugin
- file upload progress
- Today
- Total
호딩클라우드
[springboot] TestContainer, 테스트 성능 속도 30% 개선기, 테스트 주의사항 본문
Redis와 관련된 테스트를 실제 서비스 환경과 유사한 환경에서 진행하기 위해 TestContainer를 도입했습니다.
테스트 컨테이너 도입 시 주의사항
TestConatiner는 테스트 구동 중 단 1회만 생성되어야 성능에 문제가 없음으로 아래와 같이 구성했습니다.
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.MySQLContainer;
public class ContainerBaseTest {
private static final String REDIS_IMAGE = "redis:6-alpine";
private static final String MYSQL_IMAGE = "mysql:8";
private static final GenericContainer REDIS_CONTAINER;
private static final MySQLContainer MYSQL_CONTAINER;
static {
REDIS_CONTAINER = new GenericContainer<>(REDIS_IMAGE)
.withExposedPorts(6379)
.withReuse(true);
MYSQL_CONTAINER = new MySQLContainer(MYSQL_IMAGE);
MYSQL_CONTAINER.start();
REDIS_CONTAINER.start();
}
@DynamicPropertySource
public static void overrideProps(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", MYSQL_CONTAINER::getJdbcUrl);
registry.add("spring.datasource.username", MYSQL_CONTAINER::getUsername);
registry.add("spring.datasource.password", MYSQL_CONTAINER::getPassword);
registry.add("spring.redis.host", REDIS_CONTAINER::getHost);
registry.add("spring.redis.port",
() -> String.valueOf(REDIS_CONTAINER.getMappedPort(6379)));
}
}
테스트 환경 구성도
이렇게 구성된 테스트환경은 아래와 같습니다.
개선 전 테스트 결과
환경구성을 완료하고 전체 테스트를 수행해 보았습니다.
테스트 결과를 보고 무언가 잘못되었다는 생각이 들었습니다.
테스트 갯수가 44개 밖에 안되는데 8.7s가 걸린 것입니다. 문제는 이 시간이 테스트 컨테이너 및 ApplicationContext 로딩시간은 포함되지 않은 듯했습니다. 테스트 런타임부터 종료까지 측정해 보니 대략 아래와 같은 수치를 가졌습니다.
순서 | 테스크 | 걸린 시간 | 누적 시간 |
1 | 테스트 컨테이너 로딩 | 약 9s | 9s |
2 | 최초 스프링 context 로딩 | 약 3s | 12s |
3 | 최초 테스트 시작 ~ 테스트 종료 | 약 19s | 31s |
비록 테스트 컨테이너가 사용되었지만 44개 테스트에 성공/실패 여부를 확인하기 위해 개발과정에서 31초가 소요되는 것은 부담스러웠습니다. 테스트 갯수가 많아지기 전에 테스트 속도를 최적화해야 될 필요성을 느꼈습니다.
테스트 코드 속도에서 중요한 것
테스트 속도에 가장 영향을 많이 받는 것은 ApplicationContext 로딩시간입니다(이하. context) 테스트마다 매번 context를 로딩하게 된다면 테스트 속도가 저하될 수 있습니다.
적어도 테스트별로 한번만 context 로딩하도록 구성하는 것이 좋습니다. 필자의 경우에는 context를 사용하는 테스트 종류가 인수테스트, 통합테스트, Repository테스트, Controller 테스트 이렇게 4종류가 존재함으로 각각 한 번씩만 context를 로딩하도록 변경해보려고 합니다.
테스트 코드 점검
1. @DirtiesContext 제거
@DirtiesContext
테스트 격리를 위해 사용되는 어노테이션 입니다.
스프링 테스트에서는 ContextChaching을 통해 같은 Context를 사용하는 테스트가 존재할 경우 새롭게 로딩하지 않고 재활용합니다. 그러나 해당 어노테이션은 context를 새롭게 생성해 내기 때문에 ContextChacing을 적극적으로 활용하지 못합니다.
테스트가 컨텍스트를 수정한 경우(예: 싱글톤 Bean의 상태 수정, 내장된 데이터베이스의 상태 수정 등) 이 주석을 사용하십시오. 동일한 컨텍스트를 요청하는 후속 테스트에는 새 컨텍스트가 제공됩니다.
@DirtiesContext를 제거하고 아래와 테스트 격리를 보장하기위해 DB를 직접 초기화주는 클래스를 작성합니다.
@Autowired
private EntityManager entityManager;
private List<String> tableNames= List.of(
"product",
"category",
"member"
);
@Transactional
public void tearDown() {
entityManager.flush();
entityManager.clear();
(1)
entityManager.createNativeQuery("SET foreign_key_checks = 0").executeUpdate();
(2)
for (String tableName : tableNames) {
entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
}
(3)
entityManager.createNativeQuery("SET foreign_key_checks = 1").executeUpdate();
1. 외래키 제약조건을 해제합니다. (Mysql)
2. TRUNCATE를 이용해 테이블을 삭제해 줍니다.
TRUNCATE 테이블 안의 모든 데이터를 삭제하며, DELETE FROM과 다르게 삭제할 대상을 내부에서 SELECT 하지 않기 때문에 빠릅니다.
3. 외래키 제약조건을 다시 설정합니다.
이렇게 ContextChaching을 이용해 Context가 로딩되는 시간을 줄여 텍스트 속도를 향상합니다.
@DirtiesContext 제거 결과
순서 | 테스크 | 걸린 시간 | 누적 시간 |
1 | 테스트 컨테이너 로딩 | 약 9s | 9s |
2 | 최초 스프링 context 로딩 | 약 3s | 12s |
3 | 최초 테스트 시작 ~ 테스트 종료 | 약 10s | 31s -> 22s |
2. @MockBean 통합
@MockBean을 사용할 때마다 Context가 변경되기 때문에 ContextChacing을 이용할 수 없습니다. 아래 링크에 다양한 해법이 있지만 필자는 MockBean을 한 군데에 모아서 상속받아 같은 context를 사용하도록 구성하여 ContextChaing을 이용하도록 구성했습니다.
@MockBean-Context-Reload-issue
Spring 테스트 프레임워크는 ApplicationContext테스트 실행 사이에 가능할 때마다 캐시 합니다. 캐시 하려면 컨텍스트가 정확히 동일한 구성을 가져야 합니다. 를 사용할 때마다 @MockBean정의에 따라 컨텍스트 구성이 변경됩니다.
public class ValidationTestMockingCluster{
@MockBean
protected ProductUsecase productUsecase;
@MockBean
protected UserUsecase userUsecase;
}
@MockBean 통합 결과
순서 | 테스크 | 걸린 시간 | 누적 시간 |
1 | 테스트 컨테이너 로딩 | 약 9s | 9s |
2 | 최초 스프링 context 로딩 | 약 3s | 12s |
3 | 최초 테스트 시작 ~ 테스트 종료 | 약 9s | 22s -> 21 |
마무리
정리
최초 전체 테스트 시간 30s에서 21s로 약 30%가까히 빨라진 것을 알 수 있습니다. Intelij가 제공하는 순수 테스트 시간으로는 8.x -> 4.x로 약 2배 가량 테스트 속도를 개선했습니다. 테스트 개수가 44개 보다 훨씬 많았을 경우 더 높은 개선도를 보였을 것 같습니다.
테스트 주의사항
정리하자면 테스트 속도 관련 주의사항은 3가지입니다.
1. Test Container 사용 시 Test Container는 한 번만 로딩되는 것을 보장하자.
2. 테스트 격리를 위해 @DirtiesContext 사용할 경우 ContextChacing이 어려울 수 있다.
3. @mockBean은 context를 변경함으로 ContextChacing이 어려울 수 있다.
'문제해결' 카테고리의 다른 글
[MySQL] prefix index로 조회속도 최적화 (0) | 2024.08.27 |
---|---|
[Spring boot] 비동기 이벤트로 로직을 분리하여 실행시간 55% 개선 (0) | 2024.07.29 |
[SpringBoot] 스프링 문서화 자동화 하는법 가이드. 문서 배포 (with. Restdocs, restAssured) (31) | 2023.12.29 |