테스트는 최종 시험대? 아니, 설계 방향 지도!

유수민·2025년 7월 18일
3

나의 이야기

목록 보기
2/2

📌 개요

예전의 나에게 테스트란 단지 ‘구현이 끝난 후, 이상이 없는지 확인하는 마지막 절차’였다.
하지만 지금은 다르다. 테스트는 이제 내가 어떻게 구현해야 할지를 알려주는 설계 지침서다.

이 글에서는 테스트가 내 개발 방식과 사고를 어떻게 바꿔놓았는지, 그리고 내가 생각하는 좋은 테스트란 무엇인지를 이야기해보려 한다

📌 E2E 테스트

E2E(End-to-End) 테스트는 사용자의 요청이 프론트엔드를 거쳐 백엔드와 DB까지 연결되는 전체 흐름을 검증하는 테스트다.

예를 들어, ‘포인트 조회’ 기능을 개발한다고 해보자. 테스트를 먼저 작성하면서 다음과 같은 질문이 자연스럽게 떠오른다:

  1. 어떤 endpoint로 요청할 것인가?
  2. 어떤 요청값이 필요하고, 어떤 응답값을 줄 것인가?

이런 고민을 바탕으로, 먼저 간단한 테스트 코드를 작성하고 mock 응답을 내려주는 방식으로 작업을 시작했다.
이를 통해 프론트엔드 개발자와 백엔드 개발자가 기능 구현 이전에 명확한 요청/응답 계약을 공유하고, 병렬로 작업할 수 있는 기반을 만들 수 있었다.

이 단계에서의 테스트는 실제 DB나 구현 로직 없이도 API의 인터페이스와 흐름을 검증하는 데 목적이 있다.
엄밀히 말하면 이는 ‘Stub 기반 테스트’ 또는 ‘Contract 테스트’에 가깝지만, 이후 실제 구현이 완성되면 자연스럽게 진짜 E2E 테스트로 발전하게 된다.

예전의 나였다면 컨트롤러뿐 아니라 서비스, 레포지토리, 엔티티까지 전부 구현한 후 테스트를 작성했겠지만,
지금은 테스트를 먼저 작성하고, 그 테스트를 통과하는데 필요한 최소한의 구조만 정의하고 흐름에 따라 점진적으로 구현을 확장해나간다.
즉, 이 테스트는 일회용이 아닌, 이후 전체 흐름을 점검하는 완전한 E2E 테스트로 진화하며 끝까지 유지된다.

이처럼 테스트는 ‘기능이 다 구현된 후 확인하는 단계'가 아니라,
처음부터 설계를 이끄는 도구로 사용될 수 있다. 지금 보여준 예시는 아직 2단계까지 구현된 상태이며,
단위 테스트와 통합 테스트를 거쳐, 이 테스트는 완전한 E2E 테스트로 완성될 예정이다.

다음과 같은 단계를 거쳐 테스트를 완성된 E2E로 발전시켜간다.

  • 1단계 – Stub 기반 테스트: 요청/응답 형태를 테스트로 먼저 정의
  • 2단계 – 최소한의 구조 구현: DTO, Controller 등 테스트 통과에 필요한 부분만 작성
  • 3단계 – 점진적 로직 구현: 실제 서비스/도메인 구성 요소 추가
  • 4단계 – 통합된 E2E 테스트: 실제 데이터, DB, 전 구간 테스트

📌 단위 테스트

단위 테스트에서는 핵심 비즈니스 로직에 대한 정합성과 규칙을 검증한다.
예를 들어 ‘포인트 충전’ 기능을 구현하면서, 다음과 같은 규칙을 세웠다:

  • 0 이하의 값은 충전할 수 없다.

이다.

이 로직을 어디에서 검증할지 고민하게 되었고, 결국 request → command로 변환하는 시점에서 유효성을 체크하기로 했다.
이렇게 계층별로 책임을 분리함으로써 테스트 가능성과 유지보수성이 향상되었다.

내가 단위 테스트를 작성하면서 가장 크게 달라진 점이 있다면,

  • 그전에는 중요한 로직을 private 함수에 숨겨놨다.
    → 중요한 비즈니스 로직은 별도 클래스로 분리하여 작은 public 함수로 노출
  • 한 메서드에 여러 책임이 혼재되어 있었다 (생성 + validate 등).
    → 테스트가 명확해지도록 책임을 분리
  • 단, 외부에 무분별하게 public으로 열기보다는 설계를 통해 테스트 가능하게 만들자.

📌 통합테스트

각 기능이 단독으로는 잘 동작하더라도, 실제 서비스에서는 다양한 컴포넌트가 조합되어 동작한다.
통합 테스트는 이러한 구성 요소들이 서로 올바르게 연결되어 있는지, 데이터 흐름에 문제가 없는지를 확인하는 테스트다.

예를 들어, 회원가입 기능에서 다음 두 가지가 모두 성공해야 한다:

  1. 회원 정보가 DB에 저장된다.
  2. 저장된 회원 정보가 응답으로 반환된다.

이 테스트는 UserService.create()를 호출했을 때,

  • UserRepository.save()가 실제로 호출되었는지,
  • 저장된 값이 올바르게 매핑되어 응답으로 반환되었는지를 검증한다.

📌 좋은 테스트란 무엇인가

내가 생각하는 좋은 테스트는 "잘 실패하는 테스트"다.

기능을 수정하거나 리팩토링할 때,
이전에 작성한 테스트가 실패한다면 자연스럽게 이렇게 생각하게 된다:

"이번 변경이 기존 기능에 어떤 영향을 미친 걸까?"
"어디서 잘못된 흐름이 발생했지?"

이런 고민을 통해 전체 구조와 책임을 다시 한 번 되짚어보게 되고,그 과정은 곧 코드를 더욱 견고하게 다듬는 시간이 된다.

잘 실패하는 테스트는 결국
“코드를 되돌아보게 만드는 테스트”다.

테스트가 깨지면 무조건 그 원인을 분석하게 되고,
그 분석 과정에서 설계, 구조, 책임의 균형을 검토하게 된다.이것이 내가 테스트를 사랑하게 된 이유이자,
테스트의 진짜 가치라고 믿는 부분이다.

내가 생각하는 좋은 테스트의 조건

  • 잘 실패한다: 코드 변경이 기존 동작에 어떤 영향을 미치는지 즉시 드러난다.
  • 설계를 이끈다: 테스트를 먼저 작성하면서 구조와 책임이 명확해진다.
  • 신뢰할 수 있다: 반복 실행 시 항상 같은 결과를 보장한다.
  • 빠르다: 실행 속도가 빨라, 언제든 부담 없이 돌려볼 수 있다.

📌 결론

우리는 매일 많은 기능을 만들고, 그 기능들은 수많은 사용자가 경험하게 된다.
그렇기에 테스트는 단순한 ‘확인’의 수단이 아니라, 설계의 시작점이자 유지보수의 안전망이 되어야 한다.

TDD에서 말하는 "Driven"은 단순히 테스트를 먼저 쓴다는 의미가 아니다.
진짜 핵심은, 테스트가 코드의 방향, 책임 분리, 인터페이스 구조를 이끌어가는 힘이라는 점이다.

이제 테스트는 ‘시험’이 아니라 ‘설계자’다.
즉, 테스트는 단지 '기능을 확인하는 수단'이 아니다.
테스트는 설계를 이끌고, 유지보수성을 확보하며, 개발자의 자신감을 만들어주는 자신감의 근거가 된다고 생각한다.

profile
배우는 것이 즐겁다!

0개의 댓글