Spring Boot 테스트 코드 작성
한눈에 보기
Spring Boot 테스트는 컨테이너를 어디까지 띄우느냐에 따라 통합 테스트와 슬라이스 테스트로 나뉜다. @SpringBootTest, @WebMvcTest, @DataJpaTest 활용법과 컨텍스트 캐싱 주의점을 다룬다.
최종 업데이트 2026.09.17
Spring Boot 테스트 코드는 스프링 컨테이너를 어디까지 띄우느냐에 따라 통합 테스트와 슬라이스(Slice) 테스트로 나뉜다. @SpringBootTest는 전체 애플리케이션 컨텍스트(ApplicationContext)를 로딩해 실제 운영 환경에 가까운 검증을 제공하고, @WebMvcTest나 @DataJpaTest 같은 슬라이스 애너테이션(Annotation)은 특정 계층에 필요한 빈(Bean)만 로딩해 빠른 피드백을 제공한다. 좋은 테스트 전략은 이 둘과 순수 단위 테스트를 목적에 맞게 조합하는 것이다.
어디까지 띄울 것인가
테스트의 신뢰도와 속도는 반비례하는 경향이 있다. 컨테이너 없이 Mockito로 의존성을 대역(Mock) 처리하는 단위 테스트가 가장 빠르고, 전체 컨텍스트에 실제 데이터베이스까지 붙이는 통합 테스트가 가장 느리지만 신뢰도가 높다. 따라서 도메인 로직은 스프링 없이 순수 단위 테스트로, 각 계층의 설정과 매핑은 슬라이스 테스트로, 전체 흐름은 소수의 통합 테스트로 검증하는 피라미드 구조가 권장된다. Spring Boot 3.x는 JUnit 5를 기본으로 하며, 테스트 클래스에 별도 확장 선언 없이 부트 테스트 애너테이션만 붙이면 스프링 확장이 함께 적용된다.
주요 테스트 애너테이션
| 애너테이션 | 로딩 범위 | 주 용도 |
|---|---|---|
| @SpringBootTest | 전체 컨텍스트 | 여러 계층을 관통하는 통합 테스트 |
| @WebMvcTest | 웹 계층(컨트롤러, 필터, 컨버터) | 요청 매핑, 검증, 직렬화 테스트 |
| @DataJpaTest | JPA 관련 빈과 리포지토리 | 엔티티 매핑, 쿼리 메서드 테스트 |
| @JsonTest | JSON 직렬화 컴포넌트 | ObjectMapper 매핑 검증 |
| @RestClientTest | HTTP 클라이언트 빈 | 외부 API 호출 코드 테스트 |
슬라이스 테스트는 해당 계층 밖의 빈을 로딩하지 않으므로, 컨트롤러가 의존하는 서비스처럼 빠진 빈은 대역으로 채워야 한다. Spring Boot 3.4부터는 @MockBean 대신 @MockitoBean이 표준 명칭이며, 역할은 같다.
컨트롤러 테스트: @WebMvcTest와 MockMvc
웹 계층 테스트는 서버를 실제로 띄우지 않고 MockMvc로 디스패처 서블릿(DispatcherServlet)에 가짜 요청을 흘려 보내는 방식으로 작성한다.
@WebMvcTest(MemberController.class)
class MemberControllerTest {
@Autowired
MockMvc mockMvc;
@MockitoBean
MemberService memberService;
@Test
void 회원_단건_조회에_성공한다() throws Exception {
given(memberService.findById(1L))
.willReturn(new MemberResponse(1L, "김개발"));
mockMvc.perform(get("/api/members/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("김개발"));
}
}
이 방식은 요청 매핑, 파라미터 바인딩, 예외 처리기, JSON 직렬화까지 웹 계층의 책임을 실제 스프링 MVC 구성으로 검증하면서도 서비스 로직과는 분리된다는 장점이 있다.
영속성 계층 테스트: @DataJpaTest
@DataJpaTest는 기본적으로 내장 메모리 데이터베이스로 교체해 실행하고, 각 테스트를 트랜잭션으로 감싼 뒤 끝나면 롤백해 테스트 간 격리를 보장한다.
@DataJpaTest
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Autowired
TestEntityManager em;
@Test
void 이름으로_회원을_조회한다() {
em.persist(new Member("김개발"));
em.flush();
em.clear();
List<Member> result = memberRepository.findByName("김개발");
assertThat(result).hasSize(1);
}
}
flush와 clear로 영속성 컨텍스트를 비워야 1차 캐시가 아닌 실제 쿼리 실행을 검증할 수 있다는 점에 주의한다. 운영 데이터베이스와 같은 환경에서 검증하고 싶다면 @AutoConfigureTestDatabase(replace = Replace.NONE)와 Testcontainers를 조합해 실제 MySQL이나 PostgreSQL 컨테이너로 테스트한다. 방언(Dialect) 차이로 내장 데이터베이스에서만 통과하는 쿼리를 걸러낼 수 있기 때문이다.
컨텍스트 캐싱과 주의할 점
스프링 테스트 프레임워크는 같은 설정의 컨텍스트를 한 번만 띄우고 테스트 클래스 간에 재사용하는 컨텍스트 캐싱을 제공한다. 그런데 @MockitoBean 조합, @ActiveProfiles, properties 속성이 다르면 별도의 캐시 키가 되어 컨텍스트가 새로 뜬다. 테스트 클래스마다 설정이 제각각이면 빌드 시간에서 컨텍스트 기동이 큰 비중을 차지하게 되므로, 공통 설정을 추상 클래스나 메타 애너테이션으로 통일하는 것이 좋다. @DirtiesContext는 컨텍스트를 오염시켰을 때 캐시를 버리게 하는 장치이지만 남용하면 캐싱 이점이 사라진다.
실제 서버 구동이 필요한 경우에는 @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)로 임의 포트에 서버를 띄우고 TestRestTemplate이나 WebTestClient로 호출한다. 고정 포트는 병렬 빌드에서 충돌하므로 피한다.
면접에서 이렇게 물어본다
- 단위 테스트, 슬라이스 테스트, 통합 테스트를 어떤 기준으로 나누어 작성하는지 설명해 보라.
- @WebMvcTest가 @SpringBootTest와 비교해 무엇을 로딩하고 무엇을 로딩하지 않는지 말해 보라.
- @DataJpaTest에서 flush와 clear를 호출하는 이유를 영속성 컨텍스트와 연결해 설명해 보라.
- 스프링 테스트의 컨텍스트 캐싱 동작과 캐시가 재사용되지 못하는 조건을 말해 보라.
- 내장 메모리 데이터베이스 대신 Testcontainers를 쓰는 이유를 설명해 보라.
Spring / JPA에서 같이 읽기
- JPA(Java Persistence API)JPA는 자바 객체와 관계형 데이터베이스 테이블의 매핑을 표준으로 정의한 ORM 명세다. 명세와 구현체의 관계, 영속성 컨텍스트의 기능, 엔티티 생명주기, N+1 문제까지 면접 관점에서 정리한다.
- 더티 체킹(Dirty Checking)더티 체킹은 영속성 컨텍스트가 엔티티 변경을 감지해 플러시 시점에 UPDATE 쿼리를 자동 생성하는 JPA 기능이다. 스냅샷 비교 원리, 플러시 시점, 준영속 엔티티와 벌크 연산의 주의점을 다룬다.
- SpringApplication과 부트스트랩 과정SpringApplication은 스프링 부트의 main 메서드에서 부트스트랩을 총괄하는 클래스다. run() 내부의 환경 준비, 컨텍스트 생성, refresh, 러너 실행 순서와 자동 구성 원리를 설명한다.
- 스프링 빈 스코프(Bean Scope)빈 스코프는 스프링 컨테이너가 빈을 언제 생성하고 어디까지 공유할지 정하는 규칙이다. 싱글턴과 프로토타입의 동작 차이, 웹 스코프, 스코프 혼용 시 문제와 ObjectProvider, 프록시 해결법을 정리한다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기