Theme:

코드를 짜는 것보다 코드를 안전하게 배포하는 게 더 어렵다. PR 머지하고 수동으로 서버 들어가서 git pull 치던 시절은 이제 없다고 봐야 한다. 이 글에서는 CI/CD의 핵심 개념부터 배포 전략, 파이프라인 구성, 자주 나오는 꼬리 질문까지 정리했다.


CI — Continuous Integration

CI는 개발자들이 작성한 코드를 자주, 주기적으로 메인 브랜치에 머지 하고, 머지할 때마다 자동으로 빌드와 테스트를 돌리는 것 을 말해요.

왜 필요할까요? 각자 브랜치에서 한 달씩 작업하고 나중에 한꺼번에 머지하면 어떻게 되는지 생각해보면, 충돌이 수십 개 터지고, 테스트는 깨지고, 누가 언제 뭘 잘못했는지 추적하기도 어렵습니다. 이걸 "머지 지옥(merge hell)"이라 부르는데, CI는 이걸 예방하는 게 핵심이에요.

CI가 제대로 동작하려면 이런 게 갖춰져야 합니다.

  • 모든 개발자가 하루에 최소 한 번은 메인 브랜치에 머지 합니다
  • 머지할 때마다 자동으로 빌드 가 돌아갑니다
  • 빌드 후 자동으로 테스트 가 실행됩니다
  • 빌드나 테스트가 실패하면 즉시 알림 이 갑니다
  • 실패한 빌드를 고치는 게 최우선 과제 가 됩니다

단순히 도구를 도입한다고 CI가 되는 게 아닙니다. 팀 전체가 "자주 머지하고, 빌드가 깨지면 바로 고친다"는 문화를 공유해야 진짜 CI라고 할 수 있어요.


CD — Continuous Delivery vs Continuous Deployment

CD는 두 가지 의미로 쓰입니다. 비슷해 보이지만 차이가 꽤 커요.

Continuous Delivery (지속적 전달)

CI를 통해 빌드·테스트된 코드를 언제든 프로덕션에 배포할 수 있는 상태로 유지 하는 것입니다. 여기서 중요한 건, 실제 배포는 사람이 버튼을 눌러서 한다는 점이에요.

PLAINTEXT
코드 푸시 → 자동 빌드 → 자동 테스트 → 스테이징 배포 → [수동 승인] → 프로덕션 배포

Continuous Deployment (지속적 배포)

여기선 수동 승인 단계가 없어요. 테스트를 통과하면 자동으로 프로덕션까지 배포 됩니다.

PLAINTEXT
코드 푸시 → 자동 빌드 → 자동 테스트 → 자동 프로덕션 배포

뭐가 더 좋은가?

정답은 없습니다. Continuous Deployment가 더 진보적으로 보이지만, 금융이나 의료처럼 규제가 강한 도메인에서는 반드시 수동 승인을 거쳐야 하는 경우가 많아요. 반면 SaaS 제품이나 내부 도구는 Continuous Deployment가 훨씬 효율적입니다.

이 둘의 차이는 "수동 승인의 유무" 로 깔끔하게 정리할 수 있어요.


GitHub Actions

요즘 가장 많이 쓰이는 CI/CD 도구 중 하나예요. GitHub 저장소에 YAML 파일 하나 넣으면 바로 파이프라인이 돌아갑니다.

핵심 개념

개념설명
WorkflowCI/CD 파이프라인 자체. .github/workflows/ 디렉토리에 YAML로 정의한다
EventWorkflow를 트리거하는 이벤트. push, pull_request, schedule
JobWorkflow 안의 작업 단위. 기본적으로 ** 병렬** 실행된다
StepJob 안의 개별 명령. 순서대로 실행된다
RunnerJob이 실제로 돌아가는 서버. GitHub-hosted 또는 self-hosted
Action재사용 가능한 Step 묶음. actions/checkout@v4 같은 것들

YAML 구조 예시

YAML
name: CI Pipeline

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

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - name: 코드 체크아웃
        uses: actions/checkout@v4

      - name: JDK 17 설정
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Gradle 빌드
        run: ./gradlew build

      - name: 테스트 실행
        run: ./gradlew test

  deploy:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - name: 프로덕션 배포
        run: echo "배포 스크립트 실행"

