@SpringBootTest 심화 — 슬라이스 테스트와 통합 테스트 사이의 선택 기준
테스트를 실행할 때마다 애플리케이션 전체가 뜨는 데 10초씩 걸린다면, 그 테스트를 자주 실행하게 될까요?
테스트가 느리면 안 돌리게 되고, 안 돌리면 의미가 없어집니다. @SpringBootTest는 강력하지만, 모든 테스트에 사용하면 전체 테스트 실행 시간이 기하급수적으로 늘어납니다. 공부하다 보니 "이 테스트에 정말 전체 컨텍스트가 필요한가?"를 먼저 묻는 습관이 가장 중요하다는 걸 깨달았습니다.
@SpringBootTest의 동작 원리
@SpringBootTest는 전체 ApplicationContext를 로드 하는 통합 테스트 어노테이션입니다.
내부적으로 일어나는 일:
@SpringBootApplication이 붙은 클래스를 찾아 컴포넌트 스캔 실행- 모든
@Bean,@Configuration, Auto-Configuration 로드 - 내장 서버 설정 (webEnvironment에 따라)
- 테스트 프로퍼티 적용
@SpringBootTest
class OrderServiceIntegrationTest {
@Autowired
private OrderService orderService;
@Autowired
private OrderRepository orderRepository;
@Test
void 주문_생성부터_저장까지_전체_흐름을_검증한다() {
// 서비스 → 리포지토리 → DB까지 실제로 동작
OrderRequest request = new OrderRequest("상품A", 2);
Order order = orderService.createOrder(request);
assertThat(order.getId()).isNotNull();
assertThat(orderRepository.findById(order.getId())).isPresent();
}
}
이 테스트는 전체 빈을 띄우기 때문에 보통 5~10초 가 걸립니다. 빈이 수백 개인 프로젝트에서는 더 오래 걸리기도 합니다.
webEnvironment 옵션 4가지
@SpringBootTest의 webEnvironment 속성은 서버를 어떻게 띄울지 결정합니다.
| 옵션 | 동작 | 테스트 도구 |
|---|---|---|
MOCK (기본) | 실제 서버 없이 MockServletContext 사용 | MockMvc |
RANDOM_PORT | 랜덤 포트로 실제 서버 기동 | TestRestTemplate, WebTestClient |
DEFINED_PORT | server.port 설정값으로 서버 기동 | TestRestTemplate, WebTestClient |
NONE | 웹 환경 없이 ApplicationContext만 로드 | 직접 빈 호출 |
// 실제 서버를 띄워서 테스트 — HTTP 통신까지 검증하고 싶을 때
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderApiIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void 주문_API를_호출하면_201을_반환한다() {
OrderRequest request = new OrderRequest("상품A", 2);
ResponseEntity<Order> response = restTemplate.postForEntity(
"/api/orders", request, Order.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
}
}
// 웹 환경이 필요 없는 서비스 계층 통합 테스트
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class PaymentServiceIntegrationTest {
@Autowired
private PaymentService paymentService;
@Test
void 결제_처리_흐름을_검증한다() {
PaymentResult result = paymentService.process(new PaymentRequest(10000));
assertThat(result.isSuccess()).isTrue();
}
}
실무에서는 MOCK이 가장 많이 쓰이고, 실제 HTTP 레벨까지 검증해야 할 때만 RANDOM_PORT를 사용합니다. DEFINED_PORT는 포트 충돌 위험이 있어서 CI 환경에서는 피하는 편입니다.
슬라이스 테스트 — 필요한 계층만 잘라서 테스트
슬라이스 테스트는 특정 계층에 필요한 빈만 로드 하는 테스트입니다. 전체 컨텍스트를 띄우지 않으니 훨씬 빠릅니다.
| 어노테이션 | 용도 | 로드되는 빈 | 대략적 속도 |
|---|---|---|---|
@WebMvcTest | 컨트롤러 테스트 | Controller, ControllerAdvice, Filter | 1~2초 |
@DataJpaTest | Repository 테스트 | JPA 관련 빈, 내장 DB | 1~2초 |
@JsonTest | JSON 직렬화/역직렬화 | JacksonTester, JsonbTester | 1초 미만 |
@RestClientTest | 외부 API 클라이언트 | RestTemplateBuilder, MockRestServiceServer | 1초 미만 |
// 컨트롤러만 테스트 — Service는 Mock으로 대체
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@Test
void 주문_조회_요청이_200을_반환한다() throws Exception {
given(orderService.findById(1L))
.willReturn(new OrderResponse(1L, "상품A", 2));
mockMvc.perform(get("/api/orders/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.productName").value("상품A"));
}
}
// Repository만 테스트 — 내장 H2 DB 사용
@DataJpaTest
class OrderRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private OrderRepository orderRepository;
@Test
void 상태별_주문을_조회한다() {
// TestEntityManager로 테스트 데이터 직접 삽입
entityManager.persist(new Order("상품A", OrderStatus.COMPLETED));
entityManager.persist(new Order("상품B", OrderStatus.PENDING));
entityManager.flush();
List<Order> completed = orderRepository.findByStatus(OrderStatus.COMPLETED);
assertThat(completed).hasSize(1);
assertThat(completed.get(0).getProductName()).isEqualTo("상품A");
}
}
// JSON 직렬화만 테스트
@JsonTest
class OrderResponseJsonTest {
@Autowired
private JacksonTester<OrderResponse> json;
@Test
void 주문_응답이_올바르게_직렬화된다() throws Exception {
OrderResponse response = new OrderResponse(1L, "상품A", 2);
assertThat(json.write(response))
.extractingJsonPathStringValue("$.productName")
.isEqualTo("상품A");
}
}
ApplicationContext 캐시 — 모르면 테스트가 느려지는 이유
Spring Test Framework는 같은 설정의 테스트끼리 ApplicationContext를 캐시해서 공유 합니다. 이 메커니즘을 이해하지 못하면, 테스트를 추가할수록 점점 느려지는 현상을 겪게 됩니다.
컨텍스트 캐시 키를 구성하는 요소:
@ContextConfiguration(또는@SpringBootTest의 classes)@ActiveProfiles@TestPropertySource@MockBean/@SpyBean조합@DirtiesContext여부
// 이 두 테스트는 같은 컨텍스트를 공유한다 (설정이 동일)
@SpringBootTest
class OrderServiceTest { /* ... */ }
@SpringBootTest
class PaymentServiceTest { /* ... */ }
// 이 테스트는 새 컨텍스트를 생성한다 (프로파일이 다름)
@SpringBootTest
@ActiveProfiles("test")
class OrderServiceTest { /* ... */ }
@SpringBootTest
@ActiveProfiles("integration") // 다른 프로파일 → 새 컨텍스트!
class PaymentServiceTest { /* ... */ }
프로파일 함정
공부하다 보니 이 부분에서 많이 헷갈렸습니다. 테스트마다 다른 프로파일을 쓰면 컨텍스트가 매번 새로 생성 됩니다. 테스트가 100개인데 프로파일 조합이 5가지라면, 컨텍스트가 5번 로드되는 겁니다.
컨텍스트 캐시를 최대한 활용하는 방법:
- 테스트 프로파일을 하나로 통일 (
@ActiveProfiles("test")) @MockBean조합을 ** 일관되게** 유지 (A 테스트에서 ServiceA를 Mock하고, B 테스트에서 ServiceB를 Mock하면 별도 컨텍스트)@DirtiesContext는 정말 필요한 경우에만 사용@TestPropertySource로 개별 속성을 바꾸는 대신application-test.yml에 통합
// 나쁜 예: MockBean 조합이 달라서 컨텍스트가 각각 생성됨
@SpringBootTest
class Test1 {
@MockBean private ServiceA serviceA; // 컨텍스트 #1
}
@SpringBootTest
class Test2 {
@MockBean private ServiceB serviceB; // 컨텍스트 #2
}
// 좋은 예: 공통 베이스 클래스로 MockBean 조합 통일
@SpringBootTest
abstract class BaseIntegrationTest {
@MockBean protected ServiceA serviceA;
@MockBean protected ServiceB serviceB;
}
class Test1 extends BaseIntegrationTest { /* ... */ } // 같은 컨텍스트
class Test2 extends BaseIntegrationTest { /* ... */ } // 같은 컨텍스트
테스트 피라미드와 선택 기준
테스트 피라미드는 테스트를 세 계층으로 나누고, 아래로 갈수록 많이, 위로 갈수록 적게 작성하는 전략입니다.
/ E2E \ ← 가장 적게 (Selenium, Playwright)
/----------\
/ 통합 테스트 \ ← 적당히 (@SpringBootTest)
/--------------\
/ 슬라이스 테스트 \ ← 많이 (@WebMvcTest, @DataJpaTest)
/------------------\
/ 단위 테스트 \ ← 가장 많이 (Mockito, JUnit)
/----------------------\
각 계층의 특성:
** 단위 테스트 (Mockito + JUnit)**
- Spring 없이 순수 자바로 테스트
- 가장 빠름 (밀리초 단위)
- 비즈니스 로직 검증에 집중
** 슬라이스 테스트 (@WebMvcTest, @DataJpaTest 등)**
- 특정 계층만 로드
- 빠름 (1~2초)
- 계층 간 통합 검증
** 통합 테스트 (@SpringBootTest)**
- 전체 컨텍스트 로드
- 느림 (5~10초)
- 전체 흐름 검증
실무 선택 기준 — "내가 테스트하려는 것이 뭔가?"
테스트를 작성하기 전에 "지금 검증하려는 것이 정확히 뭔가?" 를 먼저 물어보는 게 핵심입니다.
내가 테스트하려는 것이 뭔가?
│
├─ 순수 비즈니스 로직 → 단위 테스트 (Mockito)
│ 예: 할인 계산, 유효성 검증, 상태 변환
│
├─ 컨트롤러의 요청/응답 매핑 → @WebMvcTest
│ 예: URL 매핑, 유효성 검증 응답, JSON 변환
│
├─ JPA 쿼리가 의도대로 동작하는지 → @DataJpaTest
│ 예: 커스텀 쿼리, @Query 메서드, 페이징
│
├─ JSON 직렬화/역직렬화 → @JsonTest
│ 예: 날짜 포맷, @JsonIgnore, 커스텀 직렬화기
│
├─ 외부 API 클라이언트 → @RestClientTest
│ 예: RestTemplate/WebClient 호출, 에러 처리
│
└─ 여러 계층이 함께 동작하는 흐름 → @SpringBootTest
예: 주문 생성 → 재고 차감 → 알림 발송
// 비즈니스 로직은 단위 테스트로 충분하다
class DiscountCalculatorTest {
private DiscountCalculator calculator = new DiscountCalculator();
@Test
void VIP_회원은_10퍼센트_할인을_받는다() {
// Spring 컨텍스트 없이 순수 자바로 테스트
Money discounted = calculator.calculate(
Money.of(10000), MemberGrade.VIP
);
assertThat(discounted).isEqualTo(Money.of(9000));
}
}
// 여러 계층의 흐름을 검증할 때만 @SpringBootTest
@SpringBootTest
@Transactional
class OrderFlowIntegrationTest {
@Autowired private OrderService orderService;
@Autowired private StockRepository stockRepository;
@Test
void 주문_생성_시_재고가_차감된다() {
// 서비스 → 리포지토리 → DB 전체 흐름 검증
stockRepository.save(new Stock("상품A", 10));
orderService.createOrder(new OrderRequest("상품A", 3));
Stock stock = stockRepository.findByProductName("상품A");
assertThat(stock.getQuantity()).isEqualTo(7);
}
}
@SpringBootTest에서 자주 쓰는 옵션들
// 특정 설정 클래스만 로드 — 전체보다 빠름
@SpringBootTest(classes = {OrderConfig.class, PaymentConfig.class})
class PartialContextTest { /* ... */ }
// 프로퍼티 오버라이드
@SpringBootTest(properties = {
"spring.datasource.url=jdbc:h2:mem:testdb",
"external.api.url=http://localhost:8089"
})
class PropertyOverrideTest { /* ... */ }
// 특정 Auto-Configuration 제외
@SpringBootTest
@EnableAutoConfiguration(exclude = SecurityAutoConfiguration.class)
class WithoutSecurityTest { /* ... */ }
테스트 조합 실무 예시
실제 프로젝트에서 주문 도메인을 테스트한다면 이런 조합이 됩니다:
주문 도메인 테스트 구성:
├── 단위 테스트 (70%)
│ ├── OrderTest.java — 주문 엔티티 비즈니스 로직
│ ├── DiscountPolicyTest.java — 할인 정책 계산
│ └── OrderValidatorTest.java — 주문 유효성 검증
│
├── 슬라이스 테스트 (20%)
│ ├── OrderControllerTest.java — @WebMvcTest
│ ├── OrderRepositoryTest.java — @DataJpaTest
│ └── OrderResponseTest.java — @JsonTest
│
└── 통합 테스트 (10%)
└── OrderFlowTest.java — @SpringBootTest
정리
@SpringBootTest는 전체 컨텍스트를 로드하므로 정말 여러 계층의 통합이 필요한 경우에만 사용합니다- 슬라이스 테스트(
@WebMvcTest,@DataJpaTest등)는 ** 특정 계층만 빠르게 테스트 **할 수 있는 방법입니다 - ApplicationContext 캐시를 잘 활용하려면 ** 프로파일과 MockBean 조합을 통일 **해야 합니다
- 테스트를 작성하기 전에 "내가 검증하려는 것이 뭔가?" 를 먼저 물어보세요 — 대부분은
@SpringBootTest없이도 검증할 수 있습니다