2단계. CI(지속적 통합) 파이프라인 설계 - 100-hours-a-week/KTB4-17th-wiki GitHub Wiki
2단계 CI 지속적 통합 파이프라인 설계
1. CI 도구 선택 및 필요성
1-1. GitHub Actions
Jenkins 외 외부 플랫폼은 고려하지 않았다. 팀원 중 Jenkins를 사용해본 사람이 없었기 때문이다.
CI의 결과는 AI팀과 Fullstack팀이 모두 기능 개발 후 확인해야 했다. 현재 빠른 개발과 배포가 필요한 상황에서 새로운 플랫폼에 익숙해지는 데 시간을 사용하는 것이 V1을 배포하는 데 필수적이지 않다고 판단했다.
따라서 팀원들이 사용해본 경험이 있는 GitHub Actions를 선택했다.
2. CI Trigger 정책 설계
현재 브랜치 전략은 feature → dev → main 구조로 구성했으며, 별도의 staging 브랜치는 운영하지 않기로 했다.
그 이유는 현재 개발 흐름에서 staging 브랜치를 추가할 경우 PR → Merge 절차가 하나 더 늘어나기 때문이었다.
따라서 dev 브랜치에는 가능한 배포 가능한 상태의 코드만 유지하는 것을 목표로 했다.
이를 위해 feature 브랜치에서 코드를 Push하거나 dev 또는 main 브랜치로 Pull Request가 생성되었을 때 CI를 실행하도록 구성했다.
2-1. 브랜치 전략
브랜치 전략은 main → dev → feature 구조로 구성했다.
- main : 배포를 위한 브랜치로 사용했다.
- dev : 팀원들의 코드를 합치는 브랜치로 사용했다.
- feature : 각자 기능을 개발하는 브랜치로 사용했다.
2-2. 트리거 시점
[feature branch push]
현재 CI에서는 정적인 코드 검사만 이루어지기 때문에 처음 GitHub에 코드를 Push하는 순간 새로 올라오는 코드에 대해서 검증을 진행하도록 했다.
→ Lint
→ Formatter Check
→ Dependency Vulnerability Check
→ Build
[feature → dev PR]
새로 개발한 기능과 기존 dev 코드 사이에서 Build가 정상적으로 이루어지는지 확인하도록 했다.
→ Build
[dev → main PR]
dev에서 Merge한 코드와 main 사이에서 Build가 정상적으로 이루어지는지 확인하도록 했다.
→ Build
3. CI Pipeline 설계
3-1. CI 검증 범위 설계 기준
초기 서비스에서는 시장의 불확실성이 크기 때문에 기능을 빠르게 개발하고 배포하여 실제 사용자 반응을 확인하는 과정이 중요하다고 판단했다.
따라서 CI 과정에서도 모든 종류의 테스트를 수행하여 높은 수준의 안정성을 확보하는 것보다는, 개발 및 배포 속도를 크게 저하시키지 않는 파이프라인을 목표로 했다.
이를 위해 CI에서 수행할 수 있는 테스트의 종류와 각각의 도입 필요성을 검토했다.
기존에는 개발 서버를 따로 두고 기능을 매번 배포해서 확인하는 것이 기능 개발에 안정적이라고 생각했다. 하지만 CI/CD를 고려하면서 V1 단계에서는 개발 서버를 별도로 두는 것이 부담된다고 판단했다.
또한 현재는 사용자가 없기 때문에 별도의 개발 서버를 두지 않고 운영 서버에 총 11번의 배포를 진행하기로 했다.
3-2. 테스트 범위 선정
CI 파이프라인은 빠르게 시장에 진입하고, 초기 사용자 반응을 빠르게 확인할 수 있어야 한다는 것을 가장 중요한 기준으로 설계했다.
따라서 CI 과정이 배포 속도를 과도하게 늦추지 않도록 검증 단계를 최소화하고자 했다.
또한 현재 Fullstack 개발자가 2명뿐이고, V1 개발 범위가 전체 V3 기준 약 70%를 차지할 정도로 크기 때문에 테스트 코드 작성 및 유지보수처럼 개발자의 추가적인 시간과 작업이 많이 필요한 테스트 역시 최소화할 필요가 있다고 판단했다.
결국 “테스트를 가볍게 가져가되, 꼭 챙겨야 할 최소한의 검증은 무엇인가?”를 기준으로 CI 테스트 범위를 선정했다.
먼저 테스트 코드 작성 및 유지보수 비용이 발생하는 Unit Test, Component Test는 현재 개발 일정상 테스트 코드까지 작성할 시간이 부족하여 제외했다.
또한 실행 환경 구축이나 수행 시간이 많이 필요한 DAST, 침투 테스트, Performance Test, Compatibility Test 등은 V1에 적용하기에는 무겁다고 판단하여 우선 제외했다.
SAST 도입 여부
SAST 도입 여부에 대해서는 고민이 있었다.
SAST는 코드를 실행하지 않고 소스 코드를 분석하여 잠재적인 보안 취약점을 탐지하는 방식으로, DAST나 침투 테스트 등 다른 보안 테스트에 비해 비교적 가볍게 수행할 수 있기 때문에 V1에서도 도입할 수 있다고 생각했다.
처음에는 Unit Test나 Component Test를 테스트 코드 작성할 시간이 없다는 이유로 제외한 만큼, “SAST를 통해 취약점을 발견하더라도 이를 수정할 시간이 없는데 굳이 도입할 필요가 있을까?”라고 생각했다.
하지만 한편으로는 현재 개발 일정상 발견된 취약점을 즉시 수정하기 어렵더라도, “취약점의 존재 자체를 모르고 있는 것보다는 미리 파악해 두고 추후 개선하는 것이 낫지 않을까?”라는 생각도 들었다.
하지만 취약점이 발견되어 SAST가 실패했음에도 이를 무시하고 배포를 진행한다면, 실패한 검증을 배포의 판단 기준으로 사용하지 않는다는 점에서 CI 단계에 포함하는 의미가 떨어진다고 판단했다.
따라서 V1에서는 SAST를 제외하고, 추후 사용자 증가, 민감 데이터 추가 등으로 보안 강화의 필요성이 느껴지는 시점에 보안 검증을 도입하는 방향으로 결정했다.
위와 같은 사고 과정을 거쳐 우선 비교적 가볍게 수행할 수 있는 다음 검증을 CI에 적용하기로 했다.
- Lint
- Formatter Check
- Dependency Vulnerability Check
- Build
Integration Test, API/Contract Test, 가벼운 E2E Test는 실행 비용 대비 필요한 안정성 수준을 고려하여 추가 도입 여부를 결정하기로 했다.
3-3. CI Workflow
# 테스트 과정이 생략되어 frontend, backend, ai의 구조상 차이는 없음.
name: CI
on:
push: # feat -> dev로 push할 때
branches: ["feature/**"]
pull_request:
branches: ["dev", "main"]
permissions:
contents: write
pull-requests: write # feature에서 dev로 자동 머지
jobs:
feature_ci:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Check source
uses: actions/checkout@v6
- name: Lint
run:
- name: Formatter Check
run:
- name: Dependency Vulnerability Check
run:
- name: Build
run:
dev_main_ci:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Check pr source
uses: actions/checkout@v6
- name: Build
run:
- name: auto-merge
if: github.base.ref == 'dev'
run: gh pr merge --auto --merge "$pr_url"
env:
github_token:
pr_url: ${{ github.event.pull_request.html_url }}
3-4. 현재 CI Pipeline의 한계
테스트 코드 및 동적 테스트의 부재
테스트 코드의 검증 없이 간단한 정적 테스트만 진행하기 때문에, 코드가 빌드 가능하다는 것만 확인할 수 있고 실제 기능이 정상적으로 동작하는지는 검증할 수 없었다.
따라서 API 동작 오류, DB 연동 문제 등이 존재하더라도 CI를 통과하여 dev/main 브랜치까지 반영될 수 있다는 문제가 있었다.
현재 V1에서는 백엔드 개발의 분량이 전체의 70%를 차지하기 때문에 테스트 코드를 작성할 여유가 없다는 점과 빠른 개발을 위해 테스트를 제외했지만, 개발 속도를 얻는 대신 배포 안정성을 포기한 구조라는 한계점이 있었다.
4. Docker Image 관리 전략
ECR Repository 관리
backend와 AI 이미지는 각각 별도의 ECR Repository에 저장한다.
backend와 AI는 서로 다른 애플리케이션이며, Dockerfile, 실행 환경, 배포 주기가 다르다. 이미지를 하나의 Repository에서 함께 관리하면 이미지 태그가 어떤 서비스의 이미지인지 구분해야 하고, 서비스별 이미지 정리 정책을 적용하기도 어려워진다.
따라서 backend Repository에는 backend 이미지, ai Repository에는 AI 이미지만 저장하여 서비스별 이미지 버전과 배포 대상을 명확하게 관리한다.
오래된 Image 관리 정책 backend와 AI Repository에는 각각 최신 11개의 SHA 태그 이미지만 유지한다.11개로 지정한 이유는 v1에서 기능 단위로 배포하는 이미지의 갯수가 11개이기 때문이다. 11개가 넘으면 오래된 이미지는 ECR Lifecycle Policy를 통해 삭제한다. 롤백에 필요한 최근 이미지는 유지하면서 불필요한 이미지 저장 비용이 계속 증가하는 것을 방지한다.
5. 실패 처리 정책
현재 CI에서 수행하는 검증은 Lint, Formatter Check, Dependency Vulnerability Check, Build로 구성하였다.
각 단계에서 실패가 발생했을 경우 실패 원인에 따라 다음과 같이 처리하기로 했다.
5-1. Lint / Formatter Check 실패
Lint 또는 Formatter Check가 실패한 경우 코드 스타일이나 정적 규칙 위반이 원인이므로 개발자가 해당 부분을 빠르게 수정한 뒤 다시 Commit 및 Push하여 CI를 재실행하도록 했다.
5-2. Dependency Vulnerability Check 실패
Dependency Vulnerability Check가 실패한 경우 취약점이 발견된 의존성을 확인하고, 해당 라이브러리의 버전 변경이나 대체 가능 여부를 검토한 뒤 수정하도록 했다.
5-3. Build 실패
Build가 실패한 경우 Lint나 Formatter 검사와 달리 컴파일 오류, 설정 문제 등의 가능성이 있기 때문에 개발자가 실패 로그를 확인하고 원인을 디버깅한 뒤 수정하도록 했다.
수정이 완료되면 다시 Commit 및 Push하여 CI를 재실행하고, 모든 검증을 통과한 경우에만 다음 단계로 진행하도록 했다.