GitHub Actions 기초: workflow, job, step
1. 개요
GitHub Actions는 GitHub에 내장된 CI/CD 도구로, 별도의 CI 서버 없이 빌드, 테스트, 배포를 자동화할 수 있습니다. 이 글에서는 workflow, job, step이라는 세 가지 핵심 개념을 설명하고 Maven을 사용하는 Spring Boot 프로젝트의 구체적인 예제를 제시합니다.
이 글을 읽고 나면, 코드를 푸시할 때마다 자동으로 테스트를 실행하는 workflow 파일을 작성할 수 있고, job이 병렬로 실행되거나 순차적으로 실행되는 방식을 이해하며, workflow 실패 시 디버깅하는 방법을 알게 됩니다.
2. Workflow란?
Workflow는 저장소의 .github/workflows/ 디렉터리에 있는 YAML 파일입니다. 각 workflow는 언제 실행될지(trigger)와 무엇을 할지(jobs)를 정의합니다.
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Build with Maven
run: mvn -B package
설명:
name은 GitHub의 Actions 탭에 표시되는 이름입니다.on은 trigger를 선언합니다. 여기서 workflow는main브랜치에push또는pull_request가 발생하면 실행됩니다.jobs.build는build라는 이름의 단일 job이며,ubuntu-latest가상 머신에서 실행됩니다.
주의: 파일은 반드시
.github/workflows/*.yml경로에 있어야 하며, 경로가 잘못되면 GitHub이 workflow를 인식하지 않습니다.
3. Job: 독립적인 실행 단위
Workflow는 여러 개의 job을 가질 수 있습니다. 각 job은 별개의 runner(가상 머신)에서 실행되며, 기본값으로 job들은 병렬로 실행됩니다. needs를 선언하지 않는 한 말입니다.
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: mvn -B test
deploy:
runs-on: ubuntu-latest
needs: test
steps:
- run: echo "Deploying application..."
설명:
test과deploy는 두 개의 별개 job이며, 각 job은 깨끗한 runner 환경을 가지고 있고 서로 시스템 파일을 공유하지 않습니다.needs: test는deploy가test의 성공을 기다려야 하고 그 다음에 실행되도록 합니다. 이것이 순차적 관계를 만듭니다.needs가 없으면 GitHub Actions는test과deploy를 동시에 실행합니다.
3.1. Job 간의 데이터 공유
각 job이 독립적인 runner를 가지고 있기 때문에, 한 job에서 다른 job으로 파일을 전달하려면 artifacts를 사용해야 합니다.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: mvn -B package
- uses: actions/upload-artifact@v4
with:
name: app-jar
path: target/*.jar
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/download-artifact@v4
with:
name: app-jar
설명:
upload-artifact는buildjob의.jar파일을 GitHub의 임시 저장소에 저장합니다.deployjob의download-artifact는 새로운 runner로 파일을 다시 가져옵니다.- 이것이 job 간에 데이터를 전달하는 유일한 방법입니다(일반적인 환경 변수는 사용할 수 없습니다).
4. Step: 가장 작은 작업 단위
Step는 job 내의 구체적인 작업으로, 위에서 아래로 순차적으로 실행됩니다. Step에는 두 가지 주요 유형이 있습니다: uses(미리 정의된 action 호출)를 사용하거나 run(셸 명령 실행)을 사용합니다.
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Cache Maven dependencies
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
- name: Run tests
run: mvn -B test
- name: Print result
run: echo "Test step finished with status ${{ job.status }}"
설명:
Checkout codestep은 미리 정의된 action인actions/checkout을 사용하여 source code를 runner로 가져옵니다.Cache Maven dependenciesstep은~/.m2/repository디렉터리를 캐시하여, 종속성을 다시 다운로드하지 않아도 되므로 이후 빌드가 더 빠릅니다.- 캐시의
key는pom.xml의 해시를 기반으로 합니다.pom.xml이 변경되면 이전 캐시는 버려지고 새로운 캐시가 생성됩니다. - 마지막 step은
run을 사용하여 일반적인 셸 명령을 실행하며,${{ job.status }}와 같은 context를 참조할 수 있습니다.
5. 환경 변수와 secrets
일반적으로 workflow에 민감한 설정(API 키, 토큰)을 전달해야 하지만 YAML 파일에 하드코딩하고 싶지 않습니다.
jobs:
deploy:
runs-on: ubuntu-latest
env:
SPRING_PROFILES_ACTIVE: prod
steps:
- uses: actions/checkout@v4
- name: Deploy to server
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
echo "Deploying with profile $SPRING_PROFILES_ACTIVE"
curl -H "Authorization: Bearer $DEPLOY_TOKEN" https://api.example.com/deploy
설명:
- job 수준의
env는 그 job의 모든 step에 적용됩니다(SPRING_PROFILES_ACTIVE). - step 수준의
env는 해당 step에만 적용됩니다(DEPLOY_TOKEN). secrets.DEPLOY_TOKEN은 저장소의 Settings → Secrets and variables → Actions에서 가져오며, 실제 값이 로그에 나타나지 않습니다.
주의:
echo $DEPLOY_TOKEN으로 secret 값을 직접 로그에 출력하지 마세요. GitHub이 자동으로 마스킹하지만, secret을 출력하지 않는 것이 가장 좋습니다.
6. Matrix: 여러 버전을 동시에 실행
여러 버전의 Java 또는 OS에서 테스트해야 할 때, matrix를 사용하면 반복되는 job을 작성할 필요가 없습니다.
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
java: [17, 21]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
- run: mvn -B test
설명:
strategy.matrix.java는 값의 목록[17, 21]을 선언합니다.- GitHub Actions는 자동으로 2개의 병렬 job을 생성하며, 각 job은 해당 Java 버전으로 실행됩니다.
- 이 방법은 반복되는 YAML 작성을 절약하며, 코드가 여러 환경에서 제대로 실행되는지 확인해야 할 때 적합합니다.
7. 간단한 테스트로 workflow 확인하기
Workflow는 JUnit으로 직접 테스트되지 않지만, workflow의 mvn test 단계가 프로젝트의 테스트 스위트를 제대로 실행하는지 확인해야 합니다. 예를 들어, CI가 실행할 OrderService에 대한 간단한 테스트는 다음과 같습니다:
class OrderServiceTest {
private final OrderService orderService = new OrderService();
@Test
void shouldCalculateTotalPriceCorrectly() {
Order order = new Order("ORD-001", List.of(
new OrderItem("Keyboard", 2, BigDecimal.valueOf(50)),
new OrderItem("Mouse", 1, BigDecimal.valueOf(20))
));
BigDecimal total = orderService.calculateTotal(order);
assertThat(total).isEqualByComparingTo(BigDecimal.valueOf(120));
}
}
설명:
- 이 테스트는
src/test/java에 있으며, workflow의 step에서mvn -B test에 의해 자동으로 실행됩니다. - 테스트가 실패하면,
Run testsstep이 오류를 보고하고(종료 코드가 0이 아님), job이 중지되며 종속된 job(needs)은 계속되지 않습니다. - 이것이 CI가 유용한 이유입니다. 오류가 있는 코드는
main에 병합되기 전에 차단됩니다.
GitHub Actions UI(tab Actions)에서 실행했을 때의 결과:
✓ Checkout code
✓ Set up JDK 21
✓ Build with Maven
✓ Run tests (shouldCalculateTotalPriceCorrectly PASSED)
8. 일반적인 오류
- YAML 들여쓰기 오류: GitHub Actions는 YAML을 사용하며, 들여쓰기가 1칸 잘못되면 job이나 step이 인식되지 않아 일반적으로 “Invalid workflow file” 오류가 발생합니다.
runs-on누락: 각 job은 반드시runs-on을 가져야 하며, 없으면 workflow가 파싱할 때 바로 실패합니다.- 한 job의 환경 변수를 다른 job에서 사용하기:
env와 출력 변수는 일반적으로 job 간에 자동으로 전달되지 않으므로outputs또는artifact을 사용해야 합니다. - dependency가 변경되었을 때 캐시 키가 변경되지 않음: 캐시
key가 너무 일반적이면(예: 단지maven-cache), 오래된 dependency가 포함된 이전 캐시를 실수로 사용할 수 있습니다. - 모든 push에서 무거운 workflow 실행:
paths또는branches를 제한하지 않으면, 작은 커밋(README 수정)도 전체 빌드 + 테스트 파이프라인을 트리거하여 무료 Actions 실행 시간을 낭비합니다.
주의: 프로젝트에 단순한 빌드만 필요하고 많은 환경이 없다면,
matrix나artifact를 사용할 필요가 없습니다. 불필요한 복잡성을 추가하면 workflow를 유지보수하기 어렵게 만듭니다.
9. 결론
Workflow는 전체 파이프라인을 정의하는 YAML 파일이고, job은 독립적인 실행 블록이며 각각의 runner에서 실행되며, step은 job 내의 구체적인 작업입니다. 이 세 가지 계층을 명확하게 이해하면 CI/CD 파이프라인을 체계적으로 구성할 수 있습니다: 어떤 job이 병렬로 실행되는지, 어떤 job이 needs를 통해 서로 기다려야 하는지, 그리고 데이터를 공유해야 할 때 artifact을 사용하는 방법을 알게 됩니다.
처음 시작할 때는 workflow를 단순하게 유지해야 합니다(하나의 job, 몇 가지 기본 step: checkout, setup Java, test). 프로젝트가 커지면, matrix, cache를 추가하고 build/test/deploy를 별개의 job으로 분리하여 관리와 디버깅을 쉽게 합니다.