CI - 100-hours-a-week/21-iceT-wiki GitHub Wiki

1. κ°œμš”

KocoλŠ” 개발 및 배포 κ³Όμ •μ—μ„œ λ°œμƒν•  수 μžˆλŠ” νœ΄λ¨Ό μ—λŸ¬μ†λ„ μ €ν•˜ν˜‘μ—… λΉ„νš¨μœ¨μ„± λ¬Έμ œλ₯Ό ν•΄κ²°ν•˜κ³ μž CI(지속적 톡합) νŒŒμ΄ν”„λΌμΈμ„ κ΅¬μΆ•ν–ˆμŠ΅λ‹ˆλ‹€.

이 λ¬Έμ„œλŠ” CIλ₯Ό λ„μž…ν•œ λ°°κ²½, 도ꡬ 선택 이유, ꡬ성 방식, μ‹€μ‚¬μš© λ°©λ²•κΉŒμ§€ μ •λ¦¬ν•©λ‹ˆλ‹€.

νŒŒμ΄ν”„λΌμΈ 흐름도 image


2. λ„μž… λ°°κ²½

(1) 휴먼 μ—λŸ¬ κ°μ†Œ

  • μˆ˜μž‘μ—… μ‹€μˆ˜λ‘œ μΈν•œ μ„œλΉ„μŠ€ μž₯μ•  μœ„ν—˜ 쑴재
  • μžλ™ν™”λœ ν…ŒμŠ€νŠΈ 및 λΉŒλ“œλ₯Ό μŠ¬λž™ μ•Œλ¦Όμ„ 톡해 휴먼 μ—λŸ¬λ₯Ό 직접 확인 ν•  수 μžˆλ„λ‘ 함

(2) 속도 κ°œμ„ 

  • κΈ°μ‘΄: (ν”„λ‘ νŠΈ, λ°±μ—”λ“œ, ai) 둜컬 λΉŒλ“œ β†’ ν…ŒμŠ€νŠΈ β†’ 배포 μ€€λΉ„κΉŒμ§€ μ•½ 10λΆ„ 이상 μ†Œμš”
  • CI λ„μž… 이후: ν…ŒμŠ€νŠΈ 및 λΉŒλ“œ 5~10λΆ„ 이내 μ™„λ£Œ
  • 평균 μž‘μ—… μ‹œκ°„ 50% κ°μ†Œ, 개발 생산성 ν–₯상

(3) 반볡 μž‘μ—… μžλ™ν™”

  • κΈ°μ‘΄: Git Clone 이후 μˆ˜λ™ λΉŒλ“œ 및 ν…ŒμŠ€νŠΈ 반볡
  • CI 적용 이후: μ½”λ“œ Push만으둜 μžλ™ λΉŒλ“œ 및 ν…ŒμŠ€νŠΈ μˆ˜ν–‰

(4) μ•ˆμ „μ„± 및 νš¨μœ¨μ„± 확보

  • κΈ°μ‘΄: Git Clone 기반 μˆ˜μž‘μ—… 배포둜 λ³΄μ•ˆ 및 운영 μœ„ν—˜ 쑴재
  • CI λ„μž… 이후: μ„œλ²„ μ ‘κ·Ό 없이 μ½”λ“œ 기반 검증, λ³΄μ•ˆμ„± 및 νš¨μœ¨μ„± ν–₯상

(5) ν˜‘μ—… ν–₯상

  • PR 생성 μ‹œ μΌκ΄€λœ ν…ŒμŠ€νŠΈ 및 ν’ˆμ§ˆ 검증 μžλ™ν™”

λ”°λΌμ„œ, μ΄λŸ¬ν•œ λ¬Έμ œλ“€μ„ ν•΄κ²°ν•˜κ³ μž μ €ν¬λŠ” CI(지속적 톡합)λ₯Ό λ„μž…ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

μžλ™ν™”λ‘œ CIκ°€ ν•˜κ²Œλ˜λŠ” 일

단계 μ„€λͺ…
μ½”λ“œ 톡합 μ—¬λŸ¬ κ°œλ°œμžκ°€ μž‘μ„±ν•œ μ½”λ“œλ₯Ό ν•˜λ‚˜μ˜ λΈŒλžœμΉ˜μ— 자주 병합
μžλ™ λΉŒλ“œ μ½”λ“œκ°€ λ³‘ν•©λ˜λ©΄ μžλ™μœΌλ‘œ λΉŒλ“œ(컴파일 λ“±)λ₯Ό μˆ˜ν–‰
μžλ™ ν…ŒμŠ€νŠΈ λΉŒλ“œκ°€ μ™„λ£Œλ˜λ©΄ ν…ŒμŠ€νŠΈ μ½”λ“œ(Unit Test, Lint λ“±)λ₯Ό μžλ™μœΌλ‘œ μˆ˜ν–‰
ν”Όλ“œλ°± 제곡 λΉŒλ“œ/ν…ŒμŠ€νŠΈ κ²°κ³Όλ₯Ό κ°œλ°œμžμ—κ²Œ μ¦‰μ‹œ μ•Œλ¦Ό(GitHub Checks, Slack λ“±)

