GitHub Actions로 Java/Maven 프로젝트 CI 설정하기

1. 개요

Continuous Integration (CI)은 코드를 푸시할 때 즉시 오류를 발견하는 데 도움이 되므로 병합할 때까지 기다렸다가 문제가 터지는 일을 피할 수 있습니다. 이 글에서는 Maven을 사용하는 Java 프로젝트의 CI 파이프라인을 GitHub Actions에서 구축하는 방법을 안내합니다. 빌드, 테스트 실행, 의존성 캐싱, 여러 JDK 버전의 빌드 매트릭스 구성, 리포트 발행 등을 다룹니다. 이 글을 읽으면 워크플로우 파일을 실제 프로젝트에 복사해서 몇 분 안에 실행할 수 있습니다.

2. 샘플 프로젝트 준비

간단한 Spring Boot 프로젝트가 있다고 가정하겠습니다. 표준 Maven 구조를 따르는 pom.xml 파일의 최소 구성은 다음과 같습니다:

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.nonladev</groupId>
    <artifactId>order-service</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.25.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version>
            </plugin>
        </plugins>
    </build>
</project>

junit-jupiter와 assertj-core 의존성은 테스트를 작성하기 위해 사용됩니다. maven-surefire-plugin 플러그인은 mvn test를 실행할 때 테스트를 실행하는 책임이 있습니다.

테스트를 위한 작은 비즈니스 클래스:

package com.nonladev.order;

public class OrderCalculator {

    public double calculateTotal(double unitPrice, int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("Quantity는 0보다 커야 합니다");
        }
        return unitPrice * quantity;
    }
}

그리고 해당하는 테스트:

package com.nonladev.order;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class OrderCalculatorTest {

    private final OrderCalculator calculator = new OrderCalculator();

    @Test
    void shouldCalculateTotalCorrectly() {
        double total = calculator.calculateTotal(50_000, 3);

        assertThat(total).isEqualTo(150_000);
    }

    @Test
    void shouldThrowWhenQuantityIsInvalid() {
        assertThatThrownBy(() -> calculator.calculateTotal(50_000, 0))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Quantity는 0보다 커야 합니다");
    }
}

shouldCalculateTotalCorrectly는 올바른 합계 계산을 확인합니다. shouldThrowWhenQuantityIsInvalid는 quantity가 유효하지 않을 때 예외가 발생하는지 확인합니다. 이는 CI가 코드를 푸시할 때마다 실행할 테스트입니다.

3. 기본 CI 워크플로우

GitHub Actions는 .github/workflows/ 디렉토리의 YAML 파일을 읽습니다. .github/workflows/ci.yml 파일을 만드세요:

name: CI

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 and run tests
        run: mvn -B clean verify

각 부분을 설명하겠습니다:

  • on: workflow는 main 브랜치를 대상으로 push 또는 pull_request가 있을 때 실행됩니다.
  • actions/checkout@v4: 소스 코드를 러너에 클론합니다.
  • actions/setup-java@v4: JDK 21 Temurin 버전을 설치합니다 — mvn을 호출하기 전에 항상 이 단계가 필요합니다.
  • mvn -B clean verify: -B 플래그(배치 모드)는 대화식 출력을 비활성화하며, CI 환경에 적합합니다. clean verify는 처음부터 다시 빌드하고 테스트를 실행합니다(verify 페이즈는 test를 포함합니다).

커밋을 푸시하고 GitHub의 Actions 탭으로 가면 작업이 실행되는 것을 보고 유사한 로그를 볼 수 있습니다:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.nonladev.order.OrderCalculatorTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

테스트가 실패하면 작업의 상태가 빨강색이 되고 로그에서 스택 트레이스를 바로 볼 수 있습니다 — 어떤 머신에도 SSH로 접속할 필요가 없습니다.

4. Maven 의존성 캐싱으로 빌드 속도 높이기

매번 새 러너에서 작업이 실행될 때마다(임시), Maven은 .m2/repository에서 모든 의존성을 다시 다운로드해야 합니다. Spring Boot 프로젝트의 경우 이 작업은 매번 1-2분이 걸릴 수 있습니다. setup-java는 미리 캐싱을 지원합니다:

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
          cache: 'maven'

cache: 'maven' 속성은 pom.xml 파일의 해시를 기반으로 ~/.m2/repository 디렉토리를 자동으로 캐시합니다. 다음 빌드에서 pom.xml이 변경되지 않으면 Actions가 캐시를 즉시 복원하고 각 jar을 다시 다운로드하는 단계를 건너뜁니다.

“Build and run tests” 단계의 시간을 캐싱 추가 전후로 비교하여 효율성을 확인할 수 있습니다 — 보통 30-60% 감소합니다.

참고: 프로젝트가 Maven Wrapper(./mvnw)를 사용하면 러너에 미리 설치된 Maven 버전을 신뢰하는 대신 mvnw에 따라 캐싱해야 합니다. GitHub가 이미지를 업데이트할 때마다 기본 Maven 버전이 변경될 수 있기 때문입니다.

5. 여러 JDK 버전의 빌드 매트릭스

