이번 포스팅에서는 GitHub Actions로 Gradle 프로젝트의 빌드·테스트·배포를 자동화하는 방법을 정리하고자 한다. GitHub Actions는 저장소에 이벤트가 발생하면 지정한 작업을 대신 실행해 주는 CI/CD 도구다. push할 때마다 로컬에서 ./gradlew build를 돌리고 테스트가 깨졌는지 확인하던 일을, 워크플로우 YAML 하나로 GitHub 서버가 대신 해 준다. 여기서는 첫 워크플로우 작성부터 러너 선택, 비용 구조, 의존성 캐싱, 시크릿, 매트릭스 빌드, 스케줄 실행, 상태 배지까지 실무에서 실제로 부딪히는 지점을 차례로 다룬다.
GitHub Actions가 정확히 뭘 해 주나
GitHub Actions는 저장소에서 일어나는 이벤트(push, PR, 스케줄 등)를 트리거로 삼아 미리 정의한 작업을 자동 실행하는 CI/CD 플랫폼이다. 워크플로우를 YAML 파일로 저장소 안에 두면, GitHub이 관리하는 가상 머신(러너)이 그 파일을 읽어 빌드·테스트·배포를 수행한다. 별도 서버를 세울 필요가 없고, public 저장소는 표준 러너를 무료로 쓴다.
핵심 개념은 네 가지다. **워크플로우(workflow)**는 .github/workflows/ 아래 YAML 파일 하나이고, 언제 실행할지는 **이벤트(on)**로 정한다. 워크플로우 안에는 여러 **잡(job)**이 있고, 잡은 여러 **스텝(step)**으로 이루어진다. 스텝은 셸 명령을 직접 실행(run)하거나, 남이 만들어 둔 재사용 단위인 **액션(action)**을 불러(uses) 쓴다.

# .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: echo "빌드를 시작한다"YAML위 파일은 GitHub Actions 워크플로우의 최소 형태다. main 브랜치로 push하거나 main을 대상으로 PR을 올리면, ubuntu-latest 러너가 저장소를 체크아웃하고 스텝을 실행한다. actions/checkout@v6은 GitHub이 공식 제공하는 액션으로, 러너에 저장소 코드를 받아 오는 첫 스텝에 거의 항상 들어간다. 이 뼈대에 실제 빌드 명령을 채워 넣는 것이 다음 단계다.
Gradle 프로젝트를 push 한 번에 빌드·테스트하기
GitHub Actions로 Gradle 빌드를 자동화하려면 체크아웃 → JDK 설치 → ./gradlew build 세 스텝이면 된다. JDK 설치는 actions/setup-java가 맡고, cache: 'gradle' 옵션을 주면 의존성 캐시까지 한 번에 잡힌다. 아래가 실무에서 바로 쓰는 완결 워크플로우다.

