needs는 “대기 지점”이며, 늘어날수록 CI는 느려질 수 있다

GitHub Actions의 needs는 job 사이에 의존(대기 지점)을 만드는 키워드입니다. 편리하지만, needs를 쓸 때마다 CI 실행 시간이 늘어날 가능성이 있다는 게 이 글의 출발점입니다. needs는 “안전하게” 쪽으로 기울어 작성되기 쉬워서, 실제로는 병렬로 돌려도 되는 job까지 직렬로 묶어버리는 경우가 자주 발생합니다.

이 글은 needs를 중심으로 한 job 의존관계의 설계 패턴과 안티패턴을 표로 정리한 레퍼런스입니다. 기존 YAML이 어느 패턴에 해당하는지 비교하며 읽어보세요.

패턴 치트시트

패턴효과사용 시점
다이아몬드형 분기병렬 실행으로 CI 시간 단축lint/test/typecheck가 서로 의존하지 않을 때
concurrency 그룹오래된 실행을 자동 취소같은 PR에 연속으로 push할 때
fail-fast: false하나의 실패가 나머지를 끌고 내려가지 않음matrix 실행에서 모든 조합의 결과를 보고 싶을 때
재사용 가능한 워크플로우(workflow_call)중복 정의 제거여러 저장소·워크플로우에서 같은 job 순서를 쓸 때
if: always() 알림 job실패해도 확실히 알림Slack 알림, 상태 리포트
부분적 matrix 대기일부 조합만 먼저 배포카나리아식 선행 실행

아래에서 각각을 실제 예제로 살펴봅니다.

패턴 1: 다이아몬드형 분기로 직렬을 깨기

가장 흔한 안티패턴은 본래 독립적인 job을 일렬로 늘어세우는 것입니다.

# 안티패턴: lint -> unit-test -> typecheck -> build 가 전부 직렬
jobs:
  lint:
    runs-on: ubuntu-latest
  unit-test:
    needs: lint
  typecheck:
    needs: unit-test
  build:
    needs: typecheck

unit-testtypecheck는 “코드가 존재한다” 이상의 전제를 공유하지 않습니다. lint의 완료만 기다리면 충분하다면, 둘 다 lint 바로 아래에 매달아 다이아몬드 형태로 만들 수 있습니다.

# 개선: lint 이후 unit-test와 typecheck가 병렬로 실행됨
jobs:
  lint:
    runs-on: ubuntu-latest
  unit-test:
    needs: lint
  typecheck:
    needs: lint
  build:
    needs: [unit-test, typecheck]   # 둘 다 완료를 기다림

buildneeds를 배열로 만들면 “둘 다 완료되면 실행”을 표현할 수 있습니다. job 수는 같아도 의존 그래프의 가로 폭(병렬도)이 그대로 CI 시간 단축으로 이어집니다. GitHub Actions Visualizer에 워크플로우를 붙여넣으면 이런 “불필요한 일직선”이 그래프 모양에서 바로 드러납니다.

패턴 2: concurrency로 “이전 실행”을 취소하기

needs 설계를 최적화해도, 같은 브랜치에 연속으로 push해서 오래된 실행이 큐에 남아있으면 대기 시간은 그대로입니다. concurrency는 이 낭비를 없애줍니다.

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

groupgithub.ref(브랜치명이나 PR 번호)를 포함시키면 같은 브랜치에 새 push가 오면 오래된 실행을 자동으로 취소합니다. PR에 3번 연속 push하면, 의미 있는 마지막 1번만 끝까지 실행되는 것이 이상적인 동작입니다.

:::message
group에 job 이름이나 워크플로우 이름을 포함하지 않으면 의도치 않게 다른 워크플로우끼리 서로 취소하는 일이 생길 수 있습니다. ${{ github.workflow }}-${{ github.ref }}처럼 워크플로우 단위로 구분하는 것이 안전합니다.
:::

패턴 3: fail-fast를 상황에 맞게 쓰기

strategy.matrix를 쓰면 하나의 job 정의에서 여러 실행(Node 18/20/22 × OS 3종 등)이 전개됩니다. 여기서 fail-fast 설정이 CI 동작을 크게 좌우합니다.

strategy:
  fail-fast: false   # 기본값은 true
  matrix:
    node: [18, 20, 22]
    os: [ubuntu-latest, windows-latest, macos-latest]
설정동작적합한 상황
fail-fast: true(기본)하나라도 실패하면 나머지를 즉시 취소빠른 피드백 우선. 실패를 알았다면 더 이상의 정보는 필요 없음
fail-fast: false하나가 실패해도 나머지 전부를 끝까지 실행”Node 20만 실패한 건가? OS도 관계있나?”를 한 번의 CI로 파악하고 싶을 때

신규 기능 릴리스 전처럼 모든 조합의 상태를 한 번에 알고 싶을 때false로 설정할 가치가 있습니다. 반대로 “일상적인 PR 체크는 무조건 빠르게”가 목표라면 기본값 true를 유지하며 빠른 실패 감지를 우선하세요.

패턴 4: matrix job을 needs로 기다릴 때의 함정

