Spring Boot 테스트 전략: 단위와 통합 나누기

좋은 테스트 전략의 핵심은 하나로 요약됩니다. 빠른 단위 테스트로 로직 대부분을 잡고, 느리지만 진짜인 통합 테스트로 위험한 경계만 검증하는 것입니다. 모든 것을 @SpringBootTest로 돌리면 빌드가 느려지고, 반대로 단위 테스트만 쌓으면 실제 환경에서 터지는 버그를 놓칩니다.

이 글에서는 Spring Boot 테스트 전략을 단위 테스트와 통합 테스트로 나누는 기준을 잡고, Mockito로 의존성을 끊는 법, 슬라이스 테스트로 계층만 떼어 검증하는 법, 그리고 Testcontainers로 실제 데이터베이스를 띄워 테스트하는 법까지 차례로 정리합니다. 테스트 코드를 어디서부터 어떻게 짜야 할지 막막한 분께 실전 기준이 되도록 구성했습니다.


테스트 피라미드: 무엇을 얼마나 만들까

테스트 전략의 출발점은 비율입니다. 흔히 말하는 테스트 피라미드는 빠르고 저렴한 단위 테스트를 가장 많이, 그 위에 통합 테스트를 적당히, 전체 흐름을 보는 E2E 테스트를 가장 적게 두라는 원칙입니다.

이유는 단순합니다. 단위 테스트는 수 밀리초 만에 끝나고 실패 원인이 명확합니다. 반면 통합 테스트는 스프링 컨텍스트를 띄우고 DB에 연결하느라 느리고, 실패했을 때 원인을 좁히기 어렵습니다. 그래서 검증의 무게중심을 아래쪽(단위)에 두는 것이 빠른 피드백과 안정성을 동시에 얻는 길입니다.

피라미드가 뒤집히면, 즉 통합 테스트가 과도하게 많아지면 빌드 시간이 수십 분으로 늘고 개발자가 테스트를 외면하게 됩니다. 전략의 첫 단추는 “이 검증을 더 가벼운 단위 테스트로 내릴 수 없는가”를 늘 먼저 묻는 것입니다.


단위 테스트와 통합 테스트, 무엇이 다른가

두 테스트는 목적이 다릅니다. 단위 테스트는 클래스 하나의 로직을 외부와 분리해 검증하고, 통합 테스트는 여러 구성요소가 실제로 맞물려 동작하는지 확인합니다.

구분단위 테스트통합 테스트
검증 대상클래스·메서드 단위 로직계층·시스템 간 연동
의존성Mock으로 대체실제 구현 사용
스프링 컨텍스트띄우지 않음띄움 (느림)
속도매우 빠름느림
주 사용 도구JUnit5 + Mockito@SpringBootTest, Testcontainers

기준은 이렇게 잡으면 깔끔합니다. 비즈니스 로직·분기·계산은 단위 테스트로 촘촘히 덮고, DB 쿼리·트랜잭션·외부 연동처럼 실제로 붙어 봐야 아는 부분은 통합 테스트로 검증합니다. 둘은 경쟁 관계가 아니라 역할 분담입니다.


단위 테스트: Mockito로 의존성 끊기

단위 테스트의 관건은 검증 대상만 남기고 나머지 의존성을 모두 가짜로 대체하는 것입니다. Spring 없이 순수 JUnit5와 Mockito만으로 충분합니다.

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    OrderRepository orderRepository; // 가짜 의존성

    @InjectMocks
    OrderService orderService;       // 테스트 대상

    @Test
    void 주문_금액을_정상_계산한다() {
        given(orderRepository.findById(1L))
            .willReturn(Optional.of(new Order(1L, 10_000)));

        int total = orderService.calculateTotal(1L);

        assertThat(total).isEqualTo(10_000);
        then(orderRepository).should().findById(1L);
    }
}

@Mock으로 리포지토리를 가짜로 만들고 @InjectMocks로 대상 서비스에 주입합니다. 스프링 컨텍스트를 띄우지 않으므로 테스트가 거의 즉시 끝납니다. 핵심 비즈니스 로직, 특히 조건 분기와 예외 처리는 이 방식으로 빠짐없이 검증하는 것이 비용 대비 효과가 가장 큽니다.

주의할 점은 Mock에만 의존하면 “내가 짠 가정”만 검증하게 된다는 것입니다. 실제 DB나 외부 시스템의 동작은 다음 단계인 통합 테스트에서 확인해야 합니다.


통합 테스트: @SpringBootTest와 슬라이스 테스트

통합 테스트는 스프링 컨텍스트를 띄워 구성요소들이 실제로 연결되는지 봅니다. 가장 무거운 선택지는 전체 컨텍스트를 로딩하는 @SpringBootTest인데, 느리기 때문에 남용하면 곤란합니다.

그래서 실무에서는 슬라이스 테스트를 더 자주 씁니다. 필요한 계층만 잘라 띄우는 방식입니다. 컨트롤러 계층만 검증하려면 @WebMvcTest로 MVC 관련 빈만 로딩하고, JPA 리포지토리만 보려면 @DataJpaTest로 영속성 계층만 띄웁니다. 전체를 띄우는 것보다 훨씬 빠릅니다.

@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockitoBean          // 스프링 빈을 가짜로 교체 (Spring Boot 3.4+)
    OrderService orderService;

    @Test
    void 주문_조회_API가_200을_반환한다() throws Exception {
        given(orderService.getOrder(1L))
            .willReturn(new OrderResponse(1L, 10_000));

        mockMvc.perform(get("/orders/1"))
            .andExpect(status().isOk());
    }
}

