본문으로 건너뛰기

안동민 개발노트

본문 시작

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

GitHub Actions에서 설치·빌드·테스트·커버리지 검사를 자동화하고 실패 증거를 남기는 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 Pipeline # 워크플로우 이름

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

jobs: # 실행될 작업들
  build-and-test: # 작업 이름
    permissions:
      contents: read # checkout에 필요한 최소 읽기 권한
    runs-on: ubuntu-latest # 이 작업이 실행될 환경 (GitHub Actions에서 제공하는 가상 머신)

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

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

      - name: Use Node.js ${{ matrix.node-version }} # 2. Node.js 환경 설정
        uses: actions/setup-node@v7
        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@v7
        if: always() # 앞 스텝 실패와 관계없이 업로드를 시도
        with:
          name: test-coverage-report-${{ matrix.node-version }}
          path: coverage/lcov-report # Jest 커버리지 HTML 리포트 경로
          if-no-files-found: warn # 앞 단계에서 멈춰 리포트가 없으면 의도를 분명히 기록
GitHub Actions의 Node 22·24 matrix가 checkout, 잠금 파일 기반 설치, 빌드, 단위 테스트, E2E 테스트, 커버리지 gate를 순차 실행하고 실제로 생성된 커버리지 HTML만 버전별 아티팩트로 보존하는 CI 계약

NestJS · GitHub Actions contract

matrix의 각 Node 버전은 별도 job이지만, 한 job 안의 스텝은 위에서 아래로 순차 실행됩니다. 빠른 실패와 신뢰 검증은 역할 구분이며, 별도 job으로 나누기 전까지 병렬 lane이 아닙니다.

Configured workflow

한 matrix job이 같은 순서와 권한으로 판정한다

  1. 트리거와 최소 권한

    main의 push·pull request가 시작하고, job 토큰은 contents: read만 갖습니다.

  2. 런타임과 결정적 설치

    Node 22.x·24.x에서 checkout@v7 setup-node@v7을 사용하고 npm ci를 실행합니다. npm cache는 잠금 파일 해시를 키로 전역 다운로드 캐시를 재사용하며 node_modules를 보존하지 않습니다.

  3. 빌드와 빠른 로직 검사

    npm run buildnpm run test가 실패하면 뒤의 일반 스텝은 기본적으로 건너뜁니다.

  4. 실제 흐름과 회귀 gate

    npm run test:e2e 뒤에 npm run test:cov가 Jest 임계값을 판정합니다. 두 스크립트의 테스트 범위는 프로젝트 설정이 정합니다.

  5. 조건부 증거 보존

    if: always()upload-artifact@v7이 버전별 고유 이름으로 실행되지만, coverage HTML 디렉터리가 실제로 생성된 경우에만 결과를 올릴 수 있습니다.

현재 예시가 판정하는 gate와 실제로 남기는 증거
검사통과 조건현재 예시의 증거 경계
BuildTypeScript 빌드 명령이 종료 코드 0Actions step log를 남기며 배포 이미지나 실행 파일은 만들지 않습니다.
Unitnpm run test가 전부 통과Jest 출력과 stack은 Actions log에 남습니다.
E2Enpm run test:e2e가 전부 통과기본 log만 남습니다. 요청·응답이나 DB seed 파일은 reporter와 upload 경로를 따로 구성해야 합니다.
Coveragetest:cov가 합의한 threshold 이상실행이 리포트를 만들었다면 Node 버전별 coverage HTML을 업로드합니다.
Build

컴파일 성공을 판정한다

배포 아티팩트 생성은 이 예시에 포함되지 않습니다.

Unit

빠른 로직 실패를 먼저 잡는다

실패 stack은 Actions log에서 확인합니다.

E2E

핵심 HTTP 흐름을 실행한다

요청·응답과 seed 파일은 별도 reporter가 있어야 보존됩니다.

Coverage

임계값과 HTML을 연결한다

리포트 경로가 생긴 실행만 버전별 artifact로 올릴 수 있습니다.

if: always()는 업로드 스텝의 실행을 보장할 뿐, 앞 단계가 만들지 못한 파일을 생성하지 않습니다. 로그·스크린샷·테스트 결과·seed가 필요한 팀은 각 도구가 파일을 쓰게 한 뒤 모든 경로를 명시적으로 업로드해야 합니다.

워크플로우 파일의 주요 부분 설명
  • name: GitHub Actions UI에 표시될 워크플로우 이름입니다.
  • on: 워크플로우가 언제 실행될지 정의합니다. 여기서는 main 브랜치에 push 또는 pull_request 이벤트가 발생할 때 실행되도록 설정했습니다.
  • jobs: 실행될 하나 이상의 작업을 정의합니다.
    • build-and-test: 작업의 이름입니다.
    • permissions: contents: read: 작업 토큰을 checkout에 필요한 레포지토리 읽기 범위로 제한합니다.
    • runs-on: ubuntu-latest: 작업이 실행될 운영체제 환경을 지정합니다.
    • strategy.matrix: 여러 Node.js 버전에서 테스트를 병렬로 실행하여 호환성을 검증할 수 있습니다.
    • steps: 작업을 구성하는 순차적인 단계들입니다.
      • actions/checkout@v7: 레포지토리 코드를 워크플로우 환경으로 가져옵니다.
      • actions/setup-node@v7: 지정된 Node.js 버전을 설정합니다. cache: 'npm'은 잠금 파일 해시를 키에 반영해 npm의 전역 패키지 다운로드 캐시를 재사용하며, node_modules를 보존하지는 않습니다.
      • 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@v7: matrix의 Node 버전을 이름에 넣어 각 실행의 커버리지 HTML을 서로 다른 아티팩트로 저장합니다. if: always()는 앞 단계가 실패해도 업로드 스텝을 실행하지만, 그 전에 커버리지 디렉터리가 생성되지 않았다면 업로드할 파일은 없습니다. 이 예시는 if-no-files-found: warn으로 그 상황을 기록하며, Unit/E2E 실패 stack은 Actions 로그에 남습니다. 요청·응답 로그, 스크린샷, DB seed 같은 파일 증거가 필요하면 테스트 reporter가 파일을 생성하게 하고 별도 path로 명시해야 합니다.

