테스트 슬라이스: @WebMvcTest · @DataJpaTest · @SpringBootTest
테스트가 느려서 안 돌리게 된다
처음엔 모든 테스트에 @SpringBootTest를 붙였습니다. 그랬더니 테스트 하나 돌리는 데 전체 컨텍스트가 다 떠서 느리고, 결국 테스트를 잘 안 돌리게 되더라고요. 😅
해결책은 두 가지입니다. (1) 필요한 계층만 잘라 띄우는 테스트 슬라이스, 그리고 의외로 더 중요한 (2) 컨텍스트 캐시를 깨지 않는 것. 슬라이스를 써도 캐시를 줄줄이 깨뜨리면 다시 느려집니다. 이 글은 슬라이스가 어떻게 일부만 띄우는지(자동 구성과 같은 메커니즘), 그리고 왜 테스트 수백 개가 갑자기 느려지는지를 소스 레벨에서 다룹니다.
전체 컨텍스트 vs 슬라이스 — 무엇이 다른가
@SpringBootTest는 메인 클래스의 @SpringBootConfiguration을 찾아 모든 자동 구성 + 모든 컴포넌트를 띄웁니다. 슬라이스는 그중 한 계층에 필요한 자동 구성·빈만 띄웁니다. 아래에서 같은 4초 동안 전체 컨텍스트는 빈을 한참 채우고, 웹 슬라이스는 몇 개만 빠르게 채우는 걸 보세요.
슬라이스는 어떻게 일부만 띄우나 — 자동 구성과 같은 뿌리
슬라이스가 마법으로 일부만 띄우는 게 아닙니다. 슬라이스 애너테이션을 뜯어보면 세 가지 장치가 보입니다.
1
2
3
4
5
6
// @WebMvcTest 정의를 펼치면 (핵심만)
@BootstrapWith(WebMvcTestContextBootstrapper.class)
@OverrideAutoConfiguration(enabled = false) // ① 전체 자동 구성 OFF
@TypeExcludeFilters(WebMvcTypeExcludeFilter.class) // ② 스캔 대상을 웹 컴포넌트로 제한
@ImportAutoConfiguration // ③ 웹용 자동 구성만 다시 켬
public @interface WebMvcTest { ... }
@OverrideAutoConfiguration(enabled = false)로 자동 구성 전체를 끕니다.@TypeExcludeFilter(여기선WebMvcTypeExcludeFilter)가 컴포넌트 스캔에서@Controller·@ControllerAdvice·Converter같은 웹 계층 타입만 통과시키고@Service·@Repository는 제외합니다.@ImportAutoConfiguration이 다시 켤 자동 구성 목록을META-INF/spring/...@AutoConfigureWebMvc.imports같은 큐레이션 파일에서 읽어옵니다. 자동 구성의imports파일과 완전히 같은 메커니즘이죠 — 끄는 게 아니라 화이트리스트로 골라 켜는 것입니다.
그래서 @DataJpaTest는 DataJpaTypeExcludeFilter로 @Entity·리포지토리만, @JsonTest는 직렬화 관련만 띄웁니다. “슬라이스 = 자동 구성을 끄고 그 계층용만 다시 켠 것”이라고 이해하면 전부 일관되게 보입니다.
flowchart LR
A["@WebMvcTest"] --> B["전체 자동구성 OFF<br/>@OverrideAutoConfiguration"]
A --> C["스캔 제한<br/>WebMvcTypeExcludeFilter"]
A --> D["웹 자동구성만 ON<br/>@AutoConfigureWebMvc.imports"]
B & C & D --> E["가벼운 웹 전용 컨텍스트"]
@WebMvcTest — 컨트롤러(웹 계층)만
서비스/리포지토리는 안 뜨므로 목(mock)으로 대체합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired MockMvc mockMvc;
@MockitoBean ProductService productService; // Boot 3.4+ : @MockBean 대체
@Test
void 상품_조회() throws Exception {
given(productService.find(1L)).willReturn(new Product(1L, "키보드"));
mockMvc.perform(get("/products/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("키보드"));
}
}
@MockBean/@SpyBean은 Spring Boot 3.4부터 deprecated →@MockitoBean/@MockitoSpyBean(spring-test 코어로 승격)으로 대체됐습니다. 단순 개명이 아니라, 뒤에서 볼 컨텍스트 캐시 키에 영향을 주는 동작은 그대로입니다.
@DataJpaTest — JPA(영속성 계층)만
리포지토리와 JPA 관련 Bean만 띄우고, 기본적으로 각 테스트를 트랜잭션 후 롤백해 격리합니다(@Transactional이 메타 애너테이션으로 들어 있음).
1
2
3
4
5
6
7
8
9
10
11
12
13
@DataJpaTest
class UserRepositoryTest {
@Autowired UserRepository userRepository;
@Autowired TestEntityManager em; // 영속성 컨텍스트 직접 제어
@Test
void 이메일로_조회() {
em.persistAndFlush(new User("kuo@example.com"));
em.clear(); // 1차 캐시 비워 진짜 쿼리 검증
assertThat(userRepository.findByEmail("kuo@example.com")).isPresent();
}
}
기본은 임베디드 DB로 교체(@AutoConfigureTestDatabase)되지만, 실제 DB와 SQL 방언·제약이 달라 통과/실패가 갈릴 수 있습니다. Testcontainers + @ServiceConnection(Boot 3.1+)으로 진짜 PostgreSQL을 띄우면 접속 정보까지 자동 주입됩니다.
1
2
3
4
5
6
7
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE) // 임베디드 교체 끄기
@Testcontainers
class UserRepositoryTest {
@Container @ServiceConnection
static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16");
}
진짜 병목: 컨텍스트 캐시를 깨고 있었다
여기가 이 글의 핵심입니다. Spring TestContext 프레임워크는 ApplicationContext를 캐시합니다. 같은 설정이면 다음 테스트 클래스가 그 컨텍스트를 재사용하므로, 컨텍스트는 테스트 클래스마다가 아니라 고유한 설정 조합마다 한 번씩 뜹니다.
캐시 키는 MergedContextConfiguration — 대략 이런 요소의 조합입니다.
| 캐시 키에 들어가는 것 | 깨지는 예 |
|---|---|
| 설정 클래스 / 슬라이스 종류 | @WebMvcTest vs @SpringBootTest |
@ActiveProfiles | test vs test,redis |
@TestPropertySource / properties | 클래스마다 다른 프로퍼티 |
@MockitoBean 조합 (ContextCustomizer) | 클래스마다 다른 목 집합 |
webEnvironment, @DirtiesContext | @DirtiesContext는 매번 컨텍스트 폐기 |
즉 테스트 클래스마다 프로퍼티/프로필/목 조합을 조금씩 다르게 쓰면, 그만큼 새 컨텍스트가 뜹니다(캐시 미스). 컨텍스트 하나 뜨는 데 수 초 → 미스 30번이면 빌드가 분 단위로 늘어납니다. 기본 캐시 크기는 32(spring.test.context.cache.maxSize)라, 그 이상 다양해지면 LRU로 쫓겨나 재생성까지 반복됩니다.
flowchart LR
T1[테스트A] --> K1{캐시 키 동일?}
T2[테스트B] --> K1
K1 -->|HIT| R[기존 컨텍스트 재사용 ⚡]
K1 -->|MISS| N[새 컨텍스트 생성 🐢]
N --> R
처방: 프로퍼티/프로필/목 조합을 몇 개의 표준 묶음으로 통일하고, @DirtiesContext는 꼭 필요할 때만 쓰며, 테스트 베이스 클래스로 설정을 공유합니다. -Dspring.test.context.cache.maxSize 와 디버그 로깅(org.springframework.test.context.cache=DEBUG)으로 캐시 적중률을 직접 확인할 수 있습니다.
어떻게 나눠 쓸까
| 검증 대상 | 도구 |
|---|---|
| 컨트롤러(요청/응답/검증/예외) | @WebMvcTest + @MockitoBean |
| 쿼리 메서드·매핑 | @DataJpaTest + Testcontainers |
| 핵심 플로우 end-to-end | @SpringBootTest(소수만) |
| 순수 로직 | 애너테이션 없는 순수 단위 테스트(가장 빠름) |
테스트 피라미드대로 빠른 슬라이스/단위 테스트를 많이, 무거운 통합 테스트는 핵심만. 단, 슬라이스 종류를 잘게 쪼갤수록 캐시 컨텍스트 종류도 늘어난다는 트레이드오프를 기억하세요.
면접/리뷰 단골 질문
- Q.
@WebMvcTest는 서비스 빈을 왜 못 찾나? →WebMvcTypeExcludeFilter가 웹 계층 타입만 스캔에 통과시키기 때문. 서비스는@MockitoBean으로 채운다. - Q. 테스트가 점점 느려지는데 원인은? → 컨텍스트 캐시 미스. 클래스마다 다른 프로퍼티·프로필·목 조합 또는
@DirtiesContext가 새 컨텍스트를 양산한다. - Q. 슬라이스가 일부 자동 구성만 켜는 원리는? →
@OverrideAutoConfiguration(false)로 전체를 끄고@ImportAutoConfiguration이@AutoConfigure*.imports화이트리스트만 다시 켠다. 자동 구성의imports와 같은 메커니즘.
정리
- 슬라이스 = 전체 자동 구성 OFF +
@TypeExcludeFilter로 스캔 제한 + 그 계층용 자동 구성만 화이트리스트로 ON. 자동 구성과 같은 뿌리. - 웹은
@WebMvcTest(+@MockitoBean), 영속성은@DataJpaTest(+Testcontainers@ServiceConnection), 통합은@SpringBootTest소수. - 진짜 속도 결정 요인은 컨텍스트 캐시. 설정 조합을 통일해 캐시 미스를 줄여라.
@MockBean→@MockitoBean(3.4+). 순수 로직은 프레임워크 없이 단위 테스트가 최선.
관련 글: 트랜잭션 롤백·
@Transactional의 동작은 이 글에서, 슬라이스가 켜고 끄는 자동 구성의 원리는 자동 구성 글에서 더 깊게 다룹니다.