buildtest(matrix로 전개됨)에 의존할 때, needs: test라고만 써도 matrix의 모든 조합의 완료를 기다려줍니다. 여기까지는 직관적이지만 주의할 점이 두 가지 있습니다.

  • 조합 하나만 실패해도 needs 쪽도 스킵됩니다(fail-fast: true를 함께 쓰면 특히, 중간 결과만으로 후속이 멈출 수 있음)
  • 일부 조합만 먼저 진행하고 싶을 때(예: Linux만 먼저 배포하고 Windows/macOS 결과는 나중에 확인) needs만으로는 표현할 수 없고, job 자체를 분할해야 합니다
# Linux 선행 배포를 위해 job을 나누는 예
jobs:
  test-linux:
    runs-on: ubuntu-latest
  test-other-os:
    strategy:
      matrix:
        os: [windows-latest, macos-latest]
    runs-on: ${{ matrix.os }}
  deploy:
    needs: test-linux   # Windows/macOS 결과를 기다리지 않음

“전부 기다릴지, 일부만 기다릴지”는 needs의 작성법이 아니라 job을 어떻게 나누는지로 결정된다는 것이 이 패턴의 핵심입니다.

패턴 5: 재사용 가능한 워크플로우로 의존 그래프째 공유하기

여러 워크플로우(또는 여러 저장소)에서 같은 job 순서를 재사용한다면, workflow_call로 호출 대상을 한 곳에 모을 수 있습니다.

# .github/workflows/ci.yml (호출하는 쪽)
jobs:
  call-shared-ci:
    uses: ./.github/workflows/shared-test-suite.yml
    with:
      node-version: '20'

호출되는 쪽(shared-test-suite.yml)의 needs 구조가 통째로 재사용되므로, “lint와 test의 의존관계”를 저장소 간에 복사-붙여넣기 하지 않아도 됩니다. 의존 그래프 설계를 패턴화했다면, 다음은 이 방법으로 설계 자체를 부품화하는 마무리 단계입니다.

알림 job은 needs 바깥에서 “반드시 실행”되도록 설계하기

실패 감지 알림 job은 needs의 의존관계 특성상 의존 대상이 실패하면 자동으로 스킵됩니다. 알림만은 예외적으로 실행하고 싶으므로 if: always()를 명시합니다.

notify:
  needs: [lint, test, build]
  if: always()   # 의존 job의 성공/실패와 무관하게 항상 실행
  runs-on: ubuntu-latest
  steps:
    - run: echo "Result: ${{ needs.build.result }}"

needs.<job>.result로 각 job의 결과(success / failure / cancelled / skipped)를 참조할 수 있으므로, “무엇이 실패했는지” 포함한 알림 문구를 만들 수 있습니다.

의존 그래프 점검 절차

  1. 워크플로우 YAML을 GitHub Actions Visualizer에 붙여넣어 의존 그래프를 시각화한다
  2. 일직선으로 이어진 부분을 찾아 정말로 순서가 필요한지 확인한다(패턴 1)
  3. 연속 push로 인한 중복 실행이 신경 쓰인다면 concurrency를 추가한다(패턴 2)
  4. matrix를 쓰는 job은 fail-fast 값이 목적에 맞는지 확인한다(패턴 3)
  5. 여러 워크플로우에서 같은 의존 구조가 반복된다면 재사용 가능한 워크플로우로의 분리를 검토한다(패턴 5)

CI 실행 시간은 스텝 내용을 줄이는 것보다 의존 그래프의 모양을 고치는 쪽이 효과가 큰 경우가 적지 않습니다. 워크플로우를 한 번 그림으로 그려보고 불필요한 직렬화가 없는지 확인해보세요.

자주 묻는 질문

needs를 늘리면 CI가 반드시 느려지나요?

needs 자체가 직접 느리게 만드는 것이 아니라, **“기다릴 필요 없는 의존을 써버리는 것”**이 문제입니다. 정말로 전제가 되는 산출물(빌드된 아티팩트 등)을 기다리는 needs는 필요한 의존이므로 없애면 안 됩니다. 점검해야 할 것은 “lint의 완료만 기다리면 충분한데 이전 job의 완료까지 기다리는” 것 같은 불필요한 연쇄입니다.

concurrency의 cancel-in-progress는 항상 true로 해야 하나요?

PR에 연속으로 push되는 CI에서는 기본적으로 유효합니다. 다만 main 브랜치 병합 후 배포 워크플로우에서는 취소되면 배포가 어중간한 상태로 멈출 위험이 있으므로, cancel-in-progress: false로 하거나 애초에 배포 워크플로우에는 concurrency를 붙이지 않는 것도 고려하세요.

fail-fast를 false로 하면 CI 시간이 늘어나나요?

늘어날 수 있습니다. fail-fast: true(기본값)는 첫 실패에서 나머지 job을 중단하므로 평균 CI 시간이 짧아지는 경향이 있습니다. false는 모든 조합의 결과를 알고 싶은 상황(릴리스 전 전체 환경 확인 등)에 한정해서 쓰는 것이 실무적인 균형입니다.

의존관계 점검에 사용한 워크플로우 YAML은 외부로 전송되나요?

아니요. GitHub Actions Visualizer는 모든 처리를 브라우저 안에서 하므로, 사내 워크플로우 정의를 붙여넣어도 서버로 전송되지 않습니다.