여기서 스프링 컨텍스트 안의 빈을 가짜로 바꿀 때는 @MockitoBean을 사용합니다. 기존 @MockBean은 Spring Boot 3.4부터 지원이 중단되었으므로, 최신 버전에서는 @MockitoBean이 표준입니다. 순수 단위 테스트의 @Mock과는 쓰임이 다르니 구분해야 합니다. 컨트롤러의 입력 검증 로직을 함께 점검하면 견고함이 올라갑니다.


Testcontainers: 진짜 데이터베이스로 검증하기

통합 테스트에서 가장 흔한 함정은 실제 운영 DB 대신 H2 같은 인메모리 DB로 테스트하는 것입니다. 빠르고 편하지만, H2와 운영 DB(예: PostgreSQL, MySQL)는 SQL 방언과 동작이 미묘하게 달라 테스트는 통과해도 운영에서 터지는 경우가 생깁니다.

Testcontainers는 이 간극을 메웁니다. 테스트 실행 시 Docker로 실제 DB 컨테이너를 띄우고, 끝나면 자동으로 정리합니다. 운영과 동일한 엔진으로 검증하니 신뢰도가 크게 올라갑니다.

@SpringBootTest
@Testcontainers
class OrderRepositoryIT {

    @Container
    @ServiceConnection   // 컨테이너 접속 정보를 자동 주입 (Spring Boot 3.1+)
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16");

    @Autowired
    OrderRepository orderRepository;

    @Test
    void 주문을_저장하고_조회한다() {
        Order saved = orderRepository.save(new Order(null, 10_000));
        assertThat(orderRepository.findById(saved.getId())).isPresent();
    }
}

Spring Boot 3.1부터는 @ServiceConnection이 컨테이너의 접속 정보를 데이터소스에 자동으로 연결해 줘서 설정이 매우 간결해졌습니다. 다만 Docker 실행 환경이 필요하고 컨테이너 기동에 시간이 걸리므로, 모든 테스트가 아니라 DB 의존도가 높은 핵심 시나리오에 집중해 적용하는 것이 좋습니다. 복잡한 JPA 쿼리의 성능 문제까지 함께 살피면 효과가 큽니다.


실전 적용 기준: 무엇을 어디서 테스트할까

도구를 알았다면, 이제 “어느 검증을 어느 테스트로 둘 것인가”가 전략의 핵심입니다. 다음 기준이 출발점이 됩니다.

서비스의 비즈니스 로직, 분기, 계산, 예외 처리는 **단위 테스트(Mockito)**로 가장 두껍게 덮습니다. 빠르고 원인 추적이 쉬워 리팩터링의 안전망이 됩니다. 컨트롤러의 요청·응답 매핑과 입력 검증은 @WebMvcTest 같은 슬라이스 테스트로 가볍게 확인합니다. 리포지토리의 실제 쿼리와 트랜잭션 동작은 Testcontainers 기반 통합 테스트로 검증해, 운영과 같은 DB에서 안전을 보장합니다.

MSA 환경이라면 서비스 간 연동 지점도 통합 테스트의 대상이 됩니다. 외부 호출이 얽힌 부분은 계약 검증과 함께 다루면 신뢰도가 올라갑니다.


자주 묻는 질문 (FAQ)

Q1. @Mock과 @MockitoBean은 어떻게 다른가요?

@Mock은 스프링 없이 순수 단위 테스트에서 객체를 가짜로 만들 때 씁니다. @MockitoBean은 스프링 컨텍스트가 떠 있는 통합·슬라이스 테스트에서 컨테이너 안의 특정 빈을 가짜로 교체할 때 사용합니다. 목적과 동작 환경이 다릅니다.

Q2. 통합 테스트에 꼭 Testcontainers를 써야 하나요?

필수는 아니지만 권장됩니다. H2 같은 인메모리 DB는 운영 DB와 방언이 달라 미묘한 버그를 숨길 수 있기 때문입니다. 운영과 동일한 엔진으로 검증하려면 Testcontainers가 가장 확실합니다. 단, Docker 환경이 필요합니다.

Q3. @SpringBootTest는 언제 쓰는 게 맞나요?

여러 계층이 한꺼번에 맞물리는 전체 흐름을 확인할 때만 제한적으로 사용합니다. 특정 계층만 검증한다면 @WebMvcTest@DataJpaTest 같은 슬라이스 테스트가 훨씬 빠르고 적절합니다.

Q4. 테스트 커버리지는 몇 %를 목표로 해야 하나요?

숫자 자체보다 “위험한 로직이 덮였는가”가 중요합니다. 단순 getter까지 채우려 무리하기보다, 분기와 예외 처리 등 깨지기 쉬운 부분을 우선 검증하는 편이 실용적입니다.


정리하며

한 줄로 요약하면, 비즈니스 로직은 단위 테스트로 빠르게 덮고 DB·연동 같은 경계만 통합 테스트로 진짜 검증하는 것이 Spring Boot 테스트 전략의 뼈대입니다. Mockito로 의존성을 끊어 핵심 로직을 촘촘히 검증하고, 슬라이스 테스트로 계층을 가볍게 떼어 보며, Testcontainers로 운영과 같은 DB를 띄워 신뢰도를 확보하면 빠른 피드백과 안정성을 함께 가져갈 수 있습니다.

댓글 남기기