matrix의 각 Node 버전은 별도 job으로 실행되지만 한 job 안의 build → test → test:e2e → test:cov 스텝은 순차 실행됩니다. 기본 설정에서는 한 스텝이 실패하면 뒤의 일반 스텝은 건너뛰며, if: always()가 붙은 업로드 스텝만 마지막에 시도됩니다. 또한 test:cov가 같은 단위 테스트를 커버리지 모드로 다시 실행하는 구성이라면 npm run test와 중복될 수 있으므로, 팀이 빠른 검사와 커버리지 검사를 별도 job으로 나눌지 또는 하나의 실행으로 합칠지 명시적으로 선택해야 합니다.

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

.github/workflows/ci.yml 파일을 생성하고 GitHub 레포지토리에 푸시하면, GitHub Actions가 자동으로 워크플로우를 감지하고 실행을 시작합니다.

GitHub 레포지토리의 Actions 탭에서 실행 상태와 결과를 확인할 수 있습니다.


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

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

CI의 실행 비용을 줄이는 병렬화·캐시·선택 검사를 테스트 격리, 임시 데이터베이스, 비밀 관리, 제한 재시도, 명시적 증거 보존과 실패 책임 분류로 제어하는 운영 기준

NestJS · CI operating controls

CI 최적화의 목표는 실행 시간만 줄이는 것이 아니라 실패를 같은 조건에서 다시 설명할 수 있게 하는 것입니다. 아래 항목은 앞의 최소 YAML에 모두 구현된 기능이 아니라, 팀이 필요에 따라 추가하고 증거로 확인할 운영 선택입니다.

Feedback budget

빠른 검사를 앞세우되 전체 gate를 잃지 않는다

Fast fail. 빌드·단위 테스트처럼 싼 실패를 먼저 실행합니다.

Parallelism. Jest worker와 별도 job을 조절할 때 공유 DB·cache 충돌 여부를 함께 봅니다.

Cache scope. setup-node의 npm cache는 다운로드를 줄일 뿐 node_modules나 빌드 결과를 복원하지 않습니다.

Selection. --onlyChanged는 빠른 보조 신호로 쓰고, 병합 gate에는 합의한 전체 범위를 유지합니다.

Reproducible boundary

테스트마다 상태와 신뢰 경계를 다시 세운다

Isolation. DB row, cache, 파일과 timer 상태가 테스트 사이에 새지 않게 정리합니다.

Database. SQLite, Testcontainers, 전용 test DB 중 선택한 저장소와 seed·cleanup 정책을 고정합니다.

Secrets. API key와 DB URL은 CI secret으로 주입하고 로그에 값을 출력하지 않습니다.

Real gates. lint·정적 분석·추가 typecheck는 실제 workflow step을 추가했을 때만 gate라고 부릅니다.

실패를 숨기지 않는 중단·재시도·증거·책임 기준
운영 결정안전한 기준남겨야 할 근거
Fast fail설치·빌드·단위 테스트 실패 뒤의 비싼 일반 스텝을 멈춥니다.최초 실패 step, 명령, 종료 코드, Node 버전
Retry알려진 네트워크·외부 sandbox 불안정에만 횟수와 대상을 제한합니다.첫 실패와 각 재시도 결과를 모두 보존
Evidenceif: always()와 실제 생성 파일 경로를 함께 설정합니다.coverage, reporter 결과, E2E log, 필요 시 screenshot·seed
Owner실패를 제품 코드, 테스트 코드, 실행 환경으로 분류하고 담당자를 정합니다.분류 이유, 재현 조건, 다음 조치
Fast fail

싼 실패에서 멈춘다

최초 실패 step과 Node 버전을 남깁니다.

Retry

원인이 알려진 외부 불안정만 제한 재시도한다

첫 실패를 지우지 않고 재시도 결과와 함께 보존합니다.

Evidence

실제 파일 경로를 업로드한다

always()만으로 coverage·log·screenshot이 생기지는 않습니다.

Owner

코드·테스트·환경으로 분류한다

재현 조건과 다음 조치의 책임을 닫습니다.

재실행은 실패를 없애는 도구가 아니라 불안정 원인을 좁히는 도구입니다. 선택 실행·캐시·병렬화로 번 시간을 격리, 증거 보존, 원인 분류에 다시 투자해야 CI가 팀의 피드백 속도를 실제로 높입니다.

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

지속적 통합(CI)은 현대 소프트웨어 개발의 핵심이며, 자동화된 테스트를 CI 파이프라인에 통합하는 것은 고품질 소프트웨어를 빠르게 제공하는 데 필수적입니다.

NestJS는 견고한 아키텍처와 테스트 친화적 설계를 통해 이런 CI 환경 구축을 쉽게 해줍니다.

Jest, Supertest, GitHub Actions 같은 도구를 효과적으로 활용하면, 팀은 변경 사항을 더 자주 통합하고 버그를 조기에 발견하며 안정적인 애플리케이션을 지속적으로 제공할 수 있습니다.

이것으로 8장 테스팅 전략을 모두 마칩니다.

테스트 작성, 커버리지 측정, CI 파이프라인 통합, 실패 결과 보관을 기준으로 품질 관리 흐름을 정리했습니다.