NestJS

nestjs와 spec.ts 그리고 테스트코드

sihanni 2025. 10. 8. 16:21

1. Nest에서 의존성과 의존성 주입이란?

의존성(Dependency)

의존성이란, 어떤 객체가 동작하기 위해 다른 객체를 필요로 하는 관계를 말한다.

class UserServuce {
	private userRepository = new UserRepository();
}

위의 코드는 UserService가 UserRepository를 직접 생성해서 사용하고 있는데, 이것은 지금 UserService에서 UserRepository에 의존하고 있는 것이다.

하지만 이렇게 사용하면 테스트하기 어렵고(UserRepository를 Mock로 바꾸기가 힘듦), 확장하기 어렵고 (DB를 Mongo -> MySQL로 바꾸려면 코드 수정이 필요), 결합도가 높다(한 쪽이 바뀌면 다른 쪽도 수정이 필요함).

그래서 이런 문제점 해결을 위해 의존성 주입(Dependency Injection) 개념이 나오게 되었다.

의존성 주입은 객체가 스스로 의존 객체를 만들지 않고, 외부에서 대신 넣어주는 것을 말한다.

class UserService {
  constructor(private readonly userRepository: UserRepository) {}
}

이제 UserService에서는 UserRepository가 들어올 것이라는 기대를 가지고 직접 생성하지 않는다.

생성의 책임을 외부로 넘긴 것이다.

 

NestJS는 객체 지향 + 함수형 + 모듈러 아키텍쳐를 추구하는 프레임 워크이다.

여기서 의존성 주입은 느슨한 결합을 실현하는 핵심 메커니즘이 된다.

NestJS는 내부에 IoC 컨테이너 (Inversion of Control Container)를 두고 모든 Provider(Service, Repository, Guard, Pipe 등)의 생명주기를 관리한다.

정리하면 NestJS에서는 어떤 클래스가 필요로 하는 다른 클래스를 직접 생성하지 않아도, Nest가 대신 생성하고 연결(주입)을 해준다는 것이다. 그래서 그 덕분에 Nest앱은 와전히 자동 조립형 구조가 될 수 있는 것이다.

 

2. Nest에서 Spec과 테스트 코드란?

spec.ts 파일은 NestJS에서 프로젝트를 생성하면 자동으로 생성될 만큼 뭔가 중요하다 싶다.

이 spec.ts 파일은 NestJS 프로젝트에서 테스트 코드를 작성하기 위해 존재한다. NestJS는 기본적으로 Jest라는 테스트 프레임워크를 사용하며, spec.ts 라는 파일명은 Jest가 테스트파일임을 식별하는 데 사용하는 명명 규칙이다.

"구현이 이렇게 동작해야 한다"라는 사양(specification)을 자연어처럼 기술하는 문서이자 실행 가능한 테스트이다.

Nest 만의 특화된 개념으로는,

  • TestingModule : Nest의 DI컨테이너를 테스트용으로 구성해 주입/대체를 쉽게 해주는 모듈
  • 의존성 대체 (override) : 실제 Provider를 Mock/Stub/Fake로 바꿔 끼워 테스트 안정성과 속도를 확보
  • 테스트 스코프 : 전역(Global)모듈, 인터셉터/가드/파이프 등을 테스트 모듈 단위로 얹었다가 떼는 방식으로 제어

spec.ts를 사용하는 주요 이유로는,

  • 자동 테스트 실행
    npm run test 와 같은 명령어를 통해 Jest는 프로젝트 내의 .spec.ts 파일을 자동으로 찾아 테스트 케이스들을 실행한다.
  • 코드의 신뢰성 확보
    테스트 코드는 실제 애플리케이션의 컨트롤러, 서비스, 모듈 등 핵심 구성 요소들이 의도한 대로 동작하는지를 검증한다.
    이를 통해 버그를 조기에 발견하고, 리팩토링이나 기능 추가 시 기존 기능이 망가지지 않도록 보장하여 코드의 신뢰도를 높여준다.
  • 단위 테스트 (Unit Test)
    개별 컴포넌트(서비스의 특정 메서드, 컨트롤러 등)를 격리하여 테스트를하는 데 사용된다.
    NestJS의 강력한 의존성 주입 시스템은 외부 의존성을 mock 처리하여 특정 단위만 독립적으로 테스트하기 쉽게 해준다.
    해당 기능이 가져야할 명세(spec)을 만족하는지를 테스트 하는 것

