소프트웨어 개발에서 테스트는 선택이 아닌 필수입니다. 특히 스프링 부트 기반의 백엔드 개발에서는 다양한 레벨의 테스트를 효율적으로 조합하여 코드의 품질과 안정성을 확보해야 합니다.
이번 글에서는 Spring Boot 환경에서 가장 많이 쓰이는 단위 테스트, 통합 테스트, 슬라이스 테스트를 언제, 어떻게 사용해야 하는지 구체적인 적용 방법을 살펴보려고 합니다.
테스트 개발 방법론의 이해: TDD와 회귀 테스트
A. 테스트 주도 개발 (TDD, Test-Driven Development)
TDD는 '실패하는 테스트를 작성 → 테스트를 통과할 만큼만 최소한의 코드를 작성 → 코드를 리팩토링'의 짧은 주기를 반복하는 개발 방법론입니다.
정의: 테스트를 먼저 작성하고, 그 테스트를 통과하는 프로덕션 코드를 나중에 작성하는 개발 방법론입니다.
효과: 설계 개선 유도, 버그 감소, 명세서 역할 수행.
B. 회귀 테스트 (Regression Testing)
회귀 테스트는 코드 변경 후 기존 기능이 여전히 정상 작동하는지 확인하는 테스트 유형입니다. 새로운 기능을 추가하거나 기존 코드를 수정할 때 사이드 이펙트(Side Effect)를 방지하는 방화벽 역할을 합니다.
테스트의 두 기둥: 단위 테스트 vs. 통합 테스트
테스트는 범위와 속도에 따라 단위 테스트와 통합 테스트로 나뉩니다. 두 가지를 적절히 섞어 쓰는 것이 이상적입니다.
단위 테스트 (Unit Test)
- 범위: 애플리케이션의 가장 작은 단위 (함수, 메서드, 클래스)를 독립적으로 검증합니다.
- 속도: 엄청나게 빠릅니다. (밀리초 단위)
- 특징: DB나 외부 API 같은 의존성은 모두 Mock(가짜 객체)으로 대체하여, 순수하게 해당 로직만 테스트합니다.
통합 테스트 (Integration Test)
- 범위: 서로 상호작용하는 여러 컴포넌트들이 전체적으로 잘 연결되어 동작하는지 확인합니다. (예:
주문이결제와배송시스템에 잘 연동되는지) - 속도: 느립니다. (실제 DB 연결, API 호출 등 리소스를 사용하기 때문입니다.)
- 특징: 실제 환경과 유사한 시나리오로 테스트하여, 구성 요소 간의 상호작용 문제를 발견할 수 있습니다.
@SpringBootTest와 슬라이스 테스트
스프링 부트는 개발자가 테스트를 쉽게 할 수 있도록 다양한 애너테이션을 제공합니다.
A. 전체 통합 테스트: @SpringBootTest
통합 테스트를 실행하려면 스프링 애플리케이션 컨텍스트 전체가 필요합니다. @SpringBootTest는 임베디드 서버를 포함한 전체 애플리케이션 컨텍스트를 초기화하여 실제 환경과 가장 유사하게 테스트할 수 있게 해줍니다.
| webEnviroment 옵션 | 설명 |
| MOCK | 가상의 서블릿 컨테이너를 사용해 빠르고 가볍게 테스트 |
| RANDOM_PORT | 임의의 포트를 열어 실제 HTTP 요청을 보내 엔드포인트를 테스트할 때 사용합니다. |
| NONE | 서블릿 컨테이너 없이 백그라운드 로직만 테스트 |
WebTestClient 예시 (Random Port 사용)
// 전체 콘텍스트 로드 및 임의 포트 사용
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CatalogServiceApplicationTests {
// Rest 엔드포인트를 호출할 유틸리티 (비동기 처리 가능)
private WebTestClient webTestClient;
@Test
void whenPostRequestThenBookCreated() {
var expectedBook = new Book("1231231231", "Title", "Author", 9.90);
webTestClient.post().uri("/books")
.bodyValue(expectedBook)
.exchange()
.expectStatus().isCreated() // 응답 상태 201 확인
.expectBody(Book.class)
.value(actualBook -> {
// AssertJ로 응답 바디의 ISBN 검증
assertThat(actualBook.isbn()).isEqualTo(expectedBook.isbn());
});
}
}
B. 슬라이스 테스트: 특정 계층만 떼어내서 빠르게!
전체 컨텍스트를 로드하는 것은 비용이 큽니다. 스프링 부트는 애플리케이션의 특정 부분만 대상으로 컨텍스트를 초기화하는 슬라이스 테스트(Slice Test) 기능을 제공합니다.
@WebMvcTest: 웹 계층 테스트
웹 MVC 컴포넌트(Controller 등)에 중점을 두고, @Service나 @Repository 같은 비즈니스 로직 빈은 로드하지 않습니다.
- MockMvc: 톰캣 같은 실제 서버를 로드하지 않고 웹 엔드포인트를 테스트할 수 있게 해주는 경량 유틸리티입니다.
// BookController 클래스만 타깃하여 테스트
@WebMvcTest(BookController.class)
public class BookControllerMvcTests {
@Autowired
private MockMvc mockMvc;
// BookService는 로드되지 않으므로, MockBean으로 가짜를 만들어 주입
@MockBean
private BookService bookService;
@Test
void whenGetBookNotExistingThenShouldReturn404() throws Exception {
String isbn = "73737271727";
// MockBean의 동작 정의: 해당 ISBN으로 조회 시 예외 발생시키도록 설정
given(bookService.viewBookDetails(isbn)).willThrow(BookNotFoundException.class);
mockMvc.perform(get("/books/" + isbn))
.andExpect(status().isNotFound()); // 응답 상태 404 확인
}
}
@JsonTest: JSON 직렬화 테스트
도메인 객체가 JSON으로 변환(직렬화)되거나 JSON이 도메인 객체로 돌아올(역직렬화) 때 오류가 없는지 확인합니다.
@JsonTest
class BookJsonTests {
@Autowired
private JacksonTester<Book> json; // JSON 변환 테스트 도구
@Test
void testSerialize() throws Exception {
var book = new Book("1234567890", "Title", "Author", 9.90);
var jsonContent = json.write(book);
// JsonPath를 사용해 JSON 객체의 특정 필드 값 검증
assertThat(jsonContent).extractingJsonPathStringValue("@.isbn").isEqualTo(book.isbn());
}
}
👀 기능 브랜치에서 통합 테스트를 피해야 하는 이유
통합 테스트는 매우 중요하지만, 개발자들이 코드를 자주 푸시하는 기능 브랜치에서는 실행하지 않는 것이 효율적입니다.
- 시간과 비용 소모: 통합 테스트는 느리고 리소스를 많이 잡아먹습니다. 기능 브랜치에서 매번 실행하면 CI/CD 파이프라인이 길어져 개발 피드백 시간이 늦어집니다.
- 생산성 우선: 기능 개발 단계에서는 2분 내외의 빠른 단위 테스트로 로직의 정확성을 검증하고 빠르게 반복하는 것이 생산성에 훨씬 유리합니다.
- 검증 시점 분리: 통합 테스트는 기능이 완성되어 메인 브랜치에 병합(MR/PR)할 때와 실제 배포(Tag) 시점에만 실행하여, 완전한 코드에 대한 최종 점검 용도로 사용하는 것이 가장 효율적이고 안정적입니다.
JUnit5를 활용한 단위테스트
단위 테스트는 JUnit 5를 기반으로 작성하며, Mockito와 AssertJ 같은 라이브러리를 함께 사용합니다.
// build.gradle에 의존성 추가 필요
// JUnit 5, Mokito, AssertJ 포함
testImplementation 'org.springframework.boot:spring-boot-starter-test'
JUnit 5를 이용한 유효성 검사 테스트 예시
public class BookValidationTests {
private static Validator validator;
@BeforeAll // 테스트 클래스가 로드될 때 한 번만 실행
static void setUp(){
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
@Test
void whenAllFieldsCorrectThenValidationSucceeds() {
var book = new Book("1234567890", "Title", "Author", 9.90);
Set<ConstraintViolation<Book>> violations = validator.validate(book);
// AssertJ: violations이 비어 있음을 확인 (성공)
assertThat(violations).isEmpty();
}
@Test
void whenIsbnDefinedButIncorrectThenValidationFails(){
var book = new Book("a234567890", "Title", "Author", 9.90);
Set<ConstraintViolation<Book>> violations = validator.validate(book);
// AssertJ: 위반이 1개인지, 메시지가 예상과 동일한지 확인
assertThat(violations).hasSize(1);
assertThat(violations.iterator().next().getMessage()).isEqualTo("The ISBN format must be valid.");
}
}
이번 글에서는 Spring Boot 테스트코드 전략에 대해 알아보았습니다.
테스트 코드는 곧 우리 코드의 문서이자, 미래의 버그로부터 프로젝트를 지켜줄 든든한 보험과 같습니다.
이번 학습을 통해 얻은 지식을 바탕으로, 저 역시 앞으로 진행할 프로젝트에서 테스트코드 작성을 습관하려고 합니다.
참고 자료
- 클라우드 네이티브 스프링 인 액션
'Backend' 카테고리의 다른 글
| Spring Bean 이란? (1) | 2026.01.05 |
|---|---|
| Maven vs Gradle vs Ant : 빌드 방식의 변화 (0) | 2025.12.27 |
| Springboot 공통 예외처리를 위한 로직 (0) | 2025.12.13 |
| Spring에서 기상청 API 사용하기 (0) | 2023.09.08 |
| Jackson 라이브러리란? (0) | 2023.09.07 |