@SpringBootTest가 3초 걸리는 이유 — Slice 테스트와 Testcontainers

@SpringBootTest
class UserServiceTest {
    @Autowired UserService service;   // 전체 컨텍스트 시작
}

이 테스트는 실행될 때마다 3초가 걸린다 — 전체 Spring 컨텍스트를 시작하기 때문이다. 빈을 전부 로드하고, 자동 구성을 평가하고, 내장 서버까지 준비한다. 단순히 서비스 로직 하나를 테스트하는 데 이것은 너무 무겁다. Spring Boot는 @WebMvcTest, @DataJpaTestSlice 테스트로 컨텍스트를 최소화한다.

이 글은 Spring Boot Test의 계층(전체 vs Slice), Mock 사용, Testcontainers 통합, JUnit 6 전환을 풀어간다. (Spring Boot Reference - Testing)

테스트 계층 — 무엇을 얼마나 띄울 것인가

flowchart TD
    UNIT["단위 테스트<br/>(순수 Java, Spring 없음)"] --> SLICE["Slice 테스트<br/>(특정 계층만)"]
    SLICE --> INT["통합 테스트<br/>(전체 컨텍스트)"]
    INT --> E2E["End-to-End<br/>(실제 환경)"]
테스트 유형 어노테이션 컨텍스트 속도 용도
단위 (Spring 없음) 없음 빠름 (<100ms) 순수 로직
Slice (Web) @WebMvcTest MVC 계층만 빠름 ( ~ 1s) 컨트롤러 테스트
Slice (JPA) @DataJpaTest JPA 계층만 빠름 ( ~ 1s) 리포지토리 테스트
통합 @SpringBootTest 전체 느림 ( ~ 3s) 전체 흐름

@WebMvcTest — 컨트롤러 Slice 테스트

// Spring Boot 4 / Java 25 — @WebMvcTest
import org.springframework.boot.test.autoconfigure.web.servlet.*;
import org.springframework.test.web.servlet.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired MockMvc mockMvc;
    @MockBean UserService service;   // 서비스는 mock으로 대체

    @Test
    void getUser_returnsJson() throws Exception {
        when(service.findById(1L))
            .thenReturn(new UserResponse(1L, "Alice", "alice@test.com", 30, null));

        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("Alice"))
            .andExpect(jsonPath("$.email").value("alice@test.com"));
    }
}

@WebMvcTestDispatcherServlet, HttpMessageConverter, Validator만 로드한다 — UserService, UserRepository는 로드하지 않는다. 의존성은 @MockBean으로 주입한다. (Spring Boot Reference - @WebMvcTest)

왜 전체 컨텍스트를 띄우지 않는가? @SpringBootTest가 3초 걸리는 이유는 — 수백 개의 bean을 생성하고, 자동 구성을 평가하고, DB 연결을 설정하기 때문이다. 하지만 컨트롤러의 "올바른 JSON을 반환하는가?"를 테스트하려면 컨트롤러 + JSON 직렬화 + 검증 로직만 있으면 된다. 서비스 계층은 실제 DB를 호출하지 않아도 — mock이 "Alice"를 반환하도록 설정하면, 컨트롤러가 그것을 올바른 JSON으로 변환하는지만 검증할 수 있다.

비유: 자동차의 브레이크를 점검할 때 엔진, 변속기, 에어백까지 모두 켤 필요가 없다. 브레이크 페달과 브레이크 시스템만 작동시키면 된다. @WebMvcTest는 "브레이크만 점검"하는 모드이고, @SpringBootTest는 "전체 차량 시동"이다.

@DataJpaTest — JPA Slice 테스트

// Spring Boot 4 / Java 25 — @DataJpaTest
import org.springframework.boot.test.autoconfigure.orm.jpa.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;

@DataJpaTest
class UserRepositoryTest {

    @Autowired UserRepository repository;
    @Autowired TestEntityManager em;

    @Test
    void findByEmail_returnsUser() {
        em.persist(new User("Alice", "alice@test.com", 30));
        em.flush();

        User found = repository.findByEmail("alice@test.com").orElseThrow();

        assertThat(found.getName()).isEqualTo("Alice");
    }
}