몇 가지 포인트를 짚어볼게요.

  • on에서 어떤 이벤트에 반응할지 정합니다. pushpull_request를 분리해서 다른 Job을 돌릴 수도 있어요
  • needs: build-and-test를 걸면 빌드/테스트 Job이 성공해야 배포 Job이 실행됩니다
  • if 조건으로 main 브랜치 푸시일 때만 배포하게 제한할 수 있어요
  • GitHub-hosted runner는 매번 깨끗한 환경에서 시작하니까 캐시 설정을 안 하면 매번 의존성을 새로 받습니다

Secrets 관리

환경 변수나 API 키는 GitHub 저장소의 Settings → Secrets에 등록하고, Workflow에서 ${{ secrets.MY_SECRET }} 형태로 참조합니다. 절대 YAML 파일에 하드코딩하면 안 돼요.


Jenkins vs GitHub Actions vs GitLab CI

세 도구 다 CI/CD를 할 수 있지만 성격이 꽤 다릅니다.

항목JenkinsGitHub ActionsGitLab CI
** 호스팅**직접 서버에 설치 (self-hosted)GitHub 클라우드 (self-hosted도 가능)GitLab 클라우드 또는 self-hosted
** 설정 방식**Jenkinsfile (Groovy)YAML.gitlab-ci.yml (YAML)
** 플러그인**1,800개 이상의 플러그인 생태계Marketplace의 Action들내장 기능 위주
** 러닝 커브**높음 (플러그인 설정, 관리)낮음중간
** 확장성**매우 높음 (자유도 최상)중간중간
** 비용**서버 비용만 (오픈소스)무료 tier 있음 (월 2,000분)무료 tier 있음 (월 400분)

Jenkins를 아직 쓰는 이유

오래된 도구인데도 살아남은 이유가 있습니다. 일단 자유도가 압도적이에요. 어떤 빌드 환경이든, 어떤 언어든, 어떤 인프라든 플러그인 조합으로 다 커버할 수 있습니다. 대기업에서 레거시 시스템과 연동해야 하거나, 보안 정책상 클라우드에 코드를 올릴 수 없는 경우에 Jenkins가 선택지가 돼요.

다만 Jenkins 자체를 관리하는 것도 일입니다. 플러그인 버전 충돌, 서버 장애, 업그레이드 호환성 같은 운영 부담이 커요.

GitHub Actions를 고르는 이유

이미 GitHub을 쓰고 있다면 별도 설정 없이 바로 시작할 수 있다는 게 가장 큽니다. PR과 Issue와의 통합도 자연스럽고, 마켓플레이스에서 남들이 만든 Action을 가져다 쓸 수 있어서 생산성이 높아요.

GitLab CI

GitHub Actions와 비슷하지만, GitLab 자체가 소스 관리부터 CI/CD, 모니터링, 보안 스캔까지 올인원으로 제공한다는 점이 차이예요. DevOps 전체를 하나의 플랫폼에서 해결하고 싶은 조직이 선호합니다.


배포 전략

코드를 프로덕션에 내보내는 방법도 여러 가지가 있어요. 각각 트레이드오프가 뚜렷합니다.

Rolling Update

서버를 하나씩 순서대로 새 버전으로 교체하는 방식입니다. Kubernetes의 기본 배포 전략이기도 해요.

PLAINTEXT
서버 A (v1) → 서버 A (v2)  ← 업데이트 완료
서버 B (v1) → 서버 B (v1)  ← 아직 대기 중
서버 C (v1) → 서버 C (v1)  ← 아직 대기 중

A가 끝나면 B, B가 끝나면 C. 이런 식으로 하나씩 바꿔나갑니다.

** 장점**

  • 추가 인프라가 필요 없어요
  • 점진적이라서 전체 장애 위험이 낮습니다

** 단점**

  • 배포 중간에 v1과 v2가 ** 동시에 서비스 **됩니다 (API 호환성 문제)
  • 문제가 생기면 이미 업데이트된 서버를 다시 롤백해야 해서 시간이 걸려요

Blue-Green Deployment

운영 환경(Blue)과 동일한 새 환경(Green)을 미리 구성해놓고, 트래픽을 한 번에 전환하는 방식입니다.

PLAINTEXT
[로드밸런서] → Blue (v1)  ← 현재 트래픽
                Green (v2) ← 대기 중, 테스트 완료

전환 후:
[로드밸런서] → Green (v2) ← 트래픽 전환
                Blue (v1)  ← 롤백 대기

** 장점**

  • 전환이 순간적이라 ** 다운타임이 사실상 없습니다**
  • 문제가 생기면 로드밸런서만 Blue로 다시 돌리면 돼요. 롤백이 빠릅니다
  • v1과 v2가 동시에 서비스되는 구간이 없어요

** 단점**

  • 환경을 두 벌 유지해야 하니 ** 인프라 비용이 2배 **예요
  • 데이터베이스 마이그레이션이 걸려 있으면 단순 전환으로 안 되는 경우가 있습니다

Canary Deployment

새 버전을 전체가 아니라 ** 일부 트래픽에만 먼저 적용 **해보는 방식이에요. 이름은 광산에서 카나리아 새를 먼저 보내서 가스를 감지한 것에서 유래했습니다.

PLAINTEXT
전체 트래픽 100%
  ├── 95% → v1 (기존)
  └──  5% → v2 (카나리)

문제 없으면 점진적으로 비율을 늘려간다:
  ├── 70% → v1
  └── 30% → v2

최종:
  └── 100% → v2

** 장점**

  • 전체 사용자에게 영향을 주기 전에 ** 실제 트래픽으로 검증 **할 수 있어요
  • 에러율이나 응답 시간 같은 메트릭 기반으로 판단할 수 있습니다

** 단점**

  • 트래픽 분배를 제어하는 인프라가 필요합니다 (Nginx weight, Istio 등)
  • 모니터링 체계가 잘 갖춰져 있어야 의미 있어요
  • 배포 시간이 길어집니다

어떤 전략을 선택할까?

상황추천 전략
인프라 여유가 없다Rolling Update
빠른 롤백이 필수다Blue-Green
실서비스 영향을 최소화해야 한다Canary
DB 스키마 변경이 잦다Blue-Green + 마이그레이션 전략

실무에서는 이 전략들을 섞어 쓰기도 해요. Canary로 먼저 5% 배포해보고, 괜찮으면 Blue-Green으로 나머지를 전환하는 식입니다.


파이프라인 구성 예시

실무에서 흔히 볼 수 있는 파이프라인 구성을 단계별로 살펴볼게요.

PLAINTEXT
[코드 푸시] → [Lint] → [Build] → [Unit Test] → [Integration Test] → [Deploy to Staging] → [E2E Test] → [승인] → [Deploy to Production]

각 단계의 역할

Lint — 코드 스타일과 정적 분석이에요. ESLint, Checkstyle, SonarQube 같은 도구로 코드 품질을 잡습니다. 빌드보다 먼저 돌리는 이유는, 스타일 문제 때문에 빌드까지 기다릴 필요가 없기 때문이에요.

Build — 소스 코드를 실행 가능한 아티팩트로 만듭니다. JAR, Docker 이미지, 번들 파일 등이요.

Unit Test — 개별 함수나 클래스 단위 테스트입니다. 빠르게 돌아야 해요. 여기서 깨지면 바로 실패 처리합니다.

Integration Test — DB, 외부 API 등 실제 의존성과 함께 테스트해요. Unit Test보다 느리지만 더 현실적인 검증이 가능합니다.

Deploy to Staging — 프로덕션과 최대한 동일한 환경에 배포합니다. QA 팀이 수동 테스트를 하거나, 자동화된 E2E 테스트가 돌아가는 환경이에요.

E2E Test — 브라우저 자동화(Selenium, Playwright 등)로 사용자 시나리오를 통째로 테스트합니다.

Deploy to Production — 최종 배포예요. 위에서 다룬 배포 전략이 여기서 적용됩니다.


