🔥딱 8일간! 인프런x토스x허먼밀러 역대급 혜택

워밍업 클럽 3기 BE 클린코드&테스트 - 3주차 발자국

워밍업 클럽 3기 BE 클린코드&테스트 - 3주차 발자국

“작동하는 코드는 끝이 아니라 시작이다. 진짜 개발자는 ‘테스트 가능한 설계’에서부터 출발한다.”

1단계: Persistence Layer 테스트 – 단위의 본질을 의심하라

JPA 테스트는 대부분 @DataJpaTest로 간단히 시작된다. 하지만 내가 여기에 의문을 품기 시작한 건 다음과 같은 경험 때문이다

  • 엔티티 간의 복잡한 연관관계 (특히 LAZY 로딩) 때문에 테스트 시점에서 LazyInitializationException이 자주 터졌다.

  • 단순한 CRUD 테스트는 통과했는데, 실제 서비스에서는 N+1 문제나 flush 타이밍 이슈가 터졌다.

그래서 Persistence 테스트에서 다음과 같은 원칙을 세웠다:

  1. 단위 테스트로 만족하지 마라. 연관관계와 Fetch 전략까지 검증하라.

  2. 실제 @Transactional 환경에서의 쿼리 생성을 직접 로그로 확인하라.

  3. @DataJpaTest보단 통합 테스트에 가까운 Repository 테스트를 설계하라.

→ 이 경험은 설계할 때부터 “테스트 가능한 엔티티 구조인가?”를 고민하게 만들어줬다.


2단계: Business Layer 테스트 – Mock보다 책임을 분리하라

Service 레이어 테스트에서는 보통 @MockBean을 통해 Repository를 가짜로 주입하고 시작한다. 하지만 실제 테스트를 하면서 난 이렇게 깨달았다:

  • Mock이 많아질수록 테스트는 ‘신뢰’가 아니라 ‘세팅’으로 변질된다.

  • 비즈니스 로직이 애매하게 도메인 모델과 얽혀 있으면 테스트가 지나치게 복잡해진다.

그래서 Service 레이어 테스트에서 내가 택한 전략은 다음과 같다.

  1. 의존성 주입보다 ‘책임 분리’에 집중하자.

    • 유틸성 도메인 서비스는 과감하게 별도 클래스로 분리했다.

    • 트랜잭션이 필요한 핵심 로직만 핵심 Service에 남겼다.

  2. Mock은 ‘부작용 있는 의존성’에만 최소한으로 사용한다.

    • 외부 API 호출, 파일 처리 등 순수하지 않은 작업에만 Mock 사용.

    • 나머지는 가능하면 In-Memory DB를 통한 통합 테스트로 전환.

→ 이 과정에서 테스트 설계 자체가 코드 구조 개선으로 이어졌다. 테스트는 곧 설계의 거울이다.


3단계: Presentation Layer 테스트 – Controller 테스트는 시나리오다

Controller 테스트는 예전엔 단순히 @WebMvcTest로 시작했지만, 실제 프로젝트에서 다양한 인가/인증 이슈를 겪으면서 이렇게 정리하게 되었다.

  1. Controller 테스트는 단위 테스트가 아니라 시나리오 테스트다.

    • 사용자의 행위 흐름을 기준으로 경로를 검증하라.

    • 예외 케이스(잘못된 입력, 인증 오류 등) 중심으로 설계하라.

  2. MockMvc로 충분한가? 아니면 통합 테스트로 전환해야 하는가?

    • Security 적용 후 로그인 기반 테스트를 위해 @SpringBootTest + TestRestTemplate을 사용.

    • 인증 흐름도 테스트 시나리오 안에 포함되도록 구성.

  3. 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로 돌리는 건 느리고 관리가 어렵다.

그래서 나는 이렇게 정리했다:

"테스트의 목적은 속도도, 커버리지도 아닌 ‘신뢰’다. 그리고 신뢰는 목적에 따라 얻을 수 있다."


마무리하며 – 테스트는 개발자의 언어다

나는 테스트를 단지 “코드가 잘 돌아가는지 확인하는 수단”으로 보지 않는다. 테스트는 설계를 검증하는 도구이자, 개발자가 소통하는 언어라고 생각한다. 이 기준을 가지고 나면 단순히 "커버리지 높이기"가 아니라 "의도 명확하게 만들기"에 집중하게 된다.

앞으로도 나는 테스트를 통해 설계하고, 설계를 통해 테스트를 더 단단히 할 것이다.

댓글을 작성해보세요.

채널톡 아이콘