icon
8장 : 테스팅 전략

지속적 통합(CI)에 테스트 통합


지난 절에서는 NestJS 애플리케이션의 테스트 커버리지를 측정하고 이를 통해 코드 품질을 관리하는 방법을 알아보았습니다. 이제 8장의 마지막 절로, 소프트웨어 개발에서 핵심적인 자동화 프로세스인 지속적 통합(CI, Continuous Integration) 에 테스트를 어떻게 효과적으로 통합하는지 살펴보겠습니다.

지속적 통합은 개발 팀의 모든 구성원이 작업한 코드를 자주 메인 브랜치에 병합하고, 이 병합된 코드가 자동으로 빌드되고 테스트되는 과정을 의미합니다. CI 파이프라인에 테스트를 통합하는 것은 소프트웨어 개발의 효율성과 안정성을 극대화하는 데 필수적인 단계입니다.


지속적 통합(CI)이란?

지속적 통합(CI, Continuous Integration) 은 개발자들이 작업한 코드를 주기적으로(하루에 여러 번) 공유 레포지토리(예: Git)에 병합하고, 병합된 코드에 대해 자동화된 빌드 및 테스트를 수행하는 소프트웨어 개발 방식입니다.

CI의 핵심 목표

  • 통합 문제 조기 발견: 코드를 자주 통합하고 테스트함으로써, 통합 과정에서 발생하는 문제를 초기에 발견하고 해결할 수 있습니다.
  • 버그 감소: 자동화된 테스트를 통해 새로운 기능이 기존 기능에 미치는 부작용(회귀 버그)을 신속하게 파악합니다.
  • 품질 향상: 항상 테스트를 통과하는 빌드 가능한 상태의 코드를 유지하여 소프트웨어 품질을 지속적으로 관리합니다.
  • 배포 준비 상태 유지: 언제든지 배포 가능한 안정적인 상태의 코드를 유지합니다 (지속적 배포(CD)의 전제 조건).
  • 개발자 생산성 향상: 수동 테스트 및 통합에 드는 시간을 줄여 개발자가 더 중요한 작업에 집중할 수 있도록 돕습니다.

CI 파이프라인의 일반적인 단계

코드 커밋(Code Commit): 개발자가 변경 사항을 버전 관리 시스템(예: GitHub, GitLab, Bitbucket)의 메인 브랜치에 푸시합니다 (또는 Pull Request 생성).

빌드 트리거(Build Trigger): 코드 변경이 감지되면 CI 도구(예: Jenkins, GitHub Actions, GitLab CI, CircleCI)가 자동으로 빌드 프로세스를 시작합니다.

코드 가져오기(Checkout Code): CI 서버가 최신 코드를 가져옵니다.

의존성 설치(Install Dependencies): 프로젝트에 필요한 라이브러리 및 패키지(예: npm install)를 설치합니다.

코드 빌드(Build Code): 소스 코드를 실행 가능한 형태로 컴파일하거나 트랜스파일합니다 (예: TypeScript를 JavaScript로 변환).

테스트 실행(Run Tests): 단위 테스트, 통합 테스트, E2E 테스트 등 모든 자동화된 테스트를 실행합니다.

테스트 커버리지 측정(Measure Test Coverage): 테스트 커버리지를 측정하고, 설정된 임계값을 충족하는지 확인합니다.

결과 보고(Report Results): 빌드 및 테스트 결과를 개발자, 팀, 또는 관련 채널(예: Slack)에 보고합니다. 실패 시 알림을 보냅니다.

아티팩트 생성(Create Artifact): 빌드 및 테스트가 성공하면 배포 가능한 아티팩트(예: Docker 이미지, 압축된 실행 파일)를 생성합니다.


GitHub Actions를 사용한 CI 통합 예시

CI/CD 도구는 매우 다양하지만, 여기서는 GitHub 레포지토리에 코드를 푸시할 때 자동으로 CI 파이프라인을 실행하는 GitHub Actions를 예로 들어 NestJS 테스트를 통합하는 방법을 설명하겠습니다.

