GitHub Actions Matrix로 여러 플랫폼과 버전을 병렬 테스트하는 방법

지난해 한 오픈 소스 프로젝트에서 CI 설정 파일이 800줄을 넘었다며 도움을 요청해 왔습니다. 파일을 열어 보니 반복되는 job 정의가 빼곡했습니다. Node 16을 Ubuntu, Windows, macOS에서 각각 실행하고, Node 18과 Node 20에서도 같은 내용을 다시 반복하고 있었습니다. 테스트 명령 하나를 바꾸려면 12곳을 고쳐야 했고 새 버전을 추가할 때마다 십여 분 동안 복사와 붙여넣기를 해야 했습니다.
그때 여러 버전과 플랫폼을 테스트하면서도 아직 설정을 일일이 작성하는 프로젝트가 많다는 사실을 새삼 깨달았습니다.
GitHub Actions의 Matrix 기능은 이런 반복 설정을 자동으로 펼쳐 줍니다. 운영체제와 런타임 버전을 정의하면 가능한 조합을 모두 실행합니다. 개념은 단순하지만 실제로 적용하면 조합 수가 폭증해 비용이 늘거나, 작업 하나의 실패로 전체 실행이 중단되거나, 특정 조합을 어떻게 제외해야 할지 막히는 등의 문제가 생길 수 있습니다. 저도 이런 문제를 모두 겪었습니다.
이 글은 가장 기본적인 Matrix 문법부터 exclude/include를 이용한 정밀 제어, fail-fast 전략 선택, max-parallel 동시 실행 제한, 동적 Matrix 생성까지 차례로 설명합니다. 마지막에는 개인 프로젝트부터 엔터프라이즈 애플리케이션까지 적용할 수 있는 프로덕션 수준의 workflow 템플릿 5개를 제공합니다.
2. Matrix 핵심 개념: 여러 작업을 한 번에 펼치기
Matrix의 핵심 원리는 간단합니다. 여러 차원을 정의하면 GitHub Actions가 데카르트 곱으로 모든 조합을 자동 생성합니다.
예를 들어 프로젝트를 Ubuntu, Windows, macOS에서 테스트하면서 Node.js 18, 20, 22도 모두 지원해야 한다고 가정해 보겠습니다. 기존 방식이라면 job 9개를 직접 작성하고 각 job마다 실행 환경, 설치 단계, 테스트 명령을 반복해야 합니다. Matrix를 사용하면 다음 설정만 있으면 됩니다.
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm test
이 10줄의 설정을 바탕으로 GitHub Actions는 3 x 3 = 9개의 병렬 작업을 자동 생성합니다. 각 작업은 서로 다른 matrix.os와 matrix.node 값을 받아 모든 조합을 실행합니다.
앞서 언급한 800줄짜리 설정 파일도 Matrix로 리팩터링한 뒤 약 120줄로 줄었습니다. 코드가 60% 이상 줄었고 유지 관리 비용도 낮아졌습니다. 새 버전을 추가할 때는 배열에 숫자 하나만 넣으면 되므로 더 이상 job 정의를 잔뜩 복사할 필요가 없습니다.
Matrix로 할 수 있는 일:
- 여러 플랫폼과 버전의 테스트 조합을 한 번에 생성
- 모든 설정을 자동으로 펼쳐 반복 코드 방지
- exclude로 문제가 확인된 조합 제외
- include로 특수 설정이 필요한 사례 추가
- 동시 실행 수를 제어해 속도와 비용의 균형 조정
Matrix가 해결하지 못하는 문제:
- 테스트 자체의 품질이 낮다면 Matrix도 해결책이 될 수 없습니다.
- 조합이 너무 많아 비용이 급증하는 문제는 차원 수를 직접 관리해야 합니다.
- 의존성 설치가 느린 문제는 캐시 전략을 함께 적용해야 합니다.
Matrix 자체는 이해하기 어렵지 않습니다. 실제 엔지니어링 문제에 맞게 사용하는 방법이 더 중요합니다. 이제 기본 문법부터 차례로 살펴보겠습니다.
3. 기본 문법: os x version 조합 원리
Matrix의 조합 규칙은 수학의 데카르트 곱과 같습니다. 정의한 각 차원의 값이 다른 차원의 값과 모두 조합됩니다.
차원 하나에 값 N개 -> 작업 N개
차원 두 개에 각각 값 M개와 N개 -> 작업 M x N개
차원 세 개에 각각 값 A개, B개, C개 -> 작업 A x B x C개
구체적인 예를 들어 보겠습니다. Python 프로젝트를 Linux와 Windows에서 테스트하고 Python 3.9, 3.10, 3.11, 3.12 네 버전 및 PostgreSQL과 MySQL 두 데이터베이스까지 함께 테스트해야 한다고 가정해 보겠습니다.
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ['3.9', '3.10', '3.11', '3.12']
database: [postgresql, mysql]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Setup ${{ matrix.database }}
run: |
# 해당 데이터베이스 서비스 시작
if [ "${{ matrix.database }}" = "postgresql" ]; then
docker run -d -p 5432:5432 postgres
else
docker run -d -p 3306:3306 mysql
fi
shell: bash
- run: pip install -r requirements.txt
- run: pytest
이 설정은 2 x 4 x 2 = 16개의 작업을 생성합니다. 각 작업은 서로 영향을 주지 않는 독립된 실행 환경입니다.
Matrix 변수에 접근하는 방법:
${{ matrix.os }}— 현재 작업의 운영체제 가져오기${{ matrix.python-version }}— 현재 작업의 Python 버전 가져오기${{ matrix.database }}— 현재 작업의 데이터베이스 유형 가져오기
이 변수는 runs-on, steps, env 등에서 사용할 수 있어 각 작업의 동작을 동적으로 조정할 수 있습니다.
자주 하는 실수: Matrix가 의존성 설치 문제까지 자동으로 처리한다고 생각하는 경우가 많습니다. 실제로는 작업마다 독립된 환경을 사용하므로 의존성 설치가 반복됩니다. 의존성 설치에 2분이 걸린다면 작업 16개에서는 총 32분의 설치 시간이 필요합니다(직렬 실행 기준).
해결 방법은 두 가지입니다.
- 캐시 사용 —
pip또는npm의존성 디렉터리를 캐시해 반복 다운로드 건너뛰기 - 조합 수 줄이기 — exclude를 사용해 불필요한 테스트 조합 제외하기
캐시 전략은 이전에 작성한 [GitHub Actions 캐시 전략: CI/CD 파이프라인을 5배 빠르게 만드는 방법]에서 자세히 다뤘으므로 여기서는 생략하겠습니다. 이제 exclude/include로 테스트 조합을 정밀하게 제어하는 방법을 살펴보겠습니다.
4. exclude/include: 테스트 조합 정밀 제어
Matrix는 기본적으로 모든 차원을 전부 조합하지만, 실제 프로젝트에서는 특정 조합을 테스트할 필요가 없거나 일부 조합에 별도 처리가 필요한 경우가 많습니다.
4.1 exclude: 불필요한 조합 제외하기
이전에 관리하던 Python 프로젝트에서는 Windows + Python 3.9 조합만 계속 실패했습니다. 특정 의존성 라이브러리의 3.9 버전이 Windows와 호환되지 않았기 때문입니다. 프로젝트는 주로 Linux 서버에 배포됐고 Windows 지원은 부가적인 요구 사항이어서 이 특정 버그를 고치는 데 시간을 쓰기 어려웠습니다.
이럴 때 exclude를 사용할 수 있습니다.
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', '3.10', '3.11', '3.12']
exclude:
- os: windows-latest
python-version: '3.9'
- os: macos-latest
python-version: '3.9'
이 설정은 Windows와 macOS의 Python 3.9 테스트를 제외합니다. 원래 3 x 4 = 12개였던 작업이 2개 줄어 10개가 됩니다.
exclude의 대표적인 사용 사례:
- 알려진 호환성 문제 — 특정 버전을 특정 운영체제에서 실행할 수 없는 경우
- 리소스 제한 — 셀프 호스팅 runner가 부족해 조합 수를 줄여야 하는 경우
- 경계 사례 — 사용자에게 거의 필요하지 않아 CI 시간을 투입할 가치가 낮은 조합
4.2 include: 특수 설정 추가하기
include는 반대로 테스트 조합을 추가하거나 특정 조합에 변수를 더할 때 사용합니다.
예를 들어 Python 3.12 테스트에서만 커버리지 보고서를 활성화하고 다른 버전에서는 비활성화하려면 다음과 같이 설정합니다.
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
include:
- python-version: '3.12'
coverage: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -r requirements.txt
- name: Run tests
run: |
if [ "${{ matrix.coverage }}" = "true" ]; then
pytest --cov=src --cov-report=xml
else
pytest
fi
shell: bash
여기서 include는 두 가지 일을 합니다.
- 새 조합 추가 — Python 3.12 테스트
- 조합에 별도 변수 추가 —
coverage: true
include의 대표적인 사용 사례:
- 실험 버전 테스트 — 예를 들어 Python 3.13 프리뷰 버전을 특정 운영체제에서만 테스트
- 특수 설정 — 일부 조합에만 별도의 환경 변수나 매개변수가 필요한 경우
- 경계 사례 보완 — 자주 쓰이지 않는 조합을 전체 조합에 넣지 않고 따로 추가
4.3 exclude와 include 함께 사용하기
실제 프로젝트에서는 exclude와 include를 동시에 사용해야 할 때가 많습니다. 예를 들어 Python 3.9 조합은 모두 제외하되 Ubuntu + Python 3.9 최소 테스트만 따로 추가할 수 있습니다.
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ['3.9', '3.10', '3.11', '3.12']
exclude:
- python-version: '3.9'
include:
- os: ubuntu-latest
python-version: '3.9'
minimal: true
적용 순서는 모든 조합 생성 -> exclude 적용 -> include 적용입니다. 최종적으로 Ubuntu에서는 버전 4개를 모두 실행하고 Windows에서는 3.9를 제외한 버전 3개를 실행합니다.
5. fail-fast 전략: 빠른 실패와 전체 디버깅
Matrix 작업은 기본적으로 작업 하나가 실패하면 실행 중인 다른 작업을 취소합니다. 이 동작을 fail-fast라고 하며 기본값은 활성화 상태입니다.
strategy:
fail-fast: true # 기본값이므로 생략 가능
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
5.1 fail-fast: true를 사용할 때(기본값)
PR 테스트 — 개발자가 PR을 올렸을 때 테스트에 문제가 있는지 빠르게 확인하려는 경우입니다. 작업 하나가 실패하면 같은 코드 문제로 다른 작업도 실패할 가능성이 크므로 계속 실행해 시간을 낭비할 필요가 적습니다.
비용에 민감한 환경 — GitHub Actions 무료 사용량이나 셀프 호스팅 runner의 리소스가 제한된 경우 빠른 실패를 사용하면 비용을 상당히 줄일 수 있습니다.
저는 보통 PR 테스트에는 fail-fast: true를, main 브랜치의 전체 테스트에는 fail-fast: false를 사용합니다.
5.2 fail-fast: false를 사용할 때
디버깅 단계 — Matrix 작업이 자주 실패하며 어떤 조합에서 왜 실패하는지 모두 확인하려는 경우입니다. fail-fast: true이면 첫 번째로 실패한 작업만 확인할 수 있고 나머지는 취소됩니다.
호환성 테스트 — 여러 버전과 플랫폼의 호환성을 테스트하며 각 조합의 결과를 모두 확인하려는 경우입니다. 특정 버전에 문제가 있어도 다른 버전의 결과를 수집할 수 있습니다.
전체 보고서가 필요한 경우 — CI 종료 후 모든 조합의 성공 및 실패 상태를 포함한 전체 테스트 보고서를 만들어야 하는 경우입니다.
strategy:
fail-fast: false # 모든 작업을 끝까지 실행
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
5.3 실제 사례
지난해 한 프로젝트의 CI 문제를 조사한 적이 있습니다. 테스트가 Ubuntu + Node 18에서 계속 실패했지만 다른 조합은 모두 통과했습니다. 기본 fail-fast가 활성화되어 있어서 실행할 때마다 Ubuntu + Node 18의 실패만 확인한 뒤 다른 작업이 취소됐습니다. Windows + Node 18에도 같은 문제가 있는지 확인하려고 fail-fast: false로 바꿨더니 Windows에서는 문제가 없고 Ubuntu에서만 발생한다는 사실을 알 수 있었습니다. 결국 파일 경로의 대소문자 호환성 문제로 원인을 좁혔습니다.
제가 권하는 방식은 개발 및 디버깅 단계에서는 fail-fast: false로 모든 문제를 확인하고, 안정적으로 운영하는 단계에서는 fail-fast: true로 비용과 시간을 절약하는 것입니다.
6. max-parallel: 동시 실행 제어와 비용 최적화
Matrix 작업은 기본적으로 병렬 실행되며 GitHub는 가능한 한 많은 작업을 동시에 시작합니다. 공개 저장소에서 GitHub 호스팅 runner의 동시 실행 제한은 20개이고, 비공개 저장소에서 무료 계정의 동시 실행 제한은 2개입니다.
그러나 동시 실행 수를 직접 제어해야 하는 경우도 있습니다. 이때 max-parallel을 사용합니다.
6.1 동시 실행을 제한해야 하는 경우
셀프 호스팅 runner의 리소스가 부족한 경우 — runner 서버가 4코어, 8GB RAM인데 작업 8개를 동시에 실행하면 시스템이 버티기 어렵습니다.
외부 서비스의 요청 제한 — 테스트에서 타사 API를 호출하며 상대 서비스에 QPS 제한이 있는 경우, 동시 요청이 너무 많으면 차단될 수 있습니다.
데이터베이스 연결 풀 제한 — 테스트가 데이터베이스에 연결해야 하는데 연결 풀에 연결이 10개뿐이라면 작업 수가 많을 때 풀이 고갈될 수 있습니다.
strategy:
max-parallel: 4 # 동시에 최대 4개 작업 실행
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
이 설정은 작업 6개를 생성하지만 동시에 실행되는 작업은 최대 4개입니다. 작업 하나가 끝나면 다음 작업이 시작됩니다.
6.2 비용 계산 예시
CI 실행 한 번에 운영체제 3개 x Node 버전 4개 = 작업 12개를 테스트해야 하고, 각 작업의 평균 실행 시간이 10분이라고 가정해 보겠습니다.
동시 실행 제한 없음(runner가 충분하다고 가정):
- 작업 12개 동시 실행
- 전체 소요 시간 약 10분
- 총 계산 시간 = 12 x 10 = 120분
max-parallel: 4로 제한:
- 작업 12개를 3번에 나눠 실행
- 전체 소요 시간 약 30분
- 총 계산 시간 = 12 x 10 = 120분(변함없음)
보이는 것처럼 max-parallel은 총 계산 시간을 줄이지 않고 전체 소요 시간만 늘립니다. 그런데도 사용하는 이유는 무엇일까요?
바로 최대 동시 리소스 비용과 리소스 제한 때문입니다.
GitHub Actions는 분 단위로 과금하지만, 셀프 호스팅 runner를 사용하거나 클라우드 공급자가 최대 사용량을 기준으로 과금한다면 동시 실행 제어가 중요합니다. 작업 12개를 동시에 실행하면 데이터베이스 연결 12개가 필요하지만, 나눠서 실행하면 4개만 있으면 됩니다.
제 경험을 바탕으로 한 권장 방식:
- 공개 저장소와 GitHub 호스팅 runner:
max-parallel을 따로 설정하지 않고 기본 스케줄링 사용 - 비공개 저장소와 무료 사용량:
max-parallel: 2로 제한해 사용량 초과 방지 - 셀프 호스팅 runner: 서버 사양에 맞춰
max-parallel제한. 4코어라면 동시 실행 2~4개 권장
7. 동적 matrix: fromJSON 고급 활용법
지금까지 살펴본 Matrix는 모두 정적 설정입니다. 테스트할 버전을 YAML에 직접 작성했습니다. 하지만 코드 변경 내용에 따라 테스트 조합을 동적으로 생성해야 하는 경우도 있습니다.
예를 들어 여러 서비스가 있고 각 서비스마다 자체 테스트 설정을 가진 monorepo를 생각해 보겠습니다. 모든 서비스를 매번 테스트하는 대신 이번 커밋에서 변경된 서비스만 테스트하고 싶을 수 있습니다.
7.1 두 단계 workflow로 동적 matrix 구현하기
GitHub Actions에는 동적 matrix 전용 문법이 없지만, job 하나에서 matrix 설정을 생성한 뒤 다른 job으로 전달할 수 있습니다. 핵심은 fromJSON() 함수입니다.
jobs:
# 1단계: 변경된 서비스를 감지하고 matrix 설정 생성
detect:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # 이전 커밋도 가져와야 함
- name: Detect changed services
id: set-matrix
run: |
# 이번 커밋에서 변경된 파일 가져오기
CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)
# 변경된 서비스 확인
SERVICES="[]"
if echo "$CHANGED_FILES" | grep -q "services/auth/"; then
SERVICES=$(echo $SERVICES | jq '. + ["auth"]')
fi
if echo "$CHANGED_FILES" | grep -q "services/api/"; then
SERVICES=$(echo $SERVICES | jq '. + ["api"]')
fi
if echo "$CHANGED_FILES" | grep -q "services/web/"; then
SERVICES=$(echo $SERVICES | jq '. + ["web"]')
fi
# 변경된 서비스가 없으면 모든 서비스를 기본값으로 테스트
if [ "$SERVICES" = "[]" ]; then
SERVICES='["auth", "api", "web"]'
fi
echo "matrix={\"service\":$(echo $SERVICES)}" >> $GITHUB_OUTPUT
# 2단계: 동적으로 생성한 matrix 사용
test:
needs: detect
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJSON(needs.detect.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- name: Test ${{ matrix.service }}
run: |
cd services/${{ matrix.service }}
npm install
npm test
이 workflow의 동작 방식은 다음과 같습니다.
detectjob이 이번 커밋에서 변경된 디렉터리를 확인합니다.- 변경된 디렉터리를 바탕으로 JSON 형식의 matrix 설정을 동적으로 생성합니다.
testjob이fromJSON()으로 설정을 해석하고 해당 작업을 생성합니다.
7.2 동적 matrix의 대표적인 사용 사례
Monorepo — 변경된 서비스만 테스트해 CI 시간 절약
필요한 항목만 배포 — Dockerfile 변경을 감지해 업데이트된 이미지만 빌드하고 배포
Matrix 테스트 최적화 — 파일 유형에 따라 테스트 조합 결정. 예를 들어 package.json이 변경됐을 때만 여러 버전의 전체 테스트 실행
제가 겪은 문제:
fromJSON()은strategy.matrix의 값에만 사용할 수 있고 다른 곳에는 사용할 수 없습니다.- 생성한 JSON은
{"service": ["auth", "api"]}와 같은 유효한 matrix 형식이어야 합니다. - 생성된 matrix가 비어 있으면 workflow에서 바로 오류가 발생하므로 기본값을 처리해야 합니다.
8. 실무 템플릿 모음: 프로덕션 수준의 workflow 예시 5개
이제 개인 프로젝트부터 엔터프라이즈 애플리케이션까지 여러 상황에 바로 복사해 사용할 수 있는 workflow 템플릿 5개를 살펴보겠습니다.
8.1 템플릿 1: Node.js 여러 버전 테스트(기본)
적합한 상황: 여러 Node 버전을 지원해야 하는 Node.js 라이브러리 또는 애플리케이션
name: Node.js CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [18, 20, 22, 23]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
- run: npm test
- name: Upload coverage
if: matrix.node-version == 22
uses: codecov/codecov-action@v4
핵심 사항:
npm install대신npm ci를 사용해 의존성 버전을 고정합니다.- 중복 업로드를 피하려고 Node 22에서만 커버리지 보고서를 업로드합니다.
cache: 'npm'으로 npm 캐시를 활성화해 의존성 설치를 빠르게 합니다.
8.2 템플릿 2: Python 여러 플랫폼과 버전 테스트(중급)
적합한 상황: 여러 플랫폼과 버전에서 테스트해야 하는 Python 프로젝트
name: Python CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.10', '3.11', '3.12']
exclude:
- os: windows-latest
python-version: '3.10' # 알려진 호환성 문제
steps:
- uses: actions/checkout@v4
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: pytest -v
- name: Lint check
run: |
pip install ruff
ruff check .
핵심 사항:
exclude로 문제가 확인된 조합을 제외합니다.cache: 'pip'으로 pip 의존성 설치를 빠르게 합니다.- 코드 검사 도구 ruff를 함께 사용합니다.
8.3 템플릿 3: exclude/include 정밀 제어(고급)
적합한 상황: 특정 조합을 제외하고 특수 테스트를 추가하는 등 테스트 조합을 세밀하게 제어해야 하는 경우
name: Advanced Matrix
on:
push:
branches: [main]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ['3.10', '3.11', '3.12']
exclude:
# Windows + Python 3.10 제외(알려진 문제)
- os: windows-latest
python-version: '3.10'
include:
# 실험 테스트 추가: Ubuntu + Python 3.13 프리뷰 버전
- os: ubuntu-latest
python-version: '3.13-dev'
experimental: true
# Python 3.12에 커버리지 보고서 추가
- python-version: '3.12'
coverage: true
continue-on-error: ${{ matrix.experimental == true }}
steps:
- uses: actions/checkout@v4
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- run: pip install -r requirements.txt
- name: Run tests
run: |
if [ "${{ matrix.coverage }}" = "true" ]; then
pytest --cov=src --cov-report=xml
else
pytest
fi
shell: bash
핵심 사항:
continue-on-error를 사용하면 실험 테스트가 실패해도 전체 상태에 영향을 주지 않습니다.include로 새 조합을 추가하는 동시에 변수를 보완할 수 있습니다.shell: bash를 사용해 Windows와 Linux에서 명령이 같은 방식으로 실행되도록 합니다.
8.4 템플릿 4: 동적 matrix + caching(고급)
적합한 상황: 파일 변경 내용에 따라 테스트 조합을 동적으로 생성하는 Monorepo
name: Dynamic Matrix CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
detect:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Detect changed packages
id: set-matrix
run: |
CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)
PACKAGES="[]"
for dir in packages/*/; do
pkg=$(basename $dir)
if echo "$CHANGED_FILES" | grep -q "^packages/$pkg/"; then
PACKAGES=$(echo $PACKAGES | jq ". + [\"$pkg\"]")
fi
done
# 변경 사항이 없으면 모든 패키지 테스트
if [ "$PACKAGES" = "[]" ]; then
PACKAGES='["core", "utils", "cli"]'
fi
echo "matrix={\"package\":$(echo $PACKAGES)}" >> $GITHUB_OUTPUT
test:
needs: detect
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJSON(needs.detect.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build --if-present
- name: Test ${{ matrix.package }}
run: |
cd packages/${{ matrix.package }}
npm test
핵심 사항:
fetch-depth: 2로 이전 커밋까지 가져와 비교합니다.jq명령으로 JSON 배열을 처리합니다.- 변경 사항이 없을 때 기본값을 제공해 빈 matrix 오류를 방지합니다.
8.5 템플릿 5: 셀프 호스팅 runner + max-parallel(엔터프라이즈)
적합한 상황: 셀프 호스팅 runner를 사용하며 동시 실행 수와 리소스를 엄격하게 제어해야 하는 경우
name: Enterprise CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: [self-hosted, linux, x64]
strategy:
fail-fast: true
max-parallel: 4
matrix:
java-version: [11, 17, 21]
database: [postgresql, mysql]
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
mysql:
image: mysql:8
env:
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: >-
--health-cmd "mysqladmin ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Java ${{ matrix.java-version }}
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java-version }}
distribution: 'temurin'
cache: 'maven'
- name: Run tests with ${{ matrix.database }}
env:
DB_TYPE: ${{ matrix.database }}
DB_HOST: localhost
DB_PORT: ${{ matrix.database == 'postgresql' && 5432 || 3306 }}
run: mvn test -Dspring.profiles.active=${{ matrix.database }}
- name: Archive test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.java-version }}-${{ matrix.database }}
path: target/surefire-reports
핵심 사항:
runs-on: [self-hosted, linux, x64]로 셀프 호스팅 runner 레이블을 지정합니다.max-parallel: 4로 동시 실행 수를 제한해 runner 서버를 보호합니다.services로 테스트 데이터베이스 컨테이너를 시작합니다.if: always()를 사용하면 테스트가 실패해도 결과를 항상 업로드합니다.
9. 자주 마주치는 함정과 권장 방식
Matrix를 오래 사용하면서 여러 문제를 겪었습니다. 그중 가장 흔한 사례를 정리해 보겠습니다.
9.1 함정 1: 조합 수 폭증
제가 본 가장 과한 설정은 운영체제 4개 x 런타임 버전 5개 x 데이터베이스 3개 x 캐시 방식 2개 = 작업 120개였습니다. CI 한 번이 끝나는 데 45분이나 걸렸고 비용도 크게 늘었습니다.
해결 방법:
- 전체 Matrix는 main 브랜치에서만 실행하고 PR에서는 핵심 조합만 실행합니다.
exclude로 경계 사례를 제외합니다.- 각 차원이 정말 필요한지 평가합니다. 운영체제 4개를 모두 테스트할 필요가 있는지부터 확인해야 합니다.
# PR에서는 핵심 조합만 테스트
on:
pull_request:
branches: [main]
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest] # PR에서는 Ubuntu만 테스트
node: [20] # PR에서는 Node 20만 테스트
9.2 함정 2: fail-fast가 디버깅을 방해함
기본값인 fail-fast: true는 디버깅할 때 불편할 수 있습니다. 작업 하나가 실패하면 나머지가 모두 취소되어 전체 실패 보고서를 볼 수 없기 때문입니다.
해결 방법: 디버깅할 때는 직접 fail-fast: false로 바꾸고, 디버깅이 끝나면 원래 설정으로 되돌립니다.
또는 환경 변수를 사용해 제어할 수 있습니다.
strategy:
fail-fast: ${{ github.event_name == 'pull_request' }}
9.3 함정 3: caching 누락
Matrix는 같은 job을 여러 번 반복합니다. 매번 의존성을 새로 설치하면 시간이 많이 듭니다. 조합 12개를 테스트할 때 각 의존성 설치에 2분이 걸려 설치에만 총 24분을 쓴 적도 있습니다.
해결 방법: GitHub Actions 캐시나 전용 캐시 action을 사용합니다.
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm' # 핵심: npm 캐시 활성화
9.4 권장 방식 요약
권장 방식 1: PR에는 작은 Matrix, main에는 전체 Matrix 사용
jobs:
test:
strategy:
matrix:
# PR에서는 핵심 조합만 테스트
${{ github.event_name == 'pull_request' && fromJSON('{"os":["ubuntu-latest"],"node":[20]}') || fromJSON('{"os":["ubuntu-latest","windows-latest","macos-latest"],"node":[18,20,22]}') }}
권장 방식 2: exclude로 알려진 문제 조합 제외
특정 조합에서 호환성 문제가 생기면 우선 exclude로 건너뛰고 TODO를 남긴 뒤 나중에 수정합니다.
권장 방식 3: caching으로 설치 시간 단축
각 job에서 가장 많은 시간을 차지하는 단계 중 하나가 의존성 설치입니다. 캐시를 잘 활용하면 분 단위의 시간을 초 단위로 줄일 수 있습니다.
권장 방식 4: 조합에 의미 있는 name 지정
기본 작업 이름은 test (ubuntu-latest, 20) 같은 형식입니다. name을 사용해 직접 지정할 수 있습니다.
jobs:
test:
name: Test (${{ matrix.os }}, Node ${{ matrix.node }})
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
이렇게 하면 GitHub Actions 화면에서 각 작업을 더 쉽게 구분할 수 있습니다.
10. 결론
GitHub Actions Matrix는 여러 플랫폼과 버전을 테스트하는 데 유용한 기능입니다. 핵심은 차원 정의, 조합 제어, 동시 실행 관리, 동적 생성입니다.
아직도 많은 프로젝트가 반복되는 CI 설정을 직접 작성합니다. 명령 하나를 바꾸려고 십여 곳을 수정하는 경우도 있습니다. Matrix를 사용하면 수백 줄의 설정을 수십 줄로 줄여 유지 관리 비용을 크게 낮출 수 있습니다.
핵심 내용 정리:
- 기본 문법:
matrix.os와matrix.node의 데카르트 곱으로 작업 생성 - 정밀 제어:
exclude로 불필요한 조합을 제외하고include로 특수 설정 추가 - 전략 선택: 상황에 맞춰
fail-fast선택. 디버깅에는 false, 프로덕션에는 true 사용 - 동시 실행 제한:
max-parallel로 셀프 호스팅 runner를 보호하고 비용 제어 - 동적 생성:
fromJSON()으로 필요한 테스트만 실행해 CI 리소스 절약
글에 포함된 템플릿 5개는 가장 간단한 Node.js 여러 버전 테스트부터 엔터프라이즈 셀프 호스팅 runner 설정까지 바로 프로젝트에 복사해 사용할 수 있습니다.
Matrix를 처음 사용한다면 템플릿 1부터 실행해 본 뒤 exclude와 include를 추가하고, 마지막으로 동적 생성에 도전하는 편이 좋습니다. 처음부터 복잡하게 만들지 말고 한 단계씩 진행해 보세요.
궁금한 점은 댓글로 남기거나 GitHub Actions 공식 문서에서 더 많은 내용을 확인할 수 있습니다. Matrix 활용 경험이 있다면 함께 공유해 주세요.
GitHub Actions Matrix로 여러 플랫폼과 버전 테스트 설정하기
Matrix 전략을 처음부터 설정해 여러 플랫폼과 버전에서 테스트를 자동화합니다.
⏱️ Estimated time: 30 min
- 1
Step 1: Matrix 차원 정의
workflow의 job에 strategy.matrix 설정을 추가합니다:
```yaml
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
```
그러면 2 x 3 = 6개의 병렬 작업이 생성됩니다. - 2
Step 2: Matrix 변수 사용
runs-on과 steps에서 matrix 변수를 참조합니다:
```yaml
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
```
각 작업은 해당 os와 node 값을 자동으로 가져옵니다. - 3
Step 3: 특정 조합 제외하기(선택 사항)
exclude로 문제가 확인된 조합을 제외합니다:
```yaml
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
exclude:
- os: windows-latest
node: 18
```
Windows + Node 18 조합을 제외하므로 최종적으로 작업 5개가 생성됩니다. - 4
Step 4: 실패 전략 설정
상황에 맞는 fail-fast 전략을 선택합니다:
- PR 테스트: fail-fast: true(빠르게 실패하고 비용 절감)
- 디버깅 단계: fail-fast: false(모든 실패 확인)
- main 브랜치: fail-fast: false(전체 보고서 확보)
```yaml
strategy:
fail-fast: false
matrix:
# ...
``` - 5
Step 5: 동시 실행 수 제한하기(선택 사항)
셀프 호스팅 runner를 사용하거나 리소스가 부족하다면 동시 실행 수를 제한합니다:
```yaml
strategy:
max-parallel: 4
matrix:
# ...
```
동시에 최대 4개 작업만 실행해 runner 과부하를 방지합니다.
FAQ
Matrix 조합 수에 제한이 있나요?
fail-fast의 기본값은 무엇인가요?
exclude와 include는 어떤 순서로 적용되나요?
동적 matrix의 fromJSON()은 어디에서 사용할 수 있나요?
max-parallel을 사용하면 전체 계산 시간이 줄어드나요?
Matrix 작업끼리 캐시를 공유할 수 있나요?
Matrix 작업에 사용자 지정 이름을 붙이려면 어떻게 하나요?
• name: Test (${{ matrix.os }}, Node ${{ matrix.node }})
그러면 GitHub Actions 화면에 'Test (ubuntu-latest, Node 20)'처럼 알아보기 쉬운 이름이 표시되어 각 작업을 구분하기 편합니다.
6분 읽기 · 게시일: 2026년 4월 28일 · 수정일: 2026년 9월 8일
GitHub Actions 완전 가이드
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
GitHub Actions Matrix 빌드: 여러 버전 병렬 테스트 실전
GitHub Actions Matrix 빌드 실전 가이드입니다. 기본 문법, exclude/include 필터링, fail-fast 전략 최적화, max-parallel 리소스 제어를 다루고 여러 버전을 병렬 테스트하는 완성형 파이프라인 템플릿을 제공합니다.
8편 중 4편
다음
GitHub Actions 배포 전략: VPS부터 클라우드 플랫폼까지의 CD 파이프라인
GitHub Actions의 세 가지 배포 전략인 VPS SSH 배포, 클라우드 플랫폼 호스팅(Vercel/Cloudflare/Netlify), 하이브리드 아키텍처를 완전한 workflow 설정과 자주 발생하는 문제 해결 방법과 함께 자세히 설명합니다.
8편 중 6편



댓글
GitHub로 로그인하여 댓글을 남기세요