티스토리 뷰
Spring Boot 기반 애플리케이션을 운영하다 보면 테스트 코드의 비중은 점점 커집니다. 초기에는 기능 구현이 우선이지만, 서비스가 확장되고 배포 주기가 빨라질수록 테스트 설계는 곧 아키텍처의 일부가 됩니다. 이 과정에서 자주 등장하는 개념이 바로 테스트 피라미드입니다.
하지만 테스트 피라미드는 단순한 “비율 공식”이 아니라, 테스트를 어떻게 설계할 것인가에 대한 사고의 틀에 가깝습니다. 이번 글에서는 Mockito 사용법을 넘어, Spring Boot 환경에서 테스트를 어떻게 구조적으로 설계해야 하는지 정리해보고자 합니다.
테스트 피라미드의 개념과 흔한 오해
테스트 피라미드는 일반적으로 단위 테스트가 가장 많고, 그 위에 통합 테스트, 최상단에 E2E 테스트가 위치하는 구조를 의미합니다. 이 개념은 테스트의 실행 속도와 유지보수 비용을 고려한 구조적 제안입니다.
다만 실무에서는 이를 “단위 테스트 80%, 통합 테스트 20%” 같은 비율 공식으로 오해하는 경우가 있습니다. 하지만 테스트 피라미드는 숫자를 제시하는 모델이 아닙니다. 핵심은 다음과 같습니다.
- 빠르게 실행되는 테스트가 많아야 한다는 점
- 느리고 비용이 높은 테스트는 전략적으로 최소화해야 한다는 점
- 각 계층의 테스트가 검증하는 책임이 다르다는 점
이 관점에서 보면, 테스트 피라미드는 비율이 아니라 검증 책임의 분리 구조라고 이해하는 편이 더 실용적입니다.
Spring Boot에서 테스트 피라미드를 구현하는 방법
Spring Boot 환경에서는 테스트 유형이 비교적 명확하게 구분됩니다.
- Mockito 기반 단위 테스트
- 슬라이스 테스트 (@WebMvcTest, @DataJpaTest)
- 전체 컨텍스트 기반 통합 테스트 (@SpringBootTest)
Spring Boot 공식 레퍼런스 문서에서는 테스트 슬라이스 어노테이션을 통해 필요한 계층만 로딩하도록 지원합니다. 이는 테스트 속도와 독립성을 확보하기 위한 설계입니다.
중요한 점은, 이 어노테이션들이 “편의 기능”이 아니라 테스트 피라미드를 구현하기 위한 도구라는 점입니다. 어떤 테스트를 작성할지 결정하는 것은 어노테이션이 아니라 설계 관점입니다.
Mockito 기반 단위 테스트의 역할과 한계
Mockito는 공식 문서에서 “mocking framework for unit tests”로 정의됩니다. 핵심 목적은 협력 객체를 대체하여 테스트 대상을 고립시키는 것입니다.
단위 테스트의 본질은 다음에 있습니다.
- Spring 컨텍스트를 로딩하지 않는다.
- 외부 의존성을 mock으로 대체한다.
- 순수 자바 객체 수준에서 로직을 검증한다.
이 접근은 매우 빠르며, CI 환경에서도 부담이 거의 없습니다. 하지만 동시에 명확한 한계도 존재합니다.
- 실제 Bean wiring을 검증하지 않는다.
- 트랜잭션 동작을 확인할 수 없다.
- JPA 매핑 오류를 발견할 수 없다.
따라서 Mockito를 잘 쓰는 것만으로는 충분하지 않습니다. 중요한 것은 “어디까지를 단위 테스트의 책임으로 둘 것인가”를 정의하는 일입니다. 단위 테스트는 로직의 분기와 도메인 규칙을 검증하는 데 집중하는 것이 바람직합니다.
@WebMvcTest와 @DataJpaTest의 위치
@WebMvcTest는 Spring MVC 계층만 로딩합니다. Controller, ControllerAdvice, Jackson 설정 등을 포함하지만, Service나 Repository는 기본적으로 로딩하지 않습니다.
이는 HTTP 요청과 응답 변환, validation, 예외 처리 흐름을 검증하기에 적합합니다. 반면 비즈니스 로직은 mock 처리하는 것이 일반적입니다.
@DataJpaTest는 JPA 관련 컴포넌트만 로딩하며, 기본적으로 인메모리 데이터베이스를 사용합니다. 이는 다음을 검증하는 데 의미가 있습니다.
- Entity 매핑 정확성
- JPQL 또는 Query Method 동작
- Repository 레벨의 쿼리 결과
이 두 테스트는 테스트 피라미드의 중간층에 위치합니다. 단위 테스트보다는 느리지만, 전체 컨텍스트 테스트보다는 훨씬 빠릅니다. 또한 실제 Spring 구성 일부를 검증한다는 점에서 단위 테스트가 놓치는 부분을 보완합니다.
@SpringBootTest는 몇 개가 적절한가
@SpringBootTest는 전체 애플리케이션 컨텍스트를 로딩합니다. 자동 설정이 모두 적용되며, 실제 Bean wiring, AOP, 트랜잭션 등이 동작합니다.
공식 문서에서도 이 테스트는 가장 무거운 방식으로 설명됩니다. 실행 속도가 느리고, 컨텍스트 초기화 비용이 큽니다.
그렇다면 몇 개가 적절할까요.
정답은 숫자가 아니라 목적입니다. 다음을 검증해야 할 때는 필요합니다.
- 실제 Bean 조합이 올바르게 동작하는지
- 보안 설정이 정상 적용되는지
- 트랜잭션 경계가 의도대로 동작하는지
- 외부 시스템과의 통합 설정이 정상인지
하지만 모든 Service를 @SpringBootTest로 검증하는 것은 피라미드를 뒤집는 결과가 됩니다. 유지보수 비용이 급격히 증가하고, CI 시간이 길어지며, 테스트 피드백이 늦어집니다.
실무에서는 핵심 시나리오 중심으로 제한적으로 유지하는 것이 현실적인 전략이라고 생각합니다.
테스트 속도와 빌드 파이프라인의 관계
테스트 속도는 단순한 개발 편의 문제가 아닙니다. 이는 배포 속도와 직접 연결됩니다.
테스트가 1분 걸리는 프로젝트와 15분 걸리는 프로젝트는 CI 운영 방식이 달라집니다. 빌드가 느리면 개발자는 테스트 실행을 꺼리게 되고, 작은 변경에도 부담을 느끼게 됩니다.
빠른 단위 테스트가 많다는 것은, 코드 변경 후 즉시 피드백을 받을 수 있다는 의미입니다. 이는 리팩토링의 심리적 비용을 낮춥니다.
테스트 피라미드의 가장 중요한 현실적 의미는 바로 여기에 있습니다. 빠른 테스트가 많아야 지속 가능한 개발이 가능합니다.
CI 환경에서의 테스트 전략
CI 환경에서는 모든 테스트를 동일한 시점에 실행할 필요는 없습니다.
예를 들어 다음과 같은 전략이 가능합니다.
- PR 단계: 단위 테스트 + 슬라이스 테스트
- main 브랜치 머지 시: 전체 테스트 실행
- 배포 직전: 핵심 통합 테스트 + E2E 테스트
이처럼 테스트 계층을 분리하면, 피드백 속도와 안정성을 동시에 확보할 수 있습니다.
테스트 설계는 코드 구조뿐 아니라 조직의 배포 전략과도 연결됩니다. 이 점에서 테스트는 기술적 문제이면서 동시에 프로세스 설계의 문제이기도 합니다.
커버리지 수치에 집착하는 것의 한계
커버리지 100%는 매력적인 숫자입니다. 하지만 커버리지는 “코드가 실행되었는지”를 보여줄 뿐, “의미 있게 검증되었는지”를 보장하지는 않습니다.
Mockito로 모든 협력 객체를 mock하고, 단순 호출 여부만 검증하면 커버리지는 쉽게 올라갑니다. 하지만 이는 시스템 안정성을 보장하지 않습니다.
중요한 것은 다음 질문입니다.
- 이 테스트가 실패하면 무엇을 알려주는가
- 이 테스트가 시스템의 어떤 계약을 보호하는가
커버리지는 참고 지표일 뿐, 목표가 되어서는 안 된다고 생각합니다.
테스트를 어떻게 설계해야 하는가
Mockito를 잘 쓰는 방법보다 더 중요한 것은, 테스트의 경계를 설계하는 일입니다.
- 어디까지를 단위로 볼 것인가
- 어떤 계층에서 무엇을 검증할 것인가
- 어떤 위험을 어떤 테스트가 방어하는가
Spring Boot는 다양한 테스트 도구를 제공합니다. 하지만 그 도구를 어떤 비율로 사용할지는 팀의 설계 철학에 달려 있습니다.
테스트 피라미드는 비율을 강제하는 규칙이 아니라, 테스트 책임을 분산시키는 구조적 사고라고 이해하는 것이 실무에 더 가깝다고 느낍니다.
마무리하며
테스트 피라미드는 단순한 이상적인 그림이 아니라, 지속 가능한 개발을 위한 구조적 제안입니다.
Mockito 사용 숙련도는 중요합니다. 하지만 그것이 테스트 전략의 전부는 아닙니다. 더 중요한 것은 어떤 테스트가 어떤 리스크를 줄이는지 이해하는 일입니다.
Spring Boot 환경에서 단위 테스트, 슬라이스 테스트, 통합 테스트를 균형 있게 배치하는 일은 숫자의 문제가 아니라 설계의 문제입니다.
결국 테스트는 코드의 안전망이자, 팀의 배포 속도를 결정하는 요소입니다. 테스트 피라미드를 “비율”이 아닌 “설계 관점”으로 바라볼 때, 비로소 실무에서 의미 있는 전략이 된다고 생각합니다.
'STUDY' 카테고리의 다른 글
| 분산 트랜잭션 환경에서 Spring Batch 재처리와 Saga 패턴 정리 (1) | 2026.02.27 |
|---|---|
| Spring Batch 실패 처리와 Retry 설계: DB 쓰기와 API 호출의 차이 (0) | 2026.02.27 |
| Mockito 내부 동작 원리 완전 정리: Bytecode 조작과 Mock Maker, 그리고 테스트 설계 철학 (0) | 2026.02.26 |
| Spring Boot 테스트 슬라이스 완전 정리: @WebMvcTest, @DataJpaTest 동작 원리와 Mockito의 역할 (0) | 2026.02.26 |
| Spring Boot에서 Mockito 단위 테스트와 @SpringBootTest의 차이 정리: 언제 무엇을 사용해야 할까 (0) | 2026.02.26 |
- Total
- Today
- Yesterday
- Java Performance
- mybatis
- Cache Avalanche
- Hot Key 문제
- 캐시와 인덱스
- 스레드 생명주기
- 백엔드 성능 설계
- Enum 기반 싱글톤
- 동시성처리
- Initialization-on-Demand Holder Idiom
- Cache Penetration
- Redis 캐시 전략
- 백엔드 성능
- DB 인덱스 성능
- spring batch 5
- 캐시 성능 비교
- DB 트랜잭션
- 트래픽 처리
- 백엔드 성능 튜닝
- Cache Aside
- 트랜잭션 관리
- Redis 성능 개선
- Double-Checked Locking
- Redis vs DB
- 캐시 장애
- 백엔드 아키텍처
- Eager Initialization
- InterruptedException
- Spring Batch
- TTL 설계
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |

