Cloud 2단계 CI 설계 - 100-hours-a-week/KTB4-5th-wiki GitHub Wiki

다먹자 v1 인프라 위키 — 2단계: CI(지속적 통합) 파이프라인 설계

KTB 4조 다먹자 팀의 v1 인프라 설계 문서입니다. 이 문서는 2단계(CI 파이프라인 설계) 만 다룹니다. 1단계(EC2 + Docker Compose 기반 초기 배포)는 별도 문서 ktb4-cloud-wiki.md를 참고하세요. 각 결정 항목은 상황 → 결론 → 이유 순서로 정리했습니다.

목차

  1. CI 도구 선택 및 필요성
  2. CI Trigger 정책 설계
  3. CI Pipeline 설계
  4. Docker Image 관리 전략
  5. 실패 처리 정책

상황 및 필요성

상황: AI 1명, 풀스택 3명, 클라우드 1명이 한 서비스를 함께 개발하면서 코드 변경 빈도가 늘어남 — merge 이후 빌드 실패, 테스트 안 된 코드가 main에 반영, 서로 다른 개발 환경, Docker Image build 실수, 잘못된 버전 배포 같은 문제가 생길 수 있음.

필요성: Source Code → Test → Build → Docker Image로 이어지는 흐름을 자동화해서, 문제 있는 코드가 다음 단계(배포)로 넘어가지 않도록 품질 Gate를 두는 것이 목적. 최종 산출물이 Docker Image이므로 이 흐름이 한 파이프라인으로 일관되게 연결돼야 함.


5. CI 도구 선택 및 필요성

  • 상황: 저장소를 GitHub로 쓰고 있고, 1단계에서 Image Registry로 GHCR을 이미 선택하는 등 인프라 전반이 이미 GitHub에 종속적으로 설계되어 있음. 별도 CI 서버를 새로 구축·운영할 여력도 없음.

  • 고려한 후보:

    후보 방식 이 상황에서의 장점 이 상황에서의 단점
    GitHub Actions GitHub 저장소 내장형 별도 설치·서버 불필요, GITHUB_TOKEN만으로 GHCR push 인증 가능해 1단계 Registry 선택과 바로 연동 무료 티어의 실행 시간·동시 실행 수 제한(Private repo에만 해당), GitHub 종속
    Jenkins (자체 호스팅) 별도 서버에 설치하는 범용 CI 플러그인 생태계가 넓고 커스터마이징 자유도 높음, 특정 SaaS에 종속되지 않음 별도 EC2 구축·운영·보안 패치 부담이 추가로 발생 (v1 규모엔 과함)
    GitLab CI/CD GitLab 저장소 내장형 GitHub Actions와 유사한 이점(내장, 별도 서버 불필요) 저장소를 GitLab으로 이전해야 사용 가능 — 현재 GitHub 사용과 충돌
  • 결론: GitHub Actions

  • 이유:

    1. 인프라가 이미 GitHub에 종속적으로 설계됨 — 저장소, 1단계에서 정한 Image Registry(GHCR)까지 GitHub 생태계 안에 있어, CI도 같은 생태계에서 해결하는 것이 가장 자연스러움.
    2. 개발자들이 가장 많이 사용하는 CI 도구 — 팀원 전원이 익숙하거나 빠르게 익힐 수 있고, 참고할 자료·커뮤니티 지원이 풍부함.
    3. 설정이 간단함 — 별도 서버·에이전트 설치 없이 저장소에 워크플로 파일만 추가하면 바로 동작.
  • 트레이드오프: 위 이점 대신 감수하는 부분은 GitHub 종속성 심화(현재도 저장소·Registry가 GitHub 기반이라 추가로 잃는 유연성은 제한적이라고 판단)뿐임. 원래는 무료 실행 시간·동시 실행 수 제한(Jenkins처럼 직접 서버를 운영하면 없는 제약)도 감수할 부분으로 봤으나, 이 제한은 Private repo에만 적용되고 8번에서 Repository를 Public으로 운영하기로 결정하면서 사실상 해소됨. 그래도 Jenkins를 택했을 때 발생했을 별도 EC2 구축·운영·보안 패치 부담(v1 규모엔 과함)을 지지 않아도 된다는 점은 여전히 이 선택의 이점으로 남음.

6. CI Trigger 정책 설계

  • 상황: dev/prod 환경이 분리되어 있고(1단계), PR 단계에서 문제를 걸러야 main(prod)의 안정성을 지킬 수 있음. Repository를 Public으로 운영하기로 해(8번 참고) GitHub Actions 실행 시간이 사실상 무제한이라, PR마다 검사를 돌리는 데 자원 제약이 없음.

  • 결론:

    트리거 대상 하는 일
    Pull Request dev, main으로 향하는 PR 빌드 + 테스트만 실행 (이미지 push 없음) — 머지 전 품질 게이트
    Push (merge 완료) dev 테스트 → 빌드 → Docker Image build/push (dev 태그, Git SHA)
    Push (merge 완료) main 테스트 → 빌드 → Docker Image build/push (prod 태그, Git SHA)
    Tag 생성 — v1 단계에서는 미사용
  • 이유: PR 트리거는 "merge 이후 빌드 실패", "테스트 안 한 코드가 main에 반영"되는 문제를 병합 전에 차단하기 위함. 모든 PR마다 빠짐없이 빌드+테스트를 돌리기로 한 것도 Public repo라 GitHub Actions 자원이 무제한이라 가능한 설계 — Private였다면 무료 실행 시간 한도 때문에 PR 검사 범위를 아꼈어야 했을 것. dev/main 각각에 별도 push 트리거를 두는 이유는 1단계의 dev/prod 환경 분리를 그대로 CI에 반영해, 각 브랜치의 최신 커밋이 곧 각 환경에 올라갈 이미지가 되도록 하기 위함(3단계 CD가 이 이미지를 가져다 배포). Tag 트리거는 지금 규모에서는 별도 릴리즈 버전 관리가 필요 없다고 보고 보류.

  • 트레이드오프:

    • PR마다 빌드+테스트가 도는 자체는 Public repo 덕에 실행 시간 비용이 들지 않지만, 그만큼 동시에 여러 PR이 겹치면 GitHub Actions 큐가 밀려 검사 대기 시간이 늘어날 가능성은 남아있음 — 그래도 팀원들이 개발한 코드들이 합쳐지는 공간에 최소한의 안정성은 있어야 한다고 생각하여 이렇게 설계함.
    • dev/main 각각에 별도 push 트리거를 두면 두 워크플로를 따로 유지·수정해야 하는 부담이 생김(하나를 고치면 다른 쪽도 확인 필요) — 대신 각 브랜치가 독립적으로 자기 환경의 이미지를 만들어, 한쪽 배포가 다른 쪽에 영향을 주지 않는 안전성을 얻음.

7. CI Pipeline 설계

  • 상황: 최종 산출물이 Docker Image이므로 Source Code → Test → Build → Docker Image가 하나의 흐름으로 이어져야 함. BE(Spring/JVM), FE, AI가 스택이 달라 검증 방식도 각자 다름.

  • 결론: 컴포넌트별로 아래 단계를 거치는 파이프라인. Lint/정적 분석은 v1 파이프라인에 포함하지 않음.

    1. Checkout
    2. 의존성 설치 (BE: Gradle/Maven, FE: npm/yarn, AI: pip 등)
    3. 단위 테스트 실행
    4. 빌드 (BE: JAR, FE: 정적 빌드 산출물)
    5. Docker Image Build
    6. (PR에서는 여기서 종료) / (dev·main push에서만) GHCR Push — Git SHA 태그

    Lint / 정적 분석을 제외한 이유

    • 상황: Lint/정적 분석 도구를 CI 단계에 포함할지 결정해야 함.
    • 결론: 포함하지 않음
    • 이유: Lint/정적 분석은 코드 스타일·품질을 관리하는 유지보수의 영역인데, v1은 기능 구현 자체가 우선순위이기 때문에 이 기준으로 제외하기로 함.
    • 트레이드오프: Lint를 넣으면 코드 스타일 불일치나 잠재적 버그를 초기에 더 빨리 잡을 수 있지만, 규칙 설정·예외 처리에 시간이 들어 지금 우선순위인 "기능부터 빠르게 구현"하는 속도를 늦출 수 있음. 유지보수 단계에 들어가면 재검토.

    모노레포 vs 폴리레포 선택

    • 상황: BE/FE/AI 코드를 하나의 저장소에 둘지, 컴포넌트별로 별도 저장소를 둘지 결정해야 함 — 파이프라인을 하나로 둘지 컴포넌트별로 나눌지에 직접 영향.
    • 결론: 폴리레포 — BE/FE/AI 각각 별도 저장소로 운영
    • 이유: 모노레포는 지금 당장은 저장소가 하나뿐이라 더 빠르게 시작할 수 있지만, 이후 컴포넌트가 늘어나거나 팀이 커지는 확장 상황에서 워크플로 필터링·배포 단위 분리 등이 점점 복잡해져 관리가 어려워질 수 있다고 판단. 폴리레포는 처음부터 저장소가 컴포넌트 단위로 나뉘어 있어 이런 확장에 구조적으로 더 잘 대응할 수 있어, 확장성을 우선해 선택함.
    • 트레이드오프: 폴리레포는 저장소를 3개(BE/FE/AI) 각각 세팅해야 해서, v1이 우선시하는 "빠른 구축"과는 실제로 방향이 반대임 — 이 부분은 팀도 인지하고 있는 모순. 또한 API 스펙처럼 여러 컴포넌트에 걸친 변경은 PR을 저장소별로 나눠 올려야 해 한 번에 리뷰하기 어려움. 그럼에도 나중에 모노레포를 폴리레포로 쪼개는 전환 비용(저장소 분리, 히스토리 이전 등)이 지금 폴리레포로 시작하는 비용보다 크다고 보고, 당장의 속도보다 향후 확장성을 우선해 이 트레이드오프를 감수함.
    flowchart LR
        PR([PR 생성]) --> Checkout1[Checkout]
        Checkout1 --> Test1[Test]
        Test1 -->|실패| Block[머지 차단]
        Test1 -->|통과| Build1[Build]
        Build1 --> Gate{머지 승인}
    
        Gate -->|dev/main에 merge| Checkout2[Checkout]
        Checkout2 --> Test2[Test]
        Test2 --> Build2[Build]
        Build2 --> DImg[Docker Image Build]
        DImg -->|실패| Notify1[실패 알림, Push 안 함]
        DImg -->|성공| Push[GHCR Push<br/>Git SHA 태그]
    
    Loading
  • 이유: PR 단계와 merge 단계를 분리한 이유는, PR에서는 "코드가 안전한가"만 빠르게 검증하고(이미지 push는 리소스 낭비), 실제 배포에 쓰일 이미지는 merge가 확정된 커밋에서만 만들어지도록 해 "잘못된 버전의 이미지 배포"를 원천 차단하기 위함. 컴포넌트별로 단계를 나눈 이유는 BE/FE/AI가 서로 다른 언어·빌드 도구를 쓰기 때문 — 한 파이프라인에 모두 우겨넣기보다 각자의 워크플로로 분리하는 편이 실패 원인 파악과 유지보수에 유리하다고 판단.

  • 트레이드오프:

    • PR 단계와 merge 단계로 나누면 사실상 같은 코드에 대해 Checkout·Test·Build를 두 번(PR 때 한 번, merge 때 한 번) 반복하게 되어 총 CI 실행 시간·자원이 늘어남 — 그래도 이미지 push까지 PR 단계에서 해버리면 병합되지 않을 수도 있는 코드로 이미지를 만드는 낭비가 생기므로, 이 중복 비용을 감수하는 편을 택함.
    • 컴포넌트별로 워크플로를 나누면 실패 원인 파악과 유지보수는 쉬워지지만, 워크플로 파일(BE/FE/AI 각각)을 따로 작성·관리해야 해서 초기 설정 공수와 이후 공통 로직 변경 시 여러 파일을 동시에 고쳐야 하는 부담이 생김.