사전 준비

  • GitHub 계정
  • NestJS 프로젝트가 GitHub 레포지토리에 푸시되어 있어야 합니다.

단계 1: GitHub Actions 워크플로우 파일 생성

GitHub 레포지토리의 .github/workflows 디렉토리 안에 .yml 또는 .yaml 확장자를 가진 워크플로우 파일을 생성합니다 (예: ci.yml).

# .github/workflows/ci.yml

name: NestJS CI/CD Pipeline # 워크플로우 이름

on: # 워크플로우가 실행될 트리거 조건
  push: # main 브랜치에 코드가 푸시될 때
    branches:
      - main
  pull_request: # main 브랜치로 풀 리퀘스트가 생성되거나 업데이트될 때
    branches:
      - main

jobs: # 실행될 작업들
  build-and-test: # 작업 이름
    runs-on: ubuntu-latest # 이 작업이 실행될 환경 (GitHub Actions에서 제공하는 가상 머신)

    strategy:
      matrix: # 여러 Node.js 버전에서 테스트를 실행하고 싶을 때 사용
        node-version: [18.x, 20.x] # Node.js 18.x와 20.x 버전에서 각각 실행

    steps: # 작업 내에서 실행될 단계들
      - name: Checkout Repository # 1. 레포지토리 코드 가져오기
        uses: actions/checkout@v4

      - name: Use Node.js ${{ matrix.node-version }} # 2. Node.js 환경 설정
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm' # npm 캐싱 설정 (재사용하여 빌드 시간 단축)

      - name: Install Dependencies # 3. 의존성 설치
        run: npm ci # 'npm ci'는 package-lock.json에 기반하여 깨끗하게 설치

      - name: Build Project # 4. 프로젝트 빌드 (TypeScript 컴파일 등)
        run: npm run build

      - name: Run Unit Tests # 5. 단위 테스트 실행
        run: npm run test

      - name: Run E2E Tests # 6. E2E 테스트 실행 (데이터베이스 필요시 이 단계에서 DB 컨테이너 실행 로직 추가)
        run: npm run test:e2e

      - name: Check Test Coverage # 7. 테스트 커버리지 확인 (임계값 미달 시 실패)
        run: npm run test:cov

      # 선택 사항: 테스트 결과 아티팩트 저장 (예: 커버리지 HTML 리포트)
      - name: Upload Test Coverage Report
        uses: actions/upload-artifact@v4
        if: always() # 실패해도 항상 업로드
        with:
          name: test-coverage-report-${{ matrix.node-version }}
          path: coverage/lcov-report # Jest 커버리지 HTML 리포트 경로

워크플로우 파일의 주요 부분 설명

  • name: GitHub Actions UI에 표시될 워크플로우 이름입니다.
  • on: 워크플로우가 언제 실행될지 정의합니다. 여기서는 main 브랜치에 push 또는 pull_request 이벤트가 발생할 때 실행되도록 설정했습니다.
  • jobs: 실행될 하나 이상의 작업을 정의합니다.
    • build-and-test: 작업의 이름입니다.
    • runs-on: ubuntu-latest: 작업이 실행될 운영체제 환경을 지정합니다.
    • strategy.matrix: 여러 Node.js 버전에서 테스트를 병렬로 실행하여 호환성을 검증할 수 있습니다.
    • steps: 작업을 구성하는 순차적인 단계들입니다.
      • actions/checkout@v4: 레포지토리 코드를 워크플로우 환경으로 가져옵니다.
      • actions/setup-node@v4: 지정된 Node.js 버전을 설정합니다.
      • npm ci: package-lock.json에 명시된 정확한 버전의 의존성을 설치하여 일관성을 보장합니다.
      • npm run build: nest build 명령어로 NestJS 애플리케이션을 빌드합니다.
      • npm run test: 단위 테스트를 실행합니다.
      • npm run test:e2e: E2E 테스트를 실행합니다. 만약 E2E 테스트에 실제 데이터베이스가 필요하다면, 이 단계 이전에 Docker Compose 등을 사용하여 데이터베이스 컨테이너를 띄우는 추가 스텝이 필요합니다. (예: docker-compose up -d --build와 같은 명령어)
      • npm run test:cov: 테스트 커버리지를 측정하고, jest.config.js에 설정된 임계값을 만족하는지 확인합니다. 임계값을 충족하지 못하면 이 단계는 실패합니다.
      • actions/upload-artifact@v4: 테스트 결과물(예: 커버리지 리포트 HTML 파일)을 CI/CD 파이프라인 실행 결과와 함께 아티팩트로 저장합니다. 이는 디버깅이나 결과 검토 시 유용합니다. if: always()는 이전 단계가 실패하더라도 항상 아티팩트를 업로드하도록 합니다.

