Soomin Hwang
플랫폼 엔지니어링 / DevSecOps

하나의 파이프라인, 세 개의 제품: CI/CD와 테스트 리포팅 표준화

역할
CI/CD 파이프라인 설계 및 운영
기간
2025년 4월 – 2026년 10월
기술 스택
JenkinsGroovy (Declarative Pipeline)DockerPlaywrightSnykSonarQubeOWASP ZAP
공개 범위
사내 인프라
수동 회귀 테스트
~5시간~1시간−80%
E2E 파이프라인 정의
314줄 (Cypress)151줄 (Playwright)−52%

배경

배포 호스트명·레지스트리 주소·저장소 경로는 생략했습니다. 파이프라인 구조와 수치는 실제와 동일합니다.

작은 플랫폼 팀이 세 개의 코드베이스를 담당하고 있었습니다 — Product A(제품A)의 백엔드와 프론트엔드, 그리고 Product B(제품B)의 프론트엔드. 각각이 개발·스테이징·프로덕션 세 환경으로 나갑니다. 곱하면 배포 경로만 아홉 갈래고, 여기에 E2E·정적 분석·보안 스캔이 따로 붙습니다.

문제

파이프라인이 코드가 아니었습니다

빌드·배포 절차가 Jenkins job 설정 화면 안의 스크립트로 존재했습니다. 이 상태에서는 변경 이력을 볼 수 없고, 리뷰를 걸 수 없고, 잘못 바꿨을 때 되돌릴 방법이 job 설정의 백업뿐입니다. 제품이 늘어날 때마다 누군가 화면을 열어 기존 스크립트를 복사해 붙여넣는 식으로 번져나갔습니다.

제품마다 스테이지 구조가 달랐습니다

같은 "스테이징 배포"인데도 제품에 따라 이미지 검증 단계가 있기도 없기도 했고, 레지스트리 푸시 시점이 달랐습니다. 파이프라인을 읽는 사람이 매번 처음부터 읽어야 했습니다.

E2E가 신뢰 구간 밖에 있었습니다

Cypress 기반 스위트가 있었지만 실행 환경이 머신마다 달라 결과가 흔들렸고, 그래서 릴리즈 판단의 근거로 쓰이지 못했습니다. 결국 릴리즈마다 사람이 UI를 한 바퀴 도는 수동 회귀가 남았습니다. 파이프라인 정의 자체도 314줄까지 불어나 있었습니다.

진행한 작업

1. 파이프라인 정의를 전용 저장소로 분리

모든 Jenkinsfile을 제품 저장소 밖의 파이프라인 전용 저장소 하나로 옮기고, Jenkins job은 전부 Pipeline script from SCM으로 전환했습니다. job 설정에는 "이 저장소의 이 경로를 읽어라"만 남습니다.

pipeline/
├── backend/          daily · development · staging · production
├── frontend-a/       development · staging · production
│                     + playwright · sonarQube · zap · (cypress, 레거시)
└── frontend-b/       development · staging · production
                      + playwright · sonarQube

현재 19개의 파이프라인 정의가 이 저장소에서 버전 관리됩니다. 파이프라인 변경이 MR로 올라오고, 리뷰를 거치고, 문제가 생기면 git revert로 되돌아갑니다. 제품 코드와 배포 절차의 수명 주기가 다르다는 점에서, 이 분리는 제품 저장소마다 Jenkinsfile을 두는 것보다 이 팀 구조에 잘 맞았습니다.

2. 스테이지 구조를 제품 간 계약으로 고정

두 프론트엔드의 스테이징 파이프라인이 같은 순서를 따르도록 맞췄습니다:

Notify Start → Clone → .env 생성 → Compose 파일 준비
  → Docker Build → Image Verification → Snyk Scan
  → Private Registry Push → 배포 호스트 저장소·.env 갱신
  → 이미지 Pull & Compose Up

두 제품의 스테이징 정의는 각각 352줄과 350줄입니다. 차이는 제품별 값(이미지 이름, 대상 호스트, 컴포즈 파일)뿐이고 구조는 동일합니다. 한쪽에서 단계를 추가하면 다른 쪽에도 같은 자리에 들어갑니다.

프로덕션은 여기에 두 가지가 더 붙습니다 — GitLab Release 자동 생성, 그리고 사내 레지스트리와 GitLab 레지스트리로의 이중 이미지 푸시. 사내 네트워크가 끊겨도 외부 레지스트리에서 이미지를 받을 수 있게 하기 위한 것입니다.

3. Cypress → Playwright 전면 이관

E2E 28개 앱 스위트를 Playwright로 전부 옮겼습니다. 옮기면서 단순 이식에 그치지 않고 앱별 Edit/Share/Unshare/Cancel 경로와 워크스페이스 교차 링크 검증까지 넓혔습니다.

CypressPlaywright
테스트 수28개 앱350 passed / 0 skipped / 0 failed
실행 시간—로컬 ~9분 / Jenkins ~12–15분
파이프라인 정의314줄151줄

