CI/CD 파이프라인에 정적 코드 분석을 통합하려면 어떻게 해야 하나요?

CI/CD 파이프라인에 정적 코드 분석을 통합하려면 어떻게 해야 하나요?

인컴 2026년 6월 11일

프로덕션 환경에 배포되는 모든 코드는 작성자가 정확하다고 믿고 작성한 것입니다. 정적 코드 분석은 코드를 직접 실행하는 것이 아니라 코드 구조를 분석하고 데이터 흐름을 추적하며 알려진 취약점, 버그, 품질 위반 패턴과 비교하여 그 믿음이 옳았는지 확인하는 자동화 시스템입니다. 핵심은 정적 코드 분석을 실행할지 여부가 아니라 파이프라인의 어느 단계에서 실행할지, 어떤 도구를 사용할지, 어떤 임계값을 적용할지, 그리고 개발자가 피드백을 무시하지 않고 실제로 조치를 취할 수 있도록 얼마나 빠르게 피드백을 제공할지입니다.

정적 분석을 단순히 체크리스트 항목으로만 수행하는 것과 진정한 품질 관리 도구로 활용하는 것의 차이는 설정에 있습니다. 아무도 읽지 않는 보고서를 생성하는 스캐너는 보여주기식 행위에 불과합니다. 반면, 새로운 심각한 취약점이 발견되면 빌드를 실패시키고, 코드 커버리지가 임계값 이하로 떨어지면 병합을 차단하며, 풀 리퀘스트 검토 인터페이스에 정확한 파일 및 라인별 피드백을 제공하는 품질 관리 도구는 의사 결정이 이루어지는 순간 동작이 변화하는 시스템입니다.

파이프라인의 어느 단계에서 정적 분석을 실행해야 할까요?

정적 분석은 여러 지점에서 실행되어야 하며, 각 지점은 서로 다른 목적을 가져야 합니다. 한 지점에서만 실행하면 사각지대가 생기고, 모든 곳에서 동일하게 실행하면 개발자들이 우회해야 하는 느린 파이프라인이 생성됩니다.

파이프라인 단계무엇을 실행할까요?지연 예산목적
사전 커밋(로컬)빠른 린터만 해당(ESLint, Clippy, Rustfmt)5 초 미만저장소에 영향을 미치기 전에 명백한 문제를 방지하세요.
풀 리퀘스트 / 병합 리퀘스트전체 린팅 검사 + SAST + SonarQube 단계별 스캔3 분 미만새로운 중요 이슈에 대한 블록 병합
메인 브랜치 빌드커버리지, 중복, 기술적 부채를 포함한 종합 분석10 분 미만품질 추세를 추적하고 대시보드를 업데이트하세요.
매일 밤 예정됨심층 분석, 종속성 감사, 규정 준수 점검예산 없음제작 지연 문제와 공급망 위험을 파악하세요

가장 가치 있는 단계는 풀 리퀘스트 단계입니다 . 메인 브랜치에 푸시할 때 실행되는 분석은 병합 결정이 내려진 후에야 이루어집니다. 반면, 풀 리퀘스트에 실행되어 정확한 파일 및 줄 컨텍스트를 포함한 인라인 주석을 남기는 분석은 개발자가 아직 코드를 수정하는 중일 때, 즉 수정 비용이 가장 낮은 시점에 이루어집니다.

매번 저장할 때마다 커밋 전에 과도한 분석을 실행하는 것은 개발자 경험을 저해합니다. 5초 이내에 완료되는 빠른 린터가 그 자리에 적합합니다. 그 외의 모든 작업은 CI(지속적 통합)에서 처리해야 합니다.

GitHub Actions: 완벽한 작동 구성

GitHub Actions는 가장 널리 사용되는 CI 플랫폼입니다. 다음 워크플로는 형식 검사, 린팅, 빌드, SAST 보안 검사 및 SonarCloud 품질 게이트를 포함하는 계층형 품질 파이프라인을 실행합니다.

얌