8. Docker Image 관리 전략

  • 상황: 1단계에서 Registry는 GHCR, Tag 정책은 Git SHA로 이미 결정함. CI 단계에서 이를 구체적인 운영 규칙으로 풀어야 함.

  • 결론:

    • Registry: GHCR (ghcr.io/100-hours-a-week/<서비스명>-<컴포넌트>)
    • Repository 공개 범위: Public
    • Repository 구성: 컴포넌트별로 이미지 분리 (예: -be, -fe, -ai) — 각자 배포 주기·의존성이 다름
    • Tag 정책: Git SHA 태그를 기본으로 사용(1단계 결정 유지). 브랜치명 태그(dev, main)는 "가장 최근 이미지"를 가리키는 참조용 별칭으로만 병행
    • latest 사용 여부: 배포에는 사용하지 않음. (이미 latest를 사용해서 최신 버전이 적용되지 않아서 버그를 찾느라 고생한 적이 있음.)
    • 오래된 이미지 정리: 하지 않음 — Public repo로 전환해 GHCR 저장 용량 제한이 사실상 없어졌기 때문
  • 이유: 컴포넌트별로 이미지를 분리하면 한 컴포넌트만 변경됐을 때 그 이미지만 다시 빌드·배포할 수 있어 빌드 시간과 배포 범위를 줄일 수 있음. Git SHA를 기본 태그로 유지하는 이유는 1단계와 동일 — 커밋 단위로 정확히 어떤 버전인지 추적하고 롤백할 수 있어야 하기 때문. latest를 배포에 안 쓰는 이유도 같은 맥락: latest는 태그가 계속 덮어써져서 "지금 떠 있는 게 정확히 어떤 커밋인지" 알 수 없고, latest 사용으로 실제로 최신 버전이 반영 안 된 채 디버깅에 시간을 쓴 경험이 있어 더더욱 지양.

  • 트레이드오프:

    • 컴포넌트별 이미지 분리는 빌드 범위를 줄여주지만, 관리해야 할 Repository·워크플로 수가 컴포넌트 수만큼 늘어나 초기 설정과 이후 공통 변경(예: 베이스 이미지 업데이트) 작업이 그만큼 반복됨.
    • Git SHA 태그는 롤백 시점을 정확히 특정할 수 있지만, dev/latest 같은 사람이 읽기 쉬운 태그에 비해 "지금 무엇이 떠 있는지"를 한눈에 파악하기 어려움 — 이를 보완하기 위해 브랜치명 태그를 참조용으로 병행하기로 함.

    Repository를 Public으로 정한 이유

    • 상황: GHCR의 Private 패키지는 무료 저장 용량(500MB)·대역폭에 제한이 있어, merge마다 이미지가 쌓이면 한도를 넘길 수 있음. Public 패키지는 이 제한이 사실상 없음.
    • 결론: Public
    • 이유: MVP(v1) 단계라 코드나 이미지에 보안적으로 민감한 정보가 없다고 판단. Secret은 이미 1단계에서 GitHub Secrets로 분리해뒀고, .env에도 노출돼도 괜찮은 값만 두기로 했기 때문에, Public으로 전환해도 실질적으로 새로 노출되는 민감 정보는 없다고 봄.
    • 트레이드오프: Public으로 하면 GitHub Actions 실행 시간·GHCR 저장 용량 제한이 사실상 사라져 이미지 정리 같은 운영 부담도 함께 없앨 수 있음. 대신 코드·커밋 이력·이미지 목록이 전부 외부에 공개되므로, 앞으로 실수로 민감한 값을 커밋하면 되돌리기 어려운 형태로 노출됨 — 지금 세워둔 Secret 관리 원칙(GitHub Secrets만 사용, .env엔 민감하지 않은 값만)을 앞으로도 지키는 것이 더 중요해짐.

    오래된 이미지를 정리하지 않기로 한 이유

    • 상황: Repository를 Public으로 전환하면서 GHCR 저장 용량 제약이 사실상 사라짐.
    • 결론: 별도 정리 없이 이미지를 계속 보관
    • 이유: 저장 용량 문제가 없어졌으니 저장공간 확보를 위해 정리할 필요가 없다고 생각함. 모든 SHA 태그 이미지가 남아있으면 임의 시점으로 언제든 롤백할 수 있다는 이점도 있음.
    • 트레이드오프: 이미지를 정리하지 않으면 GHCR 패키지 목록이 시간이 지날수록 계속 길어져 "지금 실제로 쓰이는 이미지가 뭔지" 찾기 번거로워질 수 있음 — 다만 브랜치명 태그(dev/main)를 참조용으로 병행하기로 했으므로 실제 운영에는 그 태그로 접근하면 되어 이 불편은 크지 않다고 판단.

9. 실패 처리 정책

  • 상황: 테스트/빌드/Docker Image build 각 단계에서 실패가 날 수 있고, 실패를 팀이 바로 알아야 다음 개발이 지연되지 않음. 팀 규모는 AI 1명, 풀스택 3명, 클라우드 1명 총 6명.
  • 결론:
    실패 지점 처리
    테스트 실패 PR 머지 차단(브랜치 보호 규칙), 다음 단계(빌드) 진행 안 함
    Build 실패 파이프라인 즉시 중단, GitHub Actions 로그로 원인 확인
    Docker Image Build 실패 GHCR Push 하지 않음 — 실패한 이미지가 배포 대상이 되는 일 자체를 차단
    실패 인지 방법 PR의 GitHub Actions 상태 체크(빨간 X)로만 확인. 별도 알림 채널(Slack/Discord 등)은 v1에서는 도입하지 않음
  • 이유: 실패한 코드가 다음 단계로 넘어가지 않도록 각 단계에서 막는 것이 CI의 핵심 목적임. 별도 알림 채널을 도입하지 않기로 한 이유는, 팀 규모가 6명으로 작아 PR 상태 체크만으로도 실패를 놓칠 위험이 크지 않고, 이 규모에서 별도 채널을 구축·유지보수할 필요가 없다고 생각했음.
  • 트레이드오프:
    • 별도 알림 채널을 두면 PR을 안 열어본 팀원도 실패를 놓치지 않아 확인이 더 편해지지만, 채널을 새로 연동·유지보수해야 하는 비용이 발생함. v1은 팀 규모가 작아 이 비용을 감수할 실익이 크지 않다고 보고 도입하지 않기로 함 — 팀·서비스 규모가 커지면 재검토가 필요함.
⚠️ **GitHub.com Fallback** ⚠️