왜 테스트를 쓰나요?

 

  • 회귀(Regression) 방지: 리팩토링/버전업 때 기능이 깨지지 않았는지 빠르게 확인.
  • 설계 품질 개선: 테스트 가능한 구조(느슨한 결합·DI·작은 책임)를 자연스레 유도.
  • 명세의 문서화: 테스트 이름이 곧 사양. 새 팀원이 의도를 빠르게 이해.
  • 자신감 있는 배포: PR/릴리즈 파이프라인에서 자동으로 품질 바리케이드 역할.
  • 디버깅 단축: 실패 시 정확히 어디가 잘못됐는지 핀포인트로 드러남.
  • 리스크 관리: 복잡한 도메인 규칙(결제, 재고, 권한 등)에서 의도치 않은 사이드이펙트를 막음.

원칙

 

  • 컨트롤러는 얇게, 서비스는 순수하게: 컨트롤러는 바인딩·검증·호출만. 핵심 로직은 서비스로 모아 단위 테스트하기 좋게.
  • 의존성 분리: 외부 시스템 접근(DB/Redis/S3/Kafka)은 어댑터/포트 레이어로 감싸 교체·모킹 간단하게.
  • 명확한 경계: DTO(입력), Entity/VO(도메인), Mapper를 분리해 테스트 목표를 또렷하게.

작성 습관

 

  • Given-When-Then(또는 Arrange-Act-Assert) 네이밍으로 “조건-행동-결과”를 드러내기.
  • 작은 테스트: 하나의 테스트는 하나의 주장만 검증(실패 시 원인 명확).
  • 테스트 데이터 빌더/픽스처: 반복되는 생성 로직은 빌더로 추상화(가독성·유지보수 ↑).
  • 독립성: 각 테스트는 상태 공유 금지(전역 캐시, 단일톤 외부 리소스 주의).
  • 속도: I/O(실DB/네트워크)는 최소화하고, 가능한 메모리/모킹으로 대체. E2E에서만 실체 사용.
  • 신뢰성: 랜덤/시간 의존 로직은 고정 시드/가짜 타이머로 결정론 유지.

그럼 만약 기능을 하나 맡았다면?

  1. 요구/ 경계 정의
    • 유저 스토리 & 성공 기준: “누가 무엇을 왜 한다?”, 성공 시 응답·상태 변화는?
    • API 계약 초안: 엔드포인트, 메서드, 입력/출력 스키마(예: DTO 필드), 에러 케이스 목록.
    • 도메인 규칙: 중복·권한·상태전이·TTL·락 등.
    • 테스트 전략: 단위(주요 규칙) → 통합(경계) → E2E(핵심 플로우 1~2개).
  2. dto 추가
  3. 도메인/ 서비스 로직 "설계" (핵심 규칙)
  4. 단위 테스트 먼저 작성 (위의 핵심 규칙을 확실히)
    • 서비스 단위 테스트부터 작성 (커버리지 60~70%)
      • 해피패스 (정상 흐름)
      • 엣지 케이스 (경계 조건)
      • 예외 케이스 (권한, 데이터 불일치 등)
    • Mock Repository, Mock Adapter 활용
    • 서비스 단위 테스트를 작성함, 리포지토리나 외부 sdk는 mock으로 대체
    • 여기서 비즈니스 규칙의 얄 6~7할을 확정해두면 이후가 편안해진다.
  5. 리포지토리와 엔티티 구현
    • Controller, Service, Repository, Entity 등 실제 코드 작성
      • Service: 비즈니스 로직 (핵심 규칙 중심)
      • Repository: DB 접근/트랜잭션 관리
      • Entity: ORM 모델 정의
      • Controller: DTO 바인딩, 검증, 응답 매핑
    • 외부 연동(S3, Redis, Kafka 등)은 Adapter/Provider로 분리
  6. 유닛 테스트 점검 및 통합 테스트 보강

이렇게 정리해보니 테스트코드의 역할과 중요성이 잘 이해된 것 같다. 테스트코드라고만 하니 뭔가를 테스트해야한다-> 테스트를 굳이 왜하나 로 생각했었는데, 이걸 명세(specification)의 개념으로 이해를 하니 사고가 달라졌다. 구현 전에 무엇이, 언제, 왜 어떻게 동작하는지를 먼저 작성하고, (mock을 통해 외부 의존성을 분리한채) 기능 구현에 필요한 핵심 규칙과 명세를 검증해 두는 흐름인 것이다.