# .github/workflows/quality.yml
name: Code Quality

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  # ── Layer 1: Fast checks (under 60 seconds) ─────────────────────────────
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - run: npm ci

      - name: Lint (fail on warnings)
        run: npx eslint src/ --max-warnings 0

      - name: Format check
        run: npx prettier --check src/

      - name: TypeScript type check
        run: npx tsc --noEmit

  # ── Layer 2: Security SAST (Semgrep) ────────────────────────────────────
  sast:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4

      - name: Semgrep SAST scan
        uses: semgrep/semgrep-action@v1
        with:
          config: p/javascript p/nodejs p/owasp-top-ten
        env:
          SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}

  # ── Layer 3: SonarCloud quality gate ────────────────────────────────────
  sonar:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # full history required for blame data

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - run: npm ci

      - name: Run tests with coverage
        run: npm test -- --coverage --coverageReporters=lcov

      - name: SonarCloud scan
        uses: SonarSource/sonarcloud-github-action@master
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

속성

# sonar-project.properties
sonar.projectKey=my-org_my-project
sonar.organization=my-org
sonar.sources=src
sonar.tests=tests
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.coverage.exclusions=**/*.test.js,**/*.spec.js

이 워크플로에서 중요한 구성 결정 사항:

  • --max-warnings 0 ESLint에서 경고가 발생하면 빌드가 실패합니다. 경고를 허용하는 팀은 아무도 읽지 않을 때까지 경고가 계속 쌓이게 됩니다.
  • fetch-depth: 0 체크아웃 시: SonarCloud는 새로 발생한 문제에 대한 책임을 할당하고 새 코드 메트릭을 정확하게 계산하기 위해 전체 Git 기록이 필요합니다.
  • Semgrep은 lint 검사 후 Sonar와 병렬로 실행됩니다. 보안 및 품질 검사는 독립적인 작업이므로 병렬 실행을 통해 전체 파이프라인 시간을 단축할 수 있습니다.
  • 테스트 실행 lcov 커버리지 출력: SonarCloud는 이 값을 읽어 파일별 커버리지를 표시하고 품질 게이트에서 커버리지 임계값을 적용합니다.

GitLab CI/CD: 품질 게이트를 활용한 품질 파이프라인

얌

# .gitlab-ci.yml
stages:
  - lint
  - test
  - analyze
  - security

variables:
  SONAR_HOST_URL: "https://sonarcloud.io"

lint:
  stage: lint
  image: node:20
  cache:
    paths: [node_modules/]
  script:
    - npm ci
    - npx eslint src/ --max-warnings 0
    - npx prettier --check src/
    - npx tsc --noEmit
  rules:
    - if: $CI_MERGE_REQUEST_IID
    - if: $CI_COMMIT_BRANCH == "main"

test:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm test -- --coverage --coverageReporters=lcov cobertura
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

sonarcloud:
  stage: analyze
  image:
    name: sonarsource/sonar-scanner-cli:latest
    entrypoint: [""]
  variables:
    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
    GIT_DEPTH: "0"
  cache:
    key: "${CI_JOB_NAME}"
    paths: [.sonar/cache]
  script:
    - sonar-scanner
  rules:
    - if: $CI_MERGE_REQUEST_IID
    - if: $CI_COMMIT_BRANCH == "main"

semgrep:
  stage: security
  image: returntocorp/semgrep:latest
  script:
    - semgrep scan --config=p/owasp-top-ten --config=p/javascript
      --sarif --output=semgrep.sarif src/
  artifacts:
    reports:
      sast: semgrep.sarif
  rules:
    - if: $CI_MERGE_REQUEST_IID
    - if: $CI_COMMIT_BRANCH == "main"

The artifacts: reports: sast: 이 블록은 Semgrep의 SARIF 출력을 GitLab의 보안 대시보드에 직접 통합하여, 발견 사항이 CI 로그 출력이 아닌 병합 요청 보안 보고서에 표시되도록 합니다.

Jenkins: SonarQube를 사용한 선언적 파이프라인

당연하지