단계 2: GitHub 레포지토리에 푸시

.github/workflows/ci.yml 파일을 생성하고 GitHub 레포지토리에 푸시하면, GitHub Actions가 자동으로 워크플로우를 감지하고 실행을 시작합니다. GitHub 레포지토리의 "Actions" 탭에서 실행 상태와 결과를 확인할 수 있습니다.


CI/CD 파이프라인 최적화 및 고려사항

CI 파이프라인에 테스트를 통합할 때 다음과 같은 점들을 고려하여 최적화할 수 있습니다:

  • 테스트 속도 최적화
    • 병렬 실행: Jest의 --runInBand 옵션을 제거하거나, Jest의 maxWorkers 설정을 조절하여 테스트를 병렬로 실행합니다.
    • 캐싱: actions/setup-node에서 cache: 'npm'을 사용한 것처럼, 의존성 설치 및 빌드 결과 등을 캐싱하여 재사용합니다.
    • 테스트 환경 격리: 각 테스트는 독립적으로 실행되어야 하며, 이전 테스트의 상태에 영향을 받지 않도록 환경을 격리합니다.
  • 데이터베이스 관리: E2E 테스트 시 데이터베이스가 필요하다면, CI 환경에서 경량화된 데이터베이스(예: SQLite)를 사용하거나, Testcontainers 같은 도구를 활용하여 임시 데이터베이스 컨테이너를 띄우는 것이 일반적입니다.
  • 환경 변수: CI 환경에서 필요한 환경 변수는 CI 도구의 Secret 기능을 활용하여 안전하게 관리합니다.
  • 테스트 선택적 실행: 변경된 코드와 관련된 테스트만 실행하도록 설정하여 빌드 시간을 단축할 수 있습니다. (Jest의 --onlyChanged 옵션 등)
  • 알림 설정: 빌드 실패 시 Slack, 이메일 등으로 팀에 알림을 보내도록 설정하여 문제 해결 시간을 단축합니다.
  • 코드 품질 도구 통합: ESLint, Prettier 같은 린트 및 포매터 검사를 CI 파이프라인에 통합하여 코드 스타일 및 품질을 일관되게 유지합니다.
  • 정적 분석 도구: SonarQube와 같은 정적 분석 도구를 CI에 통합하여 잠재적인 취약점이나 코드 스멜을 자동으로 탐지합니다.

지속적 통합(CI)은 현대 소프트웨어 개발의 핵심이며, 자동화된 테스트를 CI 파이프라인에 통합하는 것은 고품질 소프트웨어를 빠르게 제공하는 데 필수적입니다. NestJS는 견고한 아키텍처와 테스트 친화적인 설계를 통해 이러한 CI 환경 구축을 용이하게 합니다. Jest, Supertest, 그리고 GitHub Actions와 같은 도구들을 효과적으로 활용하면, 팀은 변경 사항을 더 자주 통합하고, 버그를 조기에 발견하며, 안정적이고 신뢰할 수 있는 애플리케이션을 지속적으로 제공할 수 있습니다.

이것으로 8장 "테스팅 전략"을 모두 마칩니다. 이제 여러분은 NestJS 애플리케이션에 대한 다양한 테스트를 작성하고, 테스트 커버리지를 측정하며, 이를 CI 파이프라인에 통합하여 자동화된 품질 관리를 수행하는 방법을 이해하게 되었습니다.