테스트는 Playwright 공식 이미지 안에서 실행합니다. 다만 Jenkins의 agent { docker { ... } } 대신 컨테이너를 직접 다루는 쪽을 택했습니다:

CONTAINER=$(docker create --ipc host \
  -e PLAYWRIGHT_BASE_URL="${PLAYWRIGHT_BASE_URL}" -e CI=true \
  --workdir /workspace ${PLAYWRIGHT_IMAGE} \
  sh -c 'yarn install --immutable && yarn test:e2e ${testArgs}; EXIT=$?; \
         chmod -R 777 node_modules playwright-report test-results 2>/dev/null || true; \
         exit $EXIT')

docker cp frontend/. $CONTAINER:/workspace/
docker start -a $CONTAINER || true
EXIT_CODE=$(docker inspect $CONTAINER --format '{{.State.ExitCode}}')

docker cp $CONTAINER:/workspace/playwright-report/. frontend/playwright-report/ 2>/dev/null || true
docker cp $CONTAINER:/workspace/test-results/. frontend/test-results/ 2>/dev/null || true
docker rm $CONTAINER || true
exit $EXIT_CODE

이렇게 한 이유는 세 가지입니다. 워크스페이스를 컨테이너 안으로 넣고 리포트와 trace를 다시 꺼내와야 아티팩트로 남길 수 있고, 컨테이너 안에서 생성된 파일의 소유권 때문에 Jenkins가 정리에 실패하는 것을 chmod로 막아야 했고, 무엇보다 docker start -a의 결과를 삼키고 docker inspect로 실제 종료 코드를 꺼내 그대로 반환해야 리포트 회수에 실패하더라도 테스트 실패가 빌드 실패로 정확히 이어지기 때문입니다.

4. 선택 실행과 "항상 도는 최소 집합"

전수 실행은 12~15분이 걸립니다. 앱 하나만 고친 변경에 매번 전수를 강요하면 사람들이 파이프라인을 우회하기 시작합니다. 그래서 Jenkins UI 파라미터로 앱 모듈을 골라 돌릴 수 있게 하되, 선택 실행일 때도 항상 함께 도는 집합을 코드에 고정했습니다:

def defaultNonAppPaths = [
    'e2e/database/',
    'e2e/workspace/libraries.spec.ts',
    'e2e/workspace/targets.spec.ts',
    'e2e/workspace/targets-create.spec.ts',
    'e2e/app/jobs-filters.spec.ts',
]

데이터베이스 검색, 워크스페이스 기본 동작, 잡 목록 필터 — 거의 모든 앱이 딛고 서 있는 공유 경로입니다. 앱 하나를 고치다 공유 컴포넌트나 API 응답 형태를 건드려 다른 기능을 조용히 망가뜨리는 사고가 여기서 걸립니다. 파라미터를 비우면 전수 실행입니다.

5. 보안·품질 스캔을 파이프라인 안으로

그때그때 수동으로 돌리던 점검 도구들을 파이프라인의 정해진 자리에 넣어, 사람이 기억해야 돌아가는 일이 아니게 만들었습니다.

  • Snyk — 스테이징에서 이미지 빌드 직후, 레지스트리 푸시 직전에 컨테이너 이미지를 스캔하고 Critical 발견 시 Slack으로 알립니다.
  • SonarQube — 별도 job으로 공용 서버에 정적 분석 리포트를 올립니다.
  • OWASP ZAP — 스테이징 환경을 대상으로 동적 보안 스캔을 돌립니다.

세 가지 모두 현재는 리포팅 단계이고, 배포를 차단하는 게이트로 올리는 것은 누적된 Critical을 정리한 다음으로 잡아두었습니다 — 상시 빨간불이 되면 팀이 게이트를 꺼버리기 때문입니다.

결과

지표이전이후변화
수동 회귀 테스트릴리즈당 ~5시간릴리즈당 ~1시간−80%
E2E 자동 실행로컬, 머신별 편차350 tests, CI에서 12–15분재현 가능
E2E 파이프라인 정의314줄 (Cypress)151줄 (Playwright)−52%
파이프라인 정의 위치Jenkins job 설정에 산재전용 저장소 19개 파일버전 관리
스테이징 스테이지 구조제품마다 상이두 프론트엔드 동일 (352 / 350줄)표준화
취약점 스캔필요할 때 수동스테이징 빌드마다 자동 스캔상시화

수동 테스트 시간이 줄어든 건 테스트를 덜 해서가 아니라, 같은 범위를 사람 대신 자동으로 검증하게 됐기 때문입니다. 확보한 시간은 자동화가 여전히 커버하지 못하는 영역 — 탐색적 테스트, 그리고 전수 실행이 실제로 무엇을 잡아냈는지 읽는 작업 — 으로 옮겨갔습니다.

이 케이스 스터디가 흥미로우셨나요?

가벼운 커피챗도 언제든 환영합니다. 편하게 연락 주세요.