website: Gradle 빌드 캐시 + 백엔드 테스트 매트릭스 샤딩으로 CI 단축
GitHub Actions에서 돌아가는 백엔드 CI를 컨테이너 기반 통합 테스트와 단위 테스트로 나눠 두 러너에 병렬로 분산시켜, GitHub 무료 러너의 4vCPU 한계로 인한 병목을 우회했습니다.
요약
2026년 7월 23일에 머지된 PR #159 "ci: Gradle 빌드 캐시 + 백엔드 테스트 매트릭스 샤딩으로 CI 단축"은 커밋 메시지 그대로 CI 파이프라인의 실행 시간을 줄이기 위한 작업입니다. 변경된 파일은 .github/workflows/ci.yml, backend/build.gradle, backend/gradle.properties 세 개고, 총 23줄이 추가되고 2줄이 삭제되었습니다. 워크플로 파일 하나만 보면 8줄 추가, 2줄 삭제로 비교적 작은 변경이지만, 실제로는 CI가 테스트를 실행하는 방식 자체를 바꾸는 구조적인 수정입니다.
배경 및 목적
백엔드 테스트 잡을 하나의 러너에서 전부 돌리다 보니, 컨테이너를 띄워서 도는 무거운 통합 테스트(*IntegrationTest)와 가벼운 단위/슬라이스 테스트가 한 프로세스 안에서 순차적으로 실행되고 있었을 겁니다. GitHub Actions 무료 러너는 4vCPU로 한정되어 있어서, 테스트 스위트가 커질수록 이 병목이 CI 전체 소요 시간을 늘리는 주된 원인이 됩니다. 코드에 남긴 주석에도 "GH 무료 러너 4vCPU 한계를 우회"한다고 명시되어 있어서, 목적이 명확합니다 — 테스트를 성격별로 쪼개서 병렬로 돌리자는 것입니다.
구현 내용
핵심은 backend 테스트 잡에 strategy.matrix를 추가한 부분입니다.
strategy:
fail-fast: false
matrix:
# 두 러너에 나눠 돌려 GH 무료 러너 4vCPU 한계를 우회 — 컨테이너 기반 무거운 통합테스트(*IntegrationTest)와
# 나머지 단위/슬라이스 테스트를 분리. 분리 기준은 build.gradle의 testShard 필터 참고.
shard: [unit, integration]
fail-fast: false를 준 게 눈에 띄는데, 한쪽 샤드(예: integration)가 실패해도 다른 샤드(unit)는 끝까지 결과를 보고 싶다는 의도로 보입니다. 두 샤드가 독립적인 테스트 그룹이니 하나 실패했다고 나머지를 중단시킬 이유가 없죠.
테스트 실행 스텝도 이에 맞춰 수정됐습니다.
- name: 빌드 + 테스트 (${{ matrix.shard }})
working-directory: backend
run: |
chmod +x gradlew
./gradlew compileJava test -PtestShard=${{ matrix.shard }}
-PtestShard=${{ matrix.shard }} 프로젝트 프로퍼티를 넘겨서, Gradle 쪽(backend/build.gradle)에서 이 값을 받아 unit이면 일반 테스트만, integration이면 *IntegrationTest 패턴에 해당하는 테스트만 필터링하도록 구성했을 것으로 보입니다. 다만 이번 데이터에는 build.gradle과 gradle.properties의 실제 diff는 포함되어 있지 않아서, 필터 로직이나 Gradle 빌드 캐시 설정(org.gradle.caching=true 같은 옵션)의 세부 구현은 워크플로 파일만큼 구체적으로 다루기는 어렵습니다. 커밋 메시지에 "Gradle 빌드 캐시"가 함께 언급된 걸 보면, gradle.properties에서 캐싱 관련 옵션을 켜서 매트릭스로 나뉜 두 잡이 각각 빌드 캐시의 이득도 보도록 설계한 것 같습니다.
기술적 의사결정
테스트를 나누는 방법은 여러 가지가 있을 텐데, 이번엔 별도의 CI 도구(예: 서드파티 테스트 스플리터)나 Gradle 자체의 병렬 워커 대신 GitHub Actions의 matrix 전략과 Gradle 프로젝트 프로퍼티(-P) 조합을 선택했습니다.
- matrix + 프로젝트 프로퍼티 방식의 장점: 워크플로 YAML 몇 줄과 Gradle 빌드 스크립트의 필터 로직만으로 구현 가능해서 별도 의존성이 늘지 않습니다.
shard배열에 항목을 추가하는 것만으로 확장도 쉽습니다. - 단점: 샤드 분류(어떤 테스트가 unit이고 어떤 게 integration인지)를 수동으로 관리해야 하고, 두 샤드 간 실행 시간이 불균형하면 병렬화 효과가 제한적입니다. 예를 들어 통합 테스트가 압도적으로 오래 걸린다면, 단위 테스트 러너는 일찍 끝나고 노는 시간이 생길 수 있습니다.
이 프로젝트 규모에서는 테스트 스위트가 크게 두 성격(무거운 컨테이너 통합 테스트 vs 가벼운 단위 테스트)으로 뚜렷이 나뉘기 때문에, 복잡한 자동 분산 도구 없이 단순한 2분할 매트릭스로도 충분히 효과를 볼 수 있다고 판단한 것으로 보입니다.
배운 점 및 개선점
CI 시간을 줄이는 방법이 꼭 코드를 병렬화하거나 테스트를 줄이는 것만이 아니라, "무엇을 어디서 실행할지" 잡을 쪼개는 것만으로도 개선할 수 있다는 걸 보여주는 사례입니다. 다만 주석에 적힌 것처럼 분리 기준이 build.gradle의 testShard 필터에 있다 보니, 앞으로 새로운 통합 테스트 클래스를 추가할 때 네이밍 컨벤션(*IntegrationTest)을 지키지 않으면 의도치 않게 unit 샤드에 섞여 들어갈 위험이 있습니다. 이 부분은 문서화나 린트 규칙으로 보완하면 좋을 것 같습니다. 다음 단계로는 두 샤드의 실제 실행 시간을 비교해서 필요하면 샤드를 더 세분화하거나, 캐시 히트율을 모니터링해서 빌드 캐시가 실제로 얼마나 효과를 내는지 확인해보는 게 자연스러운 흐름일 것 같습니다.