3. CI 도ꡬ 선택

(1) GitHub Actionsλ₯Ό μ„ νƒν•œ 이유

  • GitHub μ €μž₯μ†Œ 연동 μ΅œμ ν™”.github/workflows/*.yml μ„€μ •λ§ŒμœΌλ‘œ λ°”λ‘œ μ‚¬μš© κ°€λŠ₯
  • 가볍고 λΉ λ₯Έ 초기 μ„€μ •: Jenkins처럼 별도 μ„œλ²„ ν•„μš” μ—†μŒ
  • λΉ„μš© 효율: Public μ €μž₯μ†Œ 무료, Private μ €μž₯μ†Œλ„ μ›” 2,000λΆ„ 무료
  • 500mb / 2000λΆ„(μ›”) 무료 μ‚¬μš©λŸ‰ κΈ°μ€€μœΌλ‘œ 5λΆ„μ˜ ci 속도λ₯Ό κ°€μ •. ν•˜λ£¨μ— 10νšŒμ”© 컀밋 ν…ŒμŠ€νŠΈλ₯Ό 싀행해도 μ—¬μœ λ‘œμš°λ―€λ‘œ, 초기 ν”„λ‘œμ νŠΈ μ§„ν–‰ 쀑 ν¬λ ˆλ”§μ΄ λͺ¨μžλΌμ§€ μ•Šμ„ κ²ƒμœΌλ‘œ μΆ”μ‚°.
  • ν’λΆ€ν•œ μƒνƒœκ³„μ™€ λ¬Έμ„œ: λ‹€μ–‘ν•œ μ˜ˆμ œμ™€ ν…œν”Œλ¦Ώ 제곡

(2) λ‹€λ₯Έ CI 도ꡬλ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šμ€ 이유

도ꡬ μž₯점 단점 뢀적합 이유
Jenkins μ˜€ν”ˆμ†ŒμŠ€, μœ μ—°μ„± μ„€μΉ˜Β·μš΄μ˜ λΆ€λ‹΄ 큼 단일 EC2에 JenkinsλŠ” 과함
Travis CI GitHub 연동 쉬움 느린 속도, μš”κΈˆμ œ μ œμ•½ 무료 ν”Œλžœ 운영 λΆˆκ°€
CircleCI λΉ λ₯Έ λΉŒλ“œ λ³΅μž‘ν•œ YAML ꡬ쑰 과볡작
GitLab CI GitLab 톡합 GitHub 연동 뢈편 뢀적합
Bitbucket Pipelines Bitbucket μ΅œμ ν™” κΈ°λŠ₯ μ œν•œ GitHub μ‚¬μš© 쀑

CI/CD 도ꡬ μ˜ˆμ‹œ: Jenkins와 GitHub Actions 비ꡐ


4. νŒŒμ΄ν”„λΌμΈ ꡬ성

(1) 브랜치 운영 흐름

브랜치 λͺ©μ  νŠΉμ§•
feature/* κΈ°λŠ₯ 개발 Lint/Unit Test μžλ™ μˆ˜ν–‰, λΉ λ₯Έ κΈ°λŠ₯ μ™„μ„±
develop 톡합 ν…ŒμŠ€νŠΈ μ—¬λŸ¬ feature 병합 ν›„ 톡합 ν…ŒμŠ€νŠΈ μˆ˜ν–‰
main 운영 배포 μ΅œμ’… λΉŒλ“œ μˆ˜ν–‰, ν’ˆμ§ˆ 검증 μ™„λ£Œ μ½”λ“œλ§Œ 병합
hotfix/* κΈ΄κΈ‰ μˆ˜μ • 운영 쀑 버그 μˆ˜μ • 및 병합 ν›„ 재반영

(2) CI νŒŒμ΄ν”„λΌμΈ 흐름

단계 λͺ©μ  μ„€λͺ… 도ꡬ 및 μ–Έμ–΄
μ½”λ“œ 톡합 (Checkout) μ΅œμ‹  μ½”λ“œ μžλ™ μˆ˜μ‹  GitHub에 Push ν˜Ήμ€ PR이 λ°œμƒν•˜λ©΄ CIκ°€ ν•΄λ‹Ή 브랜치의 μ½”λ“œλ₯Ό μžλ™μœΌλ‘œ κ°€μ Έμ˜΄ actions/checkout
μ˜μ‘΄μ„± μ„€μΉ˜ / μžλ™ λΉŒλ“œ μ‹€ν–‰ ν™˜κ²½ μ€€λΉ„ + μ½”λ“œ λΉŒλ“œ package.json, pom.xml, requirements.txtμ—μ„œ μ˜μ‘΄μ„± μ„€μΉ˜ ν›„ λΉŒλ“œ μˆ˜ν–‰ ν”„λ‘ νŠΈ: λ²ˆλ“€λ§ λ°±μ—”λ“œ: 컴파일 AI: λͺ¨λΈ λ‘œλ”© 및 ν…ŒμŠ€νŠΈ ν™˜κ²½ ꡬ성 Node: npm ci, Java: ./gradlew build, Python: pip install -r
정적 뢄석 (Lint) μ½”λ“œ μŠ€νƒ€μΌ 일관성 μœ μ§€ 문법 였λ₯˜, μŠ€νƒ€μΌ 였λ₯˜ λ“± μžλ™ 뢄석. 사전 룰을 ν†΅κ³Όν•˜μ§€ λͺ»ν•˜λ©΄ PR κ±°λΆ€ FE: Prettier BE: Checkstyle AI: flake8
λ‹¨μœ„ ν…ŒμŠ€νŠΈ (Unit Test) κΈ°λŠ₯ λ‹¨μœ„ μ •ν™•μ„± 검증 ν•¨μˆ˜, 클래슀 λ“± μ΅œμ†Œ λ‹¨μœ„κ°€ λ…λ¦½μ μœΌλ‘œ μ˜¬λ°”λ₯΄κ²Œ μž‘λ™ν•˜λŠ”μ§€ μžλ™ 검증 FE: Jest BE: JUnit5 AI: pytest
톡합 ν…ŒμŠ€νŠΈ (Integration Test) μ„œλΉ„μŠ€ 연동 확인 μ—¬λŸ¬ μ»΄ν¬λ„ŒνŠΈκ°€ μ‹€μ œμ²˜λŸΌ ν†΅ν•©λ˜μ–΄ λ™μž‘ν•˜λŠ”μ§€ ν…ŒμŠ€νŠΈ FE: Cypress BE: Spring ν™˜κ²½ AI: FastAPI
ν…ŒμŠ€νŠΈ 컀버리지 μΈ‘μ • ν…ŒμŠ€νŠΈ λ²”μœ„ 확인 전체 μ½”λ“œ 쀑 ν…ŒμŠ€νŠΈκ°€ 적용된 λΉ„μœ¨ μΈ‘μ •. 80% 이상 ꢌμž₯ coverage, pytest-cov, jacoco
κ²°κ³Ό 확인 및 μ•Œλ¦Ό μ‹€μ‹œκ°„ ν”Όλ“œλ°± 제곡 Slack으둜 μ•Œλ¦Ό 전솑. μ‹€νŒ¨ μ‹œ λΉ λ₯Έ λŒ€μ‘ κ°€λŠ₯ Slack Webhook

(3) ν”„λ‘ νŠΈμ—”λ“œ .github/workflows/frontend-deploy-s3.yml μ—μ„œμ˜ ci μ£Όμš” ꡬ쑰

name: Deploy Frontend React s3 static hosting

on:
  workflow_dispatch:  

jobs:
  build-deploy-frontend:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '20.19.1'

      - name: Install dependencies
        run: npm ci

      - name: Generate .env file for Vite (based on branch)
        run: |
          echo "[FRONTEND] Generating .env file for branch: ${{ github.ref_name }}"
          
          if [ "${{ github.ref_name }}" = "main" ]; then
            echo "VITE_API_BASE_URL=https://ktbkoco.com/" >> .env
            echo "VITE_REDIRECT_URL=https://ktbkoco.com/oauth/kakao/callback" >> .env
            echo "VITE_KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}" >> .env
          else
            echo "VITE_API_BASE_URL=https://koco.click/" >> .env
            echo "VITE_REDIRECT_URL=https://koco.click/oauth/kakao/callback" >> .env
            echo "VITE_KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}" >> .env
          fi

      - name: Build React App
        run: npm run build


(4) λ°±μ—”λ“œ .github/workflows/deploy.yml μ—μ„œμ˜ ci μ£Όμš” ꡬ쑰

name: Deploy Spring Boot App

on:

  workflow_dispatch:
    inputs:
      image_tag:
        description: 'ECR 이미지 νƒœκ·Έ (예: v1.0.0)'
        required: true

env:
  AWS_REGION: ap-northeast-2
  ECR_HOST: ${{ secrets.ECR_HOST }}
  IMAGE_TAG: ${{ github.event.inputs.image_tag }}
  DEPLOY_ZIP_KEY: spring-app-deploy.zip

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      # ───────────────────────────────────────────────────────────────
      # 1) Set dynamic S3 bucket and ECR repo name based on branch
      # ───────────────────────────────────────────────────────────────
      - name: Configure environment for branch
        run: |
          BRANCH=${GITHUB_REF#refs/heads/}
          if [ "$BRANCH" = "dev" ]; then
            echo "S3_BUCKET=dev-koco-codedeploy-artifacts" >> $GITHUB_ENV
            echo "ECR_REPO_NAME=dev-app-repo"               >> $GITHUB_ENV
            echo "SSM_PATH=/spring/dev/"                    >> $GITHUB_ENV
            echo "CD_APP_NAME=dev-was-deploy-app"           >> $GITHUB_ENV
            echo "CD_DG_NAME=dev-was-deploy-group"          >> $GITHUB_ENV
          else
            echo "S3_BUCKET=prod-koco-codedeploy-artifacts" >> $GITHUB_ENV
            echo "ECR_REPO_NAME=prod-app-repo"              >> $GITHUB_ENV
            echo "SSM_PATH=/spring/prod/"                   >> $GITHUB_ENV
            echo "CD_APP_NAME=prod-was-deploy-app"          >> $GITHUB_ENV
            echo "CD_DG_NAME=prod-was-deploy-group"         >> $GITHUB_ENV
          fi

      # ───────────────────────────────────────────────────────────────
      # 2) If manual dispatch, check for existing tag
      # ───────────────────────────────────────────────────────────────
      - name: Check if tag already exists in ECR
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          FULL_REPO="$ECR_HOST/$ECR_REPO_NAME"
          echo "πŸ” Checking if tag '$IMAGE_TAG' exists at $FULL_REPO..."
          RESULT=$(aws ecr describe-images \
            --registry-id ${ECR_HOST%%.*} \
            --repository-name $ECR_REPO_NAME \
            --image-ids imageTag=$IMAGE_TAG \
            --region $AWS_REGION 2>/dev/null || true)
          if echo "$RESULT" | grep -q imageDigest; then
            echo "❌ ERROR: Tag '$IMAGE_TAG' already exists in $FULL_REPO."
            exit 1
          fi

      # ───────────────────────────────────────────────────────────────
      # 3) Build & Push Docker image for both dev and main
      # ───────────────────────────────────────────────────────────────
      - name: Build and Push Docker image to ECR
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          FULL_REPO="$ECR_HOST/$ECR_REPO_NAME"
          echo "πŸ” Logging in to Amazon ECR at $FULL_REPO..."
          aws ecr get-login-password --region $AWS_REGION \
            | docker login --username AWS --password-stdin $ECR_HOST

          echo "πŸ”§ Building Docker image..."
          docker build -t my-spring-app .

          echo "🏷 Tagging image with version: $IMAGE_TAG"
          docker tag my-spring-app:latest $FULL_REPO:$IMAGE_TAG

          echo "🏷 Tagging image as latest"
          docker tag my-spring-app:latest $FULL_REPO:latest

          echo "πŸ“€ Pushing image $FULL_REPO:$IMAGE_TAG"
          docker push $FULL_REPO:$IMAGE_TAG

          echo "πŸ“€ Pushing image $FULL_REPO:latest"
          docker push $FULL_REPO:latest

5. ν…ŒμŠ€νŠΈ 컀버리지 λͺ©ν‘œ

  • 컀버리지 κΈ°μ€€: 80% 이상 ꢌμž₯
  • μ‚¬μš© 도ꡬ μ˜ˆμ‹œ:
    • Frontend: Jest + coverage
    • Backend: Jacoco
    • AI: pytest-cov
  • Slack λ©”μ‹œμ§€λ‚˜ PR μ½”λ©˜νŠΈμ— 컀버리지 μš”μ•½ 포함

6. λΉŒλ“œ 성곡 쑰건

ν•­λͺ© 쑰건
린트 λͺ¨λ“  μ„œλΉ„μŠ€μ—μ„œ 톡과
ν…ŒμŠ€νŠΈ λͺ¨λ“  μ„œλΉ„μŠ€μ—μ„œ μ‹€νŒ¨ 없이 톡과
컀버리지 선택적 κΈ°μ€€ 만쑱 (ꢌμž₯ 80% 이상)
μŠ¬λž™ μ•Œλ¦Ό 성곡/μ‹€νŒ¨ μ—¬λΆ€ λ¬΄κ΄€ν•˜κ²Œ 항상 전솑
μ’…λ£Œ μ‹€νŒ¨ μ‹œ 단계 쀑단 및 μƒνƒœμ½”λ“œ λ°˜ν™˜