
워밍업 클럽 3기 BE 클린코드&테스트 - 3주차 발자국
“작동하는 코드는 끝이 아니라 시작이다. 진짜 개발자는 ‘테스트 가능한 설계’에서부터 출발한다.”
1단계: Persistence Layer 테스트 – 단위의 본질을 의심하라
JPA 테스트는 대부분 @DataJpaTest
로 간단히 시작된다. 하지만 내가 여기에 의문을 품기 시작한 건 다음과 같은 경험 때문이다
엔티티 간의 복잡한 연관관계 (특히 LAZY 로딩) 때문에 테스트 시점에서
LazyInitializationException
이 자주 터졌다.단순한 CRUD 테스트는 통과했는데, 실제 서비스에서는 N+1 문제나 flush 타이밍 이슈가 터졌다.
그래서 Persistence 테스트에서 다음과 같은 원칙을 세웠다:
단위 테스트로 만족하지 마라. 연관관계와 Fetch 전략까지 검증하라.
실제
@Transactional
환경에서의 쿼리 생성을 직접 로그로 확인하라.@DataJpaTest보단 통합 테스트에 가까운 Repository 테스트를 설계하라.
→ 이 경험은 설계할 때부터 “테스트 가능한 엔티티 구조인가?”를 고민하게 만들어줬다.
2단계: Business Layer 테스트 – Mock보다 책임을 분리하라
Service 레이어 테스트에서는 보통 @MockBean
을 통해 Repository를 가짜로 주입하고 시작한다. 하지만 실제 테스트를 하면서 난 이렇게 깨달았다:
Mock이 많아질수록 테스트는 ‘신뢰’가 아니라 ‘세팅’으로 변질된다.
비즈니스 로직이 애매하게 도메인 모델과 얽혀 있으면 테스트가 지나치게 복잡해진다.
그래서 Service 레이어 테스트에서 내가 택한 전략은 다음과 같다.
의존성 주입보다 ‘책임 분리’에 집중하자.
유틸성 도메인 서비스는 과감하게 별도 클래스로 분리했다.
트랜잭션이 필요한 핵심 로직만 핵심 Service에 남겼다.
Mock은 ‘부작용 있는 의존성’에만 최소한으로 사용한다.
외부 API 호출, 파일 처리 등 순수하지 않은 작업에만 Mock 사용.
나머지는 가능하면 In-Memory DB를 통한 통합 테스트로 전환.
→ 이 과정에서 테스트 설계 자체가 코드 구조 개선으로 이어졌다. 테스트는 곧 설계의 거울이다.
3단계: Presentation Layer 테스트 – Controller 테스트는 시나리오다
Controller 테스트는 예전엔 단순히 @WebMvcTest
로 시작했지만, 실제 프로젝트에서 다양한 인가/인증 이슈를 겪으면서 이렇게 정리하게 되었다.
Controller 테스트는 단위 테스트가 아니라 시나리오 테스트다.
사용자의 행위 흐름을 기준으로 경로를 검증하라.
예외 케이스(잘못된 입력, 인증 오류 등) 중심으로 설계하라.
MockMvc로 충분한가? 아니면 통합 테스트로 전환해야 하는가?
Security 적용 후 로그인 기반 테스트를 위해
@SpringBootTest + TestRestTemplate
을 사용.인증 흐름도 테스트 시나리오 안에 포함되도록 구성.
DTO 검증은 Bean Validation으로 끝내지 마라.
검증 오류 발생 시 API 응답이 일관되게 포맷되는지, 실제 클라이언트 입장에서 설계하라.
→ 사용자의 흐름을 기반으로 테스트를 설계하면서, 오히려 API 설계가 더 명확해졌고 팀원들과의 커뮤니케이션도 쉬워졌다.
4단계: Mock을 줄이고 실제 DB를 활용한 테스트를 할 때, 성능과 신뢰성 사이의 균형은 어떻게 잡아야 할까?
실제 DB를 활용한 테스트(In-Memory 또는 TestContainer 기반 통합 테스트)는 신뢰성 측면에서 분명 강점이 있다. 하지만 테스트 속도가 느려지고, 유지보수도 까다로워진다는 단점이 따른다. 이 균형은 "테스트 목적에 따른 전략적 분리"로 해결할 수 있다.
✅ 1. 테스트 목적에 따라 레벨을 명확히 나누기
테스트 레벨목표특징Unit Test로직 단위 검증빠름, Mock 적극 사용Slice TestSpring Context 일부 + 실제 Bean@DataJpaTest
, @WebMvcTest
등 사용Integration Test실제 환경과 유사한 흐름 검증느림, 신뢰도 높음
→ 단위 테스트(Unit)는 빠르게, 통합 테스트(Integration)는 신뢰 높게하자. 각각의 역할을 명확히 구분하는 게 핵심이다.
✅ 2. 테스트 커버리지를 ‘리스크 중심’으로 관리하기
전체 커버리지를 100%로 맞추는 건 비효율적이다. 나는 아래 기준으로 실제 DB를 쓰는 테스트를 판단하는게 좋다고 생각한다.
DB 연산이 중요한 의미를 가지는 도메인
→ ex) 복잡한 조건의 쿼리, QueryDSL 동적 쿼리, JPA 연관관계 테스트트랜잭션과 영속성 컨텍스트 동작에 따라 결과가 달라지는 로직
→ ex) dirty checking, flush, cascade 등API 시나리오 상 치명적인 실패가 발생할 수 있는 핵심 흐름
→ ex) 주문 생성, 결제 처리 등
위 영역은 느려도 통합 테스트로 보장하고, 나머지는 단위 테스트로 빠르게 검증한다.
✅ 내 결론: “가짜로 빠르게 vs. 진짜로 느리게”가 아니라, “목적 중심으로 정교하게”
단순히 속도만을 쫓아 Mock으로 도배된 테스트는 어느 순간부터 ‘돌아는 가는데, 신뢰할 수 없는 테스트’가 된다. 반대로 모든 테스트를 실제 DB로 돌리는 건 느리고 관리가 어렵다.
그래서 나는 이렇게 정리했다:
"테스트의 목적은 속도도, 커버리지도 아닌 ‘신뢰’다. 그리고 신뢰는 목적에 따라 얻을 수 있다."
마무리하며 – 테스트는 개발자의 언어다
나는 테스트를 단지 “코드가 잘 돌아가는지 확인하는 수단”으로 보지 않는다. 테스트는 설계를 검증하는 도구이자, 개발자가 소통하는 언어라고 생각한다. 이 기준을 가지고 나면 단순히 "커버리지 높이기"가 아니라 "의도 명확하게 만들기"에 집중하게 된다.
앞으로도 나는 테스트를 통해 설계하고, 설계를 통해 테스트를 더 단단히 할 것이다.
댓글을 작성해보세요.