@DataJpaTest는 인메모리 H2 DB를 사용하고, 각 테스트 후 롤백한다 (@Transactional 포함). 외부 DB가 필요 없다. (Spring Boot Reference - @DataJpaTest)

Testcontainers — 실제 DB로 통합 테스트

@DataJpaTest의 H2는 실제 운영 DB(PostgreSQL, MySQL)와 다를 수 있다. Testcontainers로 실제 DB 컨테이너를 띄워 테스트한다.

// Spring Boot 4 / Java 25 — Testcontainers
import org.springframework.boot.testcontainers.service.connection.*;
import org.testcontainers.containers.PostgreSQLContainer;

@DataJpaTest
@Testcontainers
class UserRepositoryIntegrationTest {

    @Container
    @ServiceConnection   // Spring Boot 3.1+ 자동 연결
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17");

    @Autowired UserRepository repository;

    @Test
    void customPostgresFunction() {
        // PostgreSQL 특정 기능(JSONB, array 등)을 실제 DB로 테스트
        em.persist(new User("Alice", "alice@test.com", 30));
        assertThat(repository.existsByEmail("alice@test.com")).isTrue();
    }
}

@ServiceConnection(Spring Boot 3.1+)은 컨테이너의 연결 정보를 자동으로 Spring DataSource에 주입한다. @DynamicPropertySource를 수동으로 쓸 필요가 없다. (Spring Boot Reference - Testcontainers)

@MockBean — 의존성 교체

// Spring Boot 4 / Java 25 — @MockBean
@SpringBootTest
class OrderServiceTest {

    @Autowired OrderService orderService;

    @MockBean PaymentClient paymentClient;   // 실제 결제 API 대신 mock

    @Test
    void orderSucceeds_whenPaymentSucceeds() {
        when(paymentClient.charge(anyString(), anyLong()))
            .thenReturn(new PaymentResult("tx-123", "SUCCESS"));

        OrderResult result = orderService.placeOrder("order-1", 50000);

        assertThat(result.status()).isEqualTo("COMPLETED");
        verify(paymentClient).charge("order-1", 50000);
    }
}

JUnit 6 — Spring Boot 4.0의 테스트 프레임워크

Spring Boot 4.0은 JUnit 6를 사용한다. (Spring Framework 7 - JUnit 6)

항목 JUnit 5 (Spring Boot 3) JUnit 6 (Spring Boot 4)
패키지 org.junit.jupiter.api 동일 (하위 호환)
@Test org.junit.jupiter.api.Test 동일
AssertJ 3.x 3.27+
Mockito 5.x 5.x+

JUnit 5에서 6으로의 전환은 대부분의 애플리케이션 코드에 영향을 주지 않는다 — Jupiter API(@Test, @BeforeEach 등)는 그대로 유지된다. 내부 아키텍처 개선이 주요 변화다.

@SpringBootTest 전략 — 통합 테스트 최적화

// Spring Boot 4 / Java 25 — 통합 테스트 최적화
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class FullStackTest {

    @Autowired TestRestTemplate restTemplate;

    @Test
    void createUser_endToEnd() {
        ResponseEntity<UserResponse> response = restTemplate.postForEntity(
            "/api/users",
            new CreateUserRequest("Alice", "alice@test.com", 30),
            UserResponse.class
        );

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody().name()).isEqualTo("Alice");
    }
}

통합 테스트는 느리지만 — 가장 높은 신뢰도를 제공한다. 전체 스택(HTTP → 컨트롤러 → 서비스 → DB → 응답)이 정상 동작하는지 검증한다. 테스트 피라미드 원칙: 단위 테스트가 많고, 통합 테스트는 핵심 흐름만.

실습 — 테스트 전략 비교

// Spring Boot 4 / Java 25 — TestStrategyDemo.java
// Spring 없이 테스트 레이어 시뮬레이션
import java.util.*;

public class TestStrategyDemo {
    interface UserRepo { Optional<String> findNameById(Long id); }

    // 단위 테스트: mock으로 의존성 대체
    static void unitTest() {
        UserRepo mockRepo = id -> Optional.of("Alice");
        var name = mockRepo.findNameById(1L).orElseThrow();
        assert name.equals("Alice") : "단위 테스트 통과";
        System.out.println("단위 테스트: PASS (의존성 mock)");
    }