여러 Java 버전과 호환되어야 하는 라이브러리를 유지 관리하는 경우 strategy.matrix를 사용하여 여러 작업을 병렬로 실행할 수 있습니다:

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        java-version: [ '17', '21' ]

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK ${{ matrix.java-version }}
        uses: actions/setup-java@v4
        with:
          java-version: ${{ matrix.java-version }}
          distribution: 'temurin'
          cache: 'maven'

      - name: Build and run tests
        run: mvn -B clean verify

matrix.java-version은 테스트할 버전 목록을 나열합니다. GitHub Actions가 2개의 병렬 작업을 자동으로 생성합니다: 하나는 JDK 17, 하나는 JDK 21입니다. ${{ matrix.java-version }} 변수가 각 해당 작업에 대체됩니다.

Actions 탭의 결과는 예를 들어 build (17)과 build (21)과 같이 2개의 개별 작업을 표시하므로 프로젝트가 어느 Java 버전에서 실패하는지 즉시 알 수 있습니다.

6. 테스트 리포트 발행 및 커버리지 확인

기본적으로 테스트 로그는 Actions에서 텍스트 형태로만 표시됩니다. 더 읽기 쉬운 리포트를 위해 dorny/test-reporter 액션을 사용하세요:

      - name: Build and run tests
        run: mvn -B clean verify

      - name: Publish test report
        uses: dorny/test-reporter@v1
        if: success() || failure()
        with:
          name: Maven Tests
          path: '**/surefire-reports/*.xml'
          reporter: java-junit

if: success() || failure(): 이 단계는 테스트가 실패해도 실행됩니다(작업이 취소되면 실행되지 않음). 항상 리포트를 볼 수 있도록 합니다. path는 테스트 실행 후 Surefire가 생성한 XML 파일을 가리킵니다. 리포트는 PR의 별도 검사로 표시되며, 각 테스트 케이스의 통과/실패 여부를 나열합니다.

커버리지를 측정하려면 pom.xml에 JaCoCo 플러그인을 추가하세요:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.12</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

mvn verify를 실행하면 target/site/jacoco/index.html에서 HTML 리포트를 자동으로 생성합니다. 이를 아티팩트로 업로드할 수 있습니다:

      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: jacoco-report
          path: target/site/jacoco/

이 아티팩트는 워크플로우 실행의 요약 페이지에서 다운로드할 수 있으며, 브라우저에서 직접 볼 수 있습니다.

7. 빌드 상태 배지

워크플로우가 안정적으로 실행되면 Actions에 들어가지 않고도 모든 사람이 CI 상태를 알 수 있도록 README.md에 배지를 추가하세요:

![CI](https://github.com/<org>/<repo>/actions/workflows/ci.yml/badge.svg)

<org>/<repo>을 실제 GitHub 이름으로 바꾸세요. 배지는 기본 브랜치에서의 최근 실행 결과에 따라 초록색/빨강색으로 자동 업데이트됩니다.

8. 자주 하는 실수

  • java-version을 maven.compiler.source와 일치하도록 설정하지 않음: 로컬 빌드는 통과하지만 CI는 release version X not supported 오류로 실패합니다. pom.xml과 워크플로우 간의 JDK 버전을 항상 동기화하세요.
  • mvn verify 대신 mvn test 사용: 프로젝트에 verify 페이즈에서 실행되는 통합 테스트(예: Failsafe 플러그인)가 있으면 mvn test는 이를 건너뛰어 거짓된 보안 감각을 만듭니다.
  • 의존성을 변경할 때 캐시 키가 잘못됨: 수동으로 캐시 액션(actions/cache)을 작성할 때 키 해시에 pom.xml을 포함하는 것을 잊으면, 이전 캐시로 인해 빌드가 오래된 의존성을 사용할 수 있습니다. 이를 피하려면 setup-java의 cache: 'maven'을 사용하세요.
  • Docker/안정적인 네트워크가 없는 자체 호스팅 러너에서 CI 실행: Maven은 중앙 저장소에서 의존성을 다운로드해야 합니다. 러너의 네트워크가 차단되면 빌드가 다운로드 단계에서 중단됩니다. 프록시를 확인하거나 내부 Maven 미러를 사용하세요.

참고: 프로젝트가 매우 작고 간단한 테스트만 있으며 자주 배포하지 않는다면, matrix + 커버리지 리포트가 포함된 완전한 CI 워크플로우는 과할 수 있습니다. 섹션 3의 기본 워크플로우로 시작한 후 프로젝트가 커질수록 점진적으로 확장하세요.

9. 결론

수십 줄의 YAML로 Maven 프로젝트를 위한 CI 파이프라인을 자동으로 빌드, 테스트, 의존성 캐싱 및 리포트 발행할 수 있습니다. 간단한 워크플로우로 시작한 후 실제 필요에 따라 JDK matrix, JaCoCo 커버리지, README 배지를 추가하세요. 가장 중요한 것은 복잡한 워크플로우가 아니라 매번 코드를 푸시할 때 CI가 안정적으로 실행되고 신뢰할 수 있다는 것입니다.

github-actionsjavamavenci-cdtesting