환경 분리 — dev / staging / production

환경을 왜 분리하는지부터 생각해볼게요. 개발자가 로컬에서 잘 되던 코드가 서버에 올리면 안 되는 경우, 대부분 환경 차이 때문입니다.

일반적인 3-tier 환경

dev — 개발자가 자유롭게 테스트하는 환경이에요. DB도 테스트 데이터로 채워져 있고, 로그 레벨도 DEBUG입니다. 여기서 뭘 하든 아무도 뭐라 안 해요.

staging — 프로덕션의 미러예요. 인프라 스펙, 환경 변수, 외부 연동까지 최대한 프로덕션과 동일하게 맞춥니다. "스테이징에서 되면 프로덕션에서도 된다"가 목표인데, 현실은 그렇지 않은 경우도 많아요.

production — 실제 사용자가 접속하는 환경입니다. 여기서 장애가 나면 매출에 직접적인 영향이 가요.

환경별 설정 관리

Spring Boot 기준으로 보면 application-{profile}.yml로 환경별 설정을 분리합니다.

YAML
# application-dev.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/myapp_dev

# application-prod.yml
spring:
  datasource:
    url: jdbc:mysql://prod-db-cluster:3306/myapp

환경 변수로 주입하는 방식도 많이 쓰여요. Docker Compose나 Kubernetes의 ConfigMap/Secret으로 관리하면 코드에 민감 정보가 들어가는 걸 막을 수 있습니다.


Infrastructure as Code (IaC)

서버를 콘솔에서 클릭으로 만들면 뭐가 문제일까요? 일단 누가 뭘 했는지 기록이 안 남습니다. 동일한 환경을 다시 만들라고 하면 기억에 의존해야 하고, 팀원에게 인수인계하기도 어려워요.

IaC는 인프라 구성을 ** 코드로 선언 **해서 버전 관리하고, 자동으로 프로비저닝하는 방식입니다.

Terraform

HashiCorp가 만든 IaC 도구로, ** 선언적 방식 **으로 인프라를 정의합니다.

HCL
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.medium"

  tags = {
    Name = "web-server"
    Env  = "production"
  }
}

이렇게 쓰면 Terraform이 알아서 EC2 인스턴스를 만들어줍니다. 이미 있으면 변경 사항만 적용하고, 삭제하면 실제 인프라도 정리해요.

** 핵심은 상태 관리(State)**입니다. Terraform은 terraform.tfstate 파일에 현재 인프라 상태를 기록해두고, 코드와 비교해서 뭘 변경해야 하는지 판단해요. 이 파일을 잘못 관리하면 인프라가 꼬이니까, 보통 S3 같은 원격 백엔드에 저장합니다.

Ansible

Red Hat이 관리하는 자동화 도구예요. Terraform이 인프라를 ** 생성 **하는 데 집중한다면, Ansible은 만들어진 서버를 ** 설정 **하는 데 강합니다. 패키지 설치, 설정 파일 배포, 서비스 재시작 같은 작업이요.

YAML
- hosts: web_servers
  tasks:
    - name: Nginx 설치
      apt:
        name: nginx
        state: present

    - name: 설정 파일 배포
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: restart nginx

SSH만 되면 별도 에이전트 설치 없이 동작한다는 게 장점이에요. 에이전트 기반인 Chef나 Puppet과 비교했을 때 진입 장벽이 낮습니다.

Terraform + Ansible 조합

실무에서는 둘을 같이 쓰는 경우가 많아요. Terraform으로 EC2, VPC, RDS 같은 인프라를 만들고, Ansible로 서버 안에 필요한 소프트웨어를 설치·설정하는 패턴입니다.


주의할 점

CI/CD 관련해서 깊이 파고들 수 있는 질문들을 정리해봤습니다.

롤백은 어떻게 하나요?

배포 전략마다 다릅니다.

  • Blue-Green: 로드밸런서 전환만 하면 돼요. 가장 빠릅니다
  • Rolling: 이미 교체된 서버들을 다시 이전 버전으로 돌려야 해요. 시간이 걸립니다
  • Canary: 카나리 트래픽을 0%로 내리면 됩니다