// Jenkinsfile
pipeline {
    agent any

    tools {
        nodejs "NodeJS-20"
    }

    environment {
        SONAR_TOKEN = credentials('sonar-token')
    }

    stages {
        stage('Lint') {
            steps {
                sh 'npm ci'
                sh 'npx eslint src/ --max-warnings 0'
                sh 'npx tsc --noEmit'
            }
        }

        stage('Test') {
            steps {
                sh 'npm test -- --coverage --coverageReporters=lcov'
            }
            post {
                always {
                    publishHTML([
                        allowMissing: false,
                        reportDir: 'coverage/lcov-report',
                        reportFiles: 'index.html',
                        reportName: 'Coverage Report'
                    ])
                }
            }
        }

        stage('SonarQube Analysis') {
            steps {
                withSonarQubeEnv('SonarQube') {
                    sh '''
                        npx sonar-scanner \
                          -Dsonar.projectKey=my-project \
                          -Dsonar.sources=src \
                          -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
                    '''
                }
            }
        }

        stage('Quality Gate') {
            steps {
                timeout(time: 5, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }
    }

    post {
        failure {
            emailext(
                subject: "Quality Gate FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                body: "Build failed quality gate. Review: ${env.BUILD_URL}",
                to: "${env.CHANGE_AUTHOR_EMAIL}"
            )
        }
    }
}

waitForQualityGate abortPipeline: true 이 부분이 핵심입니다. SonarQube에서 분석이 완료될 때까지 지속적으로 상태를 확인하고, 품질 게이트를 충족하지 못하면 빌드를 실패 처리합니다. 이 부분이 없으면 분석이 아직 진행 중인 상태에서 파이프라인이 완료되어 품질 게이트 결과가 적용되지 않습니다.

팀이 실제로 존중하는 품질 게이트 구성하기

임계값이 너무 엄격하게 설정되어 모든 커밋에서 실패하는 품질 게이트는 비활성화됩니다. 반대로 임계값이 너무 관대해서 실패하지 않는 게이트는 아무런 가치가 없습니다. 궁극적인 목표는 프로젝트의 실제 위험 프로필에 맞춰 조정된 게이트를 구축하는 것입니다.

새로운 SonarQube 품질 게이트에 권장되는 초기 구성:

메트릭상태임계값
새로운 버그보다 큰0
새로운 취약점보다 큰0
새로운 보안 취약점 검토미만100%
새로운 코드 커버리지미만80%
새로운 중복 라인보다 큰3%
새로운 코드 냄새보다 큰10

이러한 임계값은 새 코드에만 적용하십시오 (SonarQube에서 말하는 "새 코드" 기간). 기존 프로젝트의 전체 코드베이스에 적용하지 마십시오. 5년 이상 누적된 기술 부채로 인해 모든 빌드가 실패하면 팀에서 게이트를 완전히 비활성화하게 됩니다. 새 코드 접근 방식을 사용하면 기존 부채를 ​​별도로 해결하는 동안 문제 발생을 막을 수 있습니다.

시간이 지남에 따라 임계값을 점진적으로 조정하세요. 처음에는 보수적인 임계값을 설정하고, 팀이 밀린 업무를 처리함에 따라 분기별로 임계값을 낮춰가세요. 현재 75%의 커버리지로 통과한 게이트는 기준선이 안정적으로 80%를 넘어서면 80%로 업데이트할 수 있습니다. 완벽을 향해 한 번에 도달하는 것보다 점진적으로 임계값을 낮추는 것이 더 지속 가능합니다.

대규모 코드베이스에서 정적 분석 성능 관리

CI에서 팀들이 정적 분석을 비활성화하거나 우회하는 가장 일반적인 이유는 파이프라인 속도가 너무 느려지기 때문입니다. 기능 브랜치에 푸시할 때마다 15분씩 걸리는 품질 검사는 개발자 흐름을 방해합니다. 분석 속도를 유지하는 데에는 몇 가지 전략이 있습니다.

증분 분석(새 코드만 해당). SonarQube와 대부분의 엔터프라이즈 정적 분석 도구는 전체 코드베이스가 아닌 풀 리퀘스트에서 변경된 파일만 분석하도록 지원합니다. 구성 sonar.pullrequest.base, sonar.pullrequest.branch예산 및 sonar.pullrequest.key PR별 증분 스캔을 활성화하는 매개변수입니다. 전체 분석은 메인 브랜치 병합 시에 여전히 실행되지만, PR 스캔은 대부분의 코드베이스에서 2분 이내에 완료됩니다.

캐싱. 정적 ​​분석 실행에서 가장 비용이 많이 드는 부분은 도구와 규칙 정의를 다운로드하는 것입니다. 스캐너 바이너리, 규칙 데이터베이스 및 실행 간에 해결된 모든 종속성을 캐시합니다.

얌

# GitHub Actions: cache SonarCloud scanner
- name: Cache SonarCloud packages
  uses: actions/cache@v4
  with:
    path: ~/.sonar/cache
    key: ${{ runner.os }}-sonar
    restore-keys: ${{ runner.os }}-sonar

병렬 처리. 린팅, 보안 검사 및 품질 게이트 분석은 독립적인 작업입니다. 이러한 작업은 순차적으로 실행하는 대신 병렬로 실행하세요. 파이프라인의 총 소요 시간은 모든 작업의 ​​합이 아니라 가장 느린 작업에 의해 결정됩니다.

선택적으로 도구를 활성화하세요. 모든 도구를 모든 브랜치에서 실행할 필요는 없습니다. 전체 보안 스캔 및 규정 준수 검사는 풀 리퀘스트와 메인 브랜치에서 실행하고, 병합 전 브랜치에서는 빠른 린터만 실행할 수 있습니다. CI 구성에서 브랜치 필터를 사용하여 서로 다른 분석 프로필을 적용하세요.

보안 우선 파이프라인에서의 정적 분석: SAST 및 의존성 스캐닝

보안에 초점을 맞춘 파이프라인은 표준 품질 분석에 두 가지 계층을 추가합니다. 하나는 애플리케이션 코드 취약점을 검사하는 SAST(정적 애플리케이션 보안 테스트)이고, 다른 하나는 타사 라이브러리의 알려진 CVE를 검사하는 종속성 스캔입니다.

SAST 도구 (Semgrep, CodeQL, Snyk Code, Checkmarx)는 신뢰할 수 없는 데이터가 애플리케이션 코드를 통해 민감한 작업에 도달하는 방식을 분석합니다. 이러한 도구는 일반적인 품질 린터로는 감지할 수 없는 SQL 인젝션, XSS, 명령 인젝션, 안전하지 않은 역직렬화 및 하드코딩된 자격 증명을 찾아냅니다.

얌

# GitHub Actions: CodeQL analysis for deep vulnerability detection
- name: Initialize CodeQL
  uses: github/codeql-action/init@v3
  with:
    languages: javascript-typescript

- name: Perform CodeQL Analysis
  uses: github/codeql-action/analyze@v3
  with:
    category: "/language:javascript-typescript"

종속성 스캐닝 체크 무늬 package.json, pom.xml, requirements.txt취약점 데이터베이스에 대한 동등한 매니페스트 파일:

얌

# Dependency audit in the pipeline
- name: Dependency audit
  run: |
    npm audit --audit-level=high
    # Fail if high or critical vulnerabilities are found

계층형 보안 모델은 개발자의 즉각적인 피드백을 위해 모든 PR에 대해 빠른 패턴 기반 SAST(Semgrep)를 적용하고, 철저한 검증을 위해 정기적으로 심층적인 시맨틱 SAST(CodeQL)를 실행합니다. 새로운 CVE가 매일 발표되므로 모든 빌드에서 종속성 스캔이 실행됩니다.

일반적인 통합 오류 및 방지 방법

메인 브랜치에서만 분석을 실행합니다. 정적 분석의 가치는 병합 후가 아니라 병합 전에 문제를 발견하는 데 있습니다. 병합 후에만 실행되는 분석은 보고서 역할만 할 뿐, 병합 여부를 결정하는 기준이 되지 못합니다.

품질 게이트를 실패 대신 경고로 설정하세요. 파이프라인을 차단하지 않는 경고는 개발자들이 무시하게 되는 알림일 뿐입니다. 실패하지 않는 게이트는 품질 문화를 구축하는 데 도움이 되지 않습니다.

테스트 파일을 코드 커버리지 임계값 계산에서 제외하지 않으면, 테스트 코드가 다른 테스트 코드를 테스트하는 경우 커버리지 수치가 부풀려집니다. 프로덕션 코드의 커버리지를 정확하게 측정하려면 테스트 디렉터리를 커버리지 계산에서 제외해야 합니다.

기존 코드베이스의 분석 백로그를 무시하는 것은 바람직하지 않습니다. 수백만 줄에 달하는 기존 코드베이스에 SonarQube를 처음 적용했을 때 40,000만 개의 이슈가 발생한다면 의욕을 꺾고 생산성을 저해할 뿐입니다. 새로운 코드 기준선 접근 방식을 활용하세요. "새로운 코드" 시작일을 정의하고, 해당 날짜 이후에 작성된 코드에만 게이트를 적용하며, 기존 이슈 백로그는 별도의 기술 부채 감축 프로그램으로 관리해야 합니다.

결과를 풀 리퀘스트 댓글에 연결하지 않습니다. 개발자가 방문해야 하는 별도의 대시보드에 게시된 분석 결과는 무시됩니다. 풀 리퀘스트 검토 인터페이스 내에서 문제가 발생한 특정 코드 줄에 인라인 댓글로 게시된 분석 결과는 처리됩니다. 구성 sonar.pullrequest.* GitHub, GitLab 또는 Bitbucket 통합을 위한 매개변수를 설정하여 코드 검토가 이루어지는 곳에 결과가 표시되도록 합니다.

방법 SMART TS XL CI/CD 파이프라인과 통합됩니다.

CI/CD 파이프라인에 사용되는 대부분의 정적 분석 도구는 한 번에 하나의 언어만 분석합니다. ESLint는 JavaScript만 분석하고, SonarQube는 Java, Python, JavaScript, C#을 분석합니다. 하지만 COBOL, JCL, RPG와 같은 언어는 물론, 메인프레임 배치 프로그램과 그 출력을 사용하는 Java 마이크로서비스, 그리고 이를 표시하는 JavaScript 프런트엔드를 연결하는 언어 간 종속성까지 분석하는 도구는 없습니다.

SMART TS XL CI/CD 파이프라인에 통합되어 기업 환경의 모든 언어를 동시에 포괄하는 크로스 언어 정적 분석 계층 역할을 합니다. Java 서비스가 수정되면, SMART TS XL의 영향 분석 변경 사항이 배포되기 전에 종속성 그래프를 추적하여 해당 변경 사항의 영향을 받는 COBOL 프로그램, 데이터베이스 스키마 및 다운스트림 서비스를 식별합니다. COBOL 카피북이 수정되면 해당 카피북을 포함하고 유효성 검사가 필요한 전체 애플리케이션 포트폴리오의 모든 프로그램을 식별합니다.

이러한 언어 간 의존성 인식은 바로 그러한 기능을 가능하게 합니다. SMART TS XLSonarQube의 CI/CD 통합은 단순히 SonarQube에 다른 언어를 추가하는 것과는 다릅니다. 변경된 파일의 코드를 분석하는 것만이 아니라, 해당 변경 사항이 시스템의 다른 부분에 어떤 영향을 미칠지 파악하는 것이 중요합니다. 즉, 변경을 실행하기 전에 변경의 진정한 범위를 결정하는 아키텍처 분석이 필요합니다.

기존 스택과 최신 스택을 모두 사용하는 엔터프라이즈 개발 팀을 위해, SMART TS XL의 DevOps 통합 이 기능은 구조적 분석을 파이프라인 검토 워크플로에 통합하여 언어별 도구로는 제공할 수 없는 아키텍처 가시성을 제공합니다. 그 결과, 언어 경계 내에서뿐만 아니라 전체 시스템에 걸쳐 표준을 적용하는 품질 게이트가 생성되어 구성 요소의 변경으로 인해 해당 구성 요소에 의존하는 구성 요소에 예기치 않은 오류가 발생하지 않도록 보장합니다.

파이프라인의 영구적인 구성원으로서의 정적 분석

정적 분석을 제대로 수행하는 팀은 테스트를 다루는 방식과 동일하게 접근합니다. 즉, 통과해야 할 단계가 아니라 모든 커밋에 내재된 지속적인 활동으로 간주합니다. 모든 풀 리퀘스트에 대해 분석이 실행되고, 품질 게이트 결과는 코드 리뷰와 함께 제공됩니다. 코드베이스가 개선됨에 따라 임계값이 더욱 엄격해집니다. 팀의 보안 및 품질 성숙도가 높아짐에 따라 새로운 분석 범주가 추가됩니다.

이 문서에 제시된 파이프라인 구성 템플릿은 시작점에 불과합니다. 구체적인 도구, 임계값 및 게이트 조건은 프로젝트의 언어 스택, 위험 프로필 및 팀 역량 수준에 맞춰 조정해야 합니다. 변하지 않아야 할 원칙은 다음과 같습니다. 병합을 차단하지 않는 분석은 동작을 변화시키지 못하며, 코드 품질 프로그램은 바로 이러한 동작을 변화시키고자 하는 것입니다.