    // 통합 테스트: 실제 의존성 사용
    static void integrationTest() {
        UserRepo realRepo = id -> Optional.of("Alice");   // 실제 DB 호출 시뮬레이션
        var name = realRepo.findNameById(1L).orElseThrow();
        assert name.equals("Alice") : "통합 테스트 통과";
        System.out.println("통합 테스트: PASS (실제 의존성)");
    }

    public static void main(String[] args) {
        unitTest();
        integrationTest();
        System.out.println("\n단위 테스트: 빠름, 의존성 격리");
        System.out.println("통합 테스트: 느림, 전체 검증");
    }
}

AssertJ — 가독성 높은 검증

// Spring Boot 4 / Java 25 — AssertJ (Spring Boot 기본)
import static org.assertj.core.api.Assertions.*;

@Test
void userCreation_validatesFields() {
    User user = new User("Alice", "alice@test.com", 30);

    assertThat(user)
        .isNotNull()
        .extracting(User::getName, User::getEmail, User::getAge)
        .containsExactly("Alice", "alice@test.com", 30);

    assertThat(user.getName())
        .startsWith("A")
        .hasSize(5)
        .isNotBlank();
}

AssertJ는 JUnit의 assertEquals보다 가독성이 훨씬 높다 — 메서드 체이닝으로 자연어에 가까운 검증을 작성한다. Spring Boot Starter Test에 기본 포함되어 있다.

@TestConfiguration — 테스트 전용 bean

// Spring Boot 4 / Java 25 — 테스트 전용 bean 등록
@TestConfiguration
public class TestConfig {
    @Bean
    public PaymentClient mockPaymentClient() {
        return new MockPaymentClient();   // 실제 API 대신 항상 성공하는 mock
    }
}

@SpringBootTest
@Import(TestConfig.class)   // 테스트 설정 로드
class OrderServiceTest {
    @Autowired PaymentClient client;   // MockPaymentClient 주입
}

요약 — 이 글의 결론

  • Slice 테스트(@WebMvcTest, @DataJpaTest)로 컨텍스트를 최소화한다. 전체 애플리케이션을 띄우지 않고 특정 계층만 테스트하여 속도를 높인다.
  • @MockBean으로 의존성을 mock으로 교체한다. 외부 API, 느린 서비스를 실제로 호출하지 않고 테스트한다.
  • Testcontainers + @ServiceConnection으로 실제 DB를 테스트한다. H2가 아닌 PostgreSQL/MySQL 컨테이너를 띄워 DB 특정 기능을 검증한다.
  • JUnit 6 전환은 애플리케이션 코드에 큰 영향이 없다. Jupiter API(@Test, @BeforeEach)가 동일하다.
  • @SpringBootTest는 느리지만 가장 높은 신뢰도를 제공한다. 핵심 비즈니스 흐름에만 사용한다.

생각해 볼 문제

  1. @MockBean@SpyBean의 차이는? 각각 언제 써야 하는가?
  2. @DataJpaTest에서 H2 대신 Testcontainers PostgreSQL을 사용하려면 어떻게 하는가?
  3. @WebMvcTest@Service bean을 로드하지 않는 이유는? @MockBean으로 어떻게 해결하는가?
  4. Spring Boot 4.0에서 JUnit 6으로 전환 시 기존 JUnit 4 테스트는 어떻게 되는가?
  5. Testcontainers를 CI 환경(GitHub Actions 등)에서 사용할 때 주의점은?
  6. @ServiceConnection(Spring Boot 3.1+)이 기존 @DynamicPropertySource를 어떻게 대체하는가?

참고

  1. Spring Boot 4.0과 이전 버전(Spring Boot 3.x)의 가장 큰 차이점은 무엇인가? 마이그레이션 시 주의할 점은?
  2. 이 글에서 다룬 개념을 실제 프로젝트에 적용할 때 가장 먼저 고려해야 할 것은?

'Develop Artifacts > Spring Boot' 카테고리의 다른 글

SpringBoot - 08. actuator  (0) 2026.07.12
SpringBoot - 07. security  (0) 2026.07.12
SpringBoot - 05. spring-data-jpa  (0) 2026.07.12
SpringBoot - 4. rest api  (0) 2026.07.12
SpringBoot - 03. embedded-server  (0) 2026.07.12