Docker 이미지 기반이라면 이전 이미지 태그로 다시 배포하면 되니까 비교적 간단해요. 문제는 DB 마이그레이션이 같이 나갔을 때인데, 이건 backward-compatible한 마이그레이션을 작성하는 수밖에 없습니다. 컬럼 삭제가 아니라 추가로 먼저 하고, 다음 릴리즈에서 이전 컬럼을 삭제하는 2단계 방식을 써요.

Feature Flag가 뭔가요?

배포와 릴리즈를 분리하는 기법이에요. 코드는 프로덕션에 배포하지만, 특정 기능은 플래그로 감싸서 꺼둔 채로 나갑니다.

JAVA
if (featureFlags.isEnabled("new-payment-flow")) {
    // 새로운 결제 플로우
} else {
    // 기존 결제 플로우
}

이렇게 하면 새 기능이 문제를 일으킬 때 재배포 없이 플래그만 꺼서 대응할 수 있어요. LaunchDarkly 같은 서비스를 쓰거나, 직접 구현하기도 합니다. 트렁크 기반 개발(Trunk-Based Development)에서 핵심적인 역할을 해요.

다만 플래그를 너무 남발하면 코드가 분기 투성이가 되니까, 정리 주기를 정해서 더 이상 필요 없는 플래그는 삭제해야 합니다.

GitOps란 뭔가요?

Git 저장소를 ** 인프라와 배포의 단일 진실 공급원(Single Source of Truth)**으로 쓰는 방식이에요. 배포하고 싶으면 서버에 SSH 접속하는 게 아니라, Git에 원하는 상태를 커밋합니다. 그러면 도구가 알아서 클러스터를 그 상태로 맞춰줘요.

ArgoCD 가 대표적인 GitOps 도구입니다. Kubernetes 클러스터에 설치하면, Git 저장소에 정의된 매니페스트와 실제 클러스터 상태를 계속 비교해요. 차이가 나면 자동으로 동기화하거나, 수동 승인을 기다립니다.

PLAINTEXT
Git 저장소 (desired state)
    ↕ 비교
Kubernetes 클러스터 (actual state)
    → 차이가 나면 → 동기화

GitOps의 장점은 명확해요. 배포 이력이 Git 히스토리에 남으니까 감사(audit)가 쉽고, 롤백은 git revert로 끝납니다. 누가 언제 뭘 바꿨는지 PR 기록으로 다 추적할 수 있어요.

모니터링과 알림은 어떻게 구성하나요?

배포 자동화만 해놓고 모니터링을 안 하면 반쪽짜리예요. 배포 후 에러율이 올라가는지, 응답 시간이 느려지는지 실시간으로 봐야 합니다.

보통 이런 스택을 많이 씁니다.

역할도구
메트릭 수집Prometheus
시각화Grafana
로그 수집ELK (Elasticsearch + Logstash + Kibana) 또는 Loki
알림Grafana Alerting, PagerDuty, Slack Webhook
APMDatadog, New Relic, Pinpoint

Canary 배포에서 카나리 인스턴스의 에러율이 임계값을 넘으면 자동으로 롤백하는 것도 모니터링 체계가 있어야 가능한 이야기예요.


파생 개념

CI/CD를 깊이 이해하려면 주변 개념들도 알아야 해요.

  • Docker — 컨테이너 기반 배포의 기본입니다. 이미지를 빌드하고 레지스트리에 푸시하는 것 자체가 CI/CD 파이프라인의 핵심 단계예요 (이미 작성)
  • Kubernetes — 컨테이너 오케스트레이션이에요. Rolling Update, Canary 같은 배포 전략이 여기서 구현됩니다 (이미 작성)
  • 테스트 코드 — CI가 의미 있으려면 자동화된 테스트가 있어야 합니다. 테스트 없는 CI는 그냥 자동 빌드일 뿐이에요
  • ** 모니터링** — 배포 이후의 관찰 가능성(Observability)이에요. 메트릭, 로그, 트레이싱 세 가지를 기본으로 봅니다
댓글 로딩 중...