# .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 저장소 체크아웃
uses: actions/checkout@v6
- name: JDK 21 설치
uses: actions/setup-java@v6
with:
distribution: 'temurin' # Eclipse Temurin 배포판
java-version: '21'
cache: 'gradle' # ~/.gradle 의존성 캐시 자동 관리
- name: gradlew 실행 권한 부여
run: chmod +x ./gradlew
- name: Gradle 빌드 및 테스트
run: ./gradlew buildYAML이 워크플로우는 push나 PR이 생길 때마다 Ubuntu 러너에서 JDK 21(Temurin)을 설치하고 ./gradlew build를 실행한다. build 태스크는 컴파일과 test를 함께 돌리므로, 테스트가 깨지면 잡이 실패로 표시되고 PR 화면에 빨간 체크가 뜬다. chmod +x ./gradlew는 리눅스 러너에서 gradlew 스크립트에 실행 권한이 없을 때를 대비한 안전장치다. distribution은 Temurin 외에 zulu, corretto, oracle 등을 지원한다(setup-java 문서). JDK 버전과 액션 메이저 버전은 프로젝트 상황에 맞춰 바꾸면 된다.
액션 버전 뒤의 @v6 같은 태그는 메이저 버전 고정을 뜻한다. actions/setup-java는 v1~v4가 이미 deprecated 처리됐으므로 최신 메이저로 올려 쓰는 편이 안전하고, 보안이 중요한 조직이라면 태그 대신 커밋 SHA로 핀(uses: actions/checkout@<40자리 SHA>)해 공급망 위험을 줄인다. 최신 버전은 각 액션의 GitHub 저장소나 Marketplace에서 확인한다.
러너 고르기 — GitHub-hosted, self-hosted, larger
러너(runner)는 워크플로우를 실제로 실행하는 머신이고, runs-on으로 지정한다. 선택지는 셋이다. GitHub이 매번 깨끗한 VM을 띄워 주는 GitHub-hosted 러너(ubuntu-latest 등), 내 서버에 에이전트를 설치해 붙이는 self-hosted 러너, 그리고 더 큰 사양이 필요할 때 쓰는 larger 러너다. 대부분은 GitHub-hosted로 충분하고, 특수한 요구가 생길 때만 나머지를 고른다.
| 러너 종류 | 언제 쓰나 | 특징 |
|---|---|---|
| GitHub-hosted | 기본값. 대부분의 CI | ubuntu-latest·windows-latest·macos-latest. 매번 깨끗한 VM, 관리 불필요 |
| self-hosted | 사내망 접근·특수 하드웨어·GPU | 내 인프라에 러너 에이전트 설치. 비용은 자기 서버 부담 |
| larger | 코어·메모리 부족, 정적 IP 필요 | Team·Enterprise Cloud 전용. 오토스케일 풀, 고정 IP 대역 |
self-hosted 러너는 내 서버에서 ./config.sh로 저장소에 등록하고 ./run.sh로 띄운 뒤, 워크플로우에서 runs-on: self-hosted로 지정한다. 사내망 데이터베이스에 접근해야 하거나 GPU 빌드가 필요한 경우처럼, GitHub-hosted로는 안 되는 상황에서 쓴다. 다만 2026년 3월 16일부터 GitHub은 v2.329.0보다 오래된 self-hosted 러너의 등록을 차단하므로, 러너 에이전트를 주기적으로 업데이트해야 한다(GitHub Changelog). larger 러너는 Team·Enterprise Cloud 플랜에서만 열리는 관리형 VM으로, 풀 단위로 오토스케일되며 같은 고정 IP 대역을 공유해 방화벽 화이트리스트를 잡기 쉽다(larger runners 문서).
GitHub Actions 요금 — 무료 한도와 과금 구조
요금은 public 저장소냐 private 저장소냐에서 갈린다. GitHub Actions는 public 저장소에서 표준 러너를 무제한 무료로 쓰고, private 저장소는 플랜별 무료 분(minute)이 정해져 있다. 무료 분을 넘기면 분당 요금이 붙는데, 러너 OS에 따라 단가가 크게 다르다. 비용은 워크플로우를 실행한 사람이 아니라 저장소 소유자에게 청구된다(공식 요금 문서).
| 플랜 | 월 포함 분 | 아티팩트 저장소 |
|---|---|---|
| Free | 2,000 | 500 MB |
| Pro | 3,000 | 1 GB |
| Team | 3,000 | 2 GB |
| Enterprise Cloud | 50,000 | 50 GB |
무료 분을 초과하면 표준 러너 기준으로 Linux 2코어가 분당 $0.006, Windows 2코어가 $0.010, macOS가 $0.062다. 같은 잡이라도 macOS 러너가 Linux의 10배 안팎으로 비싸다는 점이 실무에서 가장 흔한 함정이다. iOS 빌드처럼 꼭 필요한 경우가 아니면 macOS 러너를 매 push마다 돌리지 않는 게 좋다. 저장소 저장 공간도 아티팩트는 GB·월당 $0.25, 캐시는 $0.07이 초과분에 붙는다. private 저장소에서 매트릭스로 여러 OS를 병렬로 돌리면 분 소모가 배수로 늘어나므로, 무료 분 안에서 굴리려면 Linux 위주로 잡고 macOS·Windows는 필요한 브랜치에서만 실행하도록 트리거를 좁히는 편이 낫다.
의존성 캐싱으로 빌드 시간 줄이기
GitHub Actions에서 Gradle 빌드가 느린 가장 큰 이유는 매번 의존성을 새로 내려받기 때문이고, 캐싱으로 이걸 없애면 빌드가 눈에 띄게 빨라진다. 방법은 두 가지다. setup-java의 cache 옵션을 쓰는 간단한 방식과, Gradle 팀이 만든 gradle/actions/setup-gradle를 쓰는 정교한 방식이다.
간단한 방식은 앞서 본 것처럼 setup-java에 cache: 'gradle' 한 줄을 더하는 것이다. 이러면 ~/.gradle 아래 의존성과 wrapper가 캐시돼, 다음 실행부터 다운로드를 건너뛴다. Maven이나 sbt를 쓴다면 값만 maven, sbt로 바꾸면 된다.
- name: Gradle 캐싱 (정교한 방식)
uses: gradle/actions/setup-gradle@v4
- name: 빌드
run: ./gradlew buildYAMLgradle/actions/setup-gradle는 캐싱이 기본으로 켜져 있어, Gradle User Home의 핵심 부분(빌드 스크립트 컴파일 결과, 다운로드한 의존성)을 자동으로 캐시한다(gradle/actions 문서). 브랜치별로 캐시를 분리하고 만료를 관리해 주기 때문에, 모노레포나 서브프로젝트가 많은 저장소에서 setup-java의 기본 캐싱보다 적중률이 높다. 단순한 단일 모듈 프로젝트라면 setup-java의 cache: 'gradle'로 충분하고, 서브프로젝트가 여럿이거나 빌드가 무거우면 setup-gradle로 올리는 판단이 합리적이다.
같은 브랜치에 연달아 push하면 이전 실행은 취소하기
GitHub Actions는 기본적으로 push할 때마다 새 실행을 띄우고 이전 실행은 그대로 둔다. 빠르게 여러 번 커밋하면 이미 의미가 없어진 낡은 실행이 러너를 붙잡고 무료 분을 태운다. concurrency 블록으로 같은 브랜치의 진행 중 실행을 자동 취소하면 이 낭비가 사라진다.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: trueYAMLgroup을 워크플로우 이름과 github.ref(브랜치나 PR) 조합으로 잡으면 같은 브랜치의 실행이 하나의 그룹으로 묶인다. cancel-in-progress: true면 새 push가 들어올 때 같은 그룹의 진행 중 실행이 즉시 취소된다(concurrency 문서). PR에 커밋을 연달아 올릴 때 특히 효과가 크고, private 저장소라면 앞서 본 요금 절감으로 바로 이어진다. 워크플로우 최상단(잡 바깥)에 두면 워크플로우 전체에, 잡 안에 두면 그 잡에만 적용된다.
시크릿과 매트릭스 — 실무에서 바로 걸리는 두 가지
배포 토큰 같은 민감한 값은 코드에 넣지 않고 GitHub Actions의 시크릿으로 관리하며, 여러 JDK나 OS 조합을 한 번에 검증하려면 매트릭스를 쓴다. 시크릿은 저장소 Settings → Secrets and variables → Actions에 등록하고, 워크플로우에서 ${{ secrets.이름 }}으로 참조한다. 값은 로그에 자동으로 마스킹된다.
- name: 배포
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: ./gradlew publishYAML위 스텝은 DEPLOY_TOKEN 시크릿을 환경 변수로 주입해 ./gradlew publish에 넘긴다. 토큰이 워크플로우 파일이나 로그에 노출되지 않으므로, 저장소가 공개돼 있어도 안전하다. 포크에서 올라온 PR에는 기본적으로 시크릿이 전달되지 않는다는 점도 알아 두면 좋다.
매트릭스는 하나의 잡을 여러 변수 조합으로 병렬 실행한다. “우리 라이브러리가 JDK 17과 21, Linux와 Windows에서 다 도는가”를 한 워크플로우로 확인할 때 쓴다.
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest ]
java: [ '17', '21' ]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v6
with:
distribution: 'temurin'
java-version: ${{ matrix.java }}
cache: 'gradle'
- run: ./gradlew testYAML이 매트릭스는 OS 2종 × JDK 2종 = 총 4개의 잡을 병렬로 띄운다. fail-fast: false를 주면 한 조합이 실패해도 나머지 조합은 끝까지 실행돼, 어떤 조합에서만 깨지는지 한눈에 본다. 다만 앞서 말했듯 private 저장소에서는 조합 수만큼 분이 소모되므로, macOS를 매트릭스에 넣을 때는 비용을 함께 고려한다.
빌드를 넘어 배포까지 — Docker 이미지를 GHCR에 올리기
CI에서 CD로 넘어가는 가장 흔한 형태는 빌드 산출물을 컨테이너 이미지로 만들어 레지스트리에 올리는 것이다. GitHub Actions는 GitHub Container Registry(GHCR)와 통합돼 있어, 별도로 시크릿을 만들지 않고 실행마다 자동 발급되는 GITHUB_TOKEN만으로 이미지를 푸시한다. 필요한 건 packages: write 권한과 도커 액션 세 개다.
name: Publish image
on:
push:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # GHCR 푸시에 필수
steps:
- uses: actions/checkout@v6
- name: GHCR 로그인
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 태그·라벨 자동 생성
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: 이미지 빌드 및 푸시
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}YAMLmain에 push되면 저장소 루트의 Dockerfile로 이미지를 빌드해 ghcr.io/{소유자}/{저장소}에 올린다. 핵심은 permissions의 packages: write와 로그인에 쓰는 GITHUB_TOKEN이다. 이 토큰은 워크플로우 실행마다 GitHub이 자동 발급하므로 별도 시크릿을 만들 필요가 없고, github.actor(실행을 트리거한 계정)로 로그인한다(Publishing Docker images 문서). metadata-action이 브랜치·태그·커밋 SHA를 읽어 이미지 태그를 자동으로 붙여 주기 때문에, 태그를 손으로 관리하지 않아도 된다.
여기서 실제 서버나 쿠버네티스 배포로 확장하려면, 이 이미지를 받아 배포하는 잡을 뒤에 잇거나 environments 기능으로 승인 게이트를 건다. production 환경에 required reviewers를 지정하면 승인 전까지 배포 잡이 멈추므로, 검토 없이 프로덕션에 나가는 실수를 막을 수 있다.
스케줄 실행과 상태 배지
push가 없어도 워크플로우는 돌 수 있다. GitHub Actions는 cron 스케줄로 정해진 시각에 워크플로우를 실행하고, 빌드 상태를 README에 배지로 노출한다. 스케줄은 야간 정기 빌드나 의존성 취약점 점검처럼 “정해진 시각에 도는 작업”에 쓴다. cron은 UTC 기준이라 한국 시간으로 환산해야 한다.
on:
schedule:
- cron: '0 18 * * 1-5' # UTC 18:00 = KST 03:00, 평일(월~금)YAML위 설정은 평일 새벽 3시(KST)에 워크플로우를 실행한다. cron 5필드는 분 시 일 월 요일 순서이고 UTC를 쓴다는 점만 주의하면 된다. 스케줄 워크플로우는 기본 브랜치의 파일 기준으로만 동작하므로, 스케줄을 추가한 워크플로우는 main에 머지돼 있어야 실제로 트리거된다.
상태 배지는 워크플로우가 성공했는지 실패했는지를 이미지 한 장으로 보여 준다. README에 아래 마크다운을 넣으면 된다.
PlaintextOWNER와 REPO를 자기 저장소로 바꾸고 ci.yml을 워크플로우 파일명으로 맞추면, 최신 실행 결과가 초록/빨강 배지로 표시된다. 특정 브랜치 기준으로 보고 싶으면 뒤에 ?branch=main을 붙인다. 오픈소스 저장소라면 이 배지 하나로 “이 프로젝트의 빌드가 지금 통과 상태인가”를 방문자에게 바로 알릴 수 있다.
자주 묻는 질문 (FAQ)
GitHub Actions는 무료인가?
public 저장소는 표준 GitHub-hosted 러너를 무제한 무료로 쓴다. private 저장소는 플랜별로 월 무료 분이 있는데 Free 2,000분, Pro·Team 3,000분, Enterprise Cloud 50,000분이다. 초과분은 러너 OS에 따라 분당 과금되며 저장소 소유자에게 청구된다.
워크플로우 파일은 어디에 두나?
저장소 루트의 .github/workflows/ 디렉토리 아래에 .yml(또는 .yaml) 파일로 둔다. 파일 하나가 워크플로우 하나이고, 여러 개를 둬서 CI용·배포용·스케줄용으로 나눠도 된다. 파일명은 자유지만 상태 배지 URL에 그대로 들어가므로 알아보기 쉽게 짓는 게 좋다.
빌드가 매번 너무 느린데 어떻게 줄이나?
의존성 캐싱부터 켠다. setup-java에 cache: 'gradle'을 주거나 gradle/actions/setup-gradle을 쓰면, 두 번째 실행부터 의존성 다운로드를 건너뛰어 빌드가 빨라진다. 그래도 느리면 불필요한 OS를 매트릭스에서 빼고, 테스트를 병렬화하거나 변경된 모듈만 빌드하도록 트리거를 좁힌다.
macOS 러너가 비싸다는데 얼마나 차이 나나?
표준 러너 기준으로 macOS는 분당 $0.062로, Linux 2코어($0.006)의 10배 안팎이다. iOS·macOS 앱 빌드처럼 꼭 필요한 경우가 아니면 상시 실행은 피하고, 릴리스 태그나 특정 브랜치에서만 돌도록 트리거를 좁히는 게 비용 관리의 핵심이다.
self-hosted 러너는 언제 쓰나?
사내망 리소스(내부 DB, 라이선스 서버)에 접근해야 하거나, GPU 같은 특수 하드웨어가 필요하거나, GitHub-hosted 사양으로는 부족할 때 쓴다. 대신 러너 서버의 보안 패치와 에이전트 업데이트를 직접 책임져야 하고, 2026년 3월부터는 v2.329.0 미만 러너의 등록이 막힌다는 점을 감안해야 한다.
마치며
지금까지 GitHub Actions로 Gradle 프로젝트를 자동화하는 방법을 정리해 보았다. 처음 CI를 붙일 때는 대개 빌드 워크플로우 하나로 시작하는데, 정작 팀을 괴롭히는 건 문법이 아니라 비용과 속도인 경우가 많았다. 아무 생각 없이 매트릭스에 macOS를 넣어 두고 매 push마다 돌리다가 무료 분을 순식간에 태워 먹은 적이 있는데, 그때부터 러너 OS와 트리거 범위를 먼저 따지는 습관이 생겼다. 캐싱도 마찬가지다. cache: 'gradle' 한 줄을 빼먹어서 3분이면 끝날 빌드를 매번 8분씩 기다린 경험이 있다면, 이 글의 캐싱 섹션만이라도 먼저 적용해 보길 권한다. 워크플로우는 한 번 제대로 잡아 두면 그 뒤로는 손이 거의 안 가는, 투자 대비 회수가 확실한 축에 속하는 작업이다.
