이 글은 우아한테크코스 6기 팀 ‘크루루’의 백엔드 크루 초코칩, 도비가 작성하였습니다.


Team 크루루의 CI pipeline

도입 배경

저희는 프로젝트의 비즈니스 로직 개발에 집중하면서 안정적인 기능 작동 여부를 확인하기 위해 테스트 케이스를 검증하고 있습니다.

하지만 개별적으로 테스트가 완료되었는지 테스트 코드를 통해 확인하는 것은 많은 노력이 필요합니다. 또한 매 리뷰마다 작업 브랜치를 로컬로 가져와 테스트 커버리지를 실행하는 것은 번거롭다고 느꼈습니다.

따라서 테스트 코드 커버리지를 정량적으로 측정하고 문서화할 수 있는 도구를 CI 과정에서 자동으로 실행하도록 도입하기로 결정했습니다.

JaCoCo (Gradle)

JaCoCo(Java Code Coverage)는 Java 프로젝트에서 코드 커버리지를 측정하기 위해 사용되는 도구입니다. IntelliJ의 Run with Coverage 기능으로도 쉽게 테스트 커버리지를 측정할 수 있지만, 문서화가 어렵고 측정 기준을 세부화하기 힘들어 JaCoCo 도입을 고민했습니다.

JaCoCo의 장점은 다음과 같습니다.

  1. 세부적인 설정 기능: 테스트 커버리지는 여러 커버리지 기준이 있습니다 JaCoCo는 이러한 세부적인 설정으로 측정 단위, 기준 등을 세부적으로 설정할 수 있습니다. 뿐만 아니라 테스트 커버리지에서 식별되지 않을 영역도 설정할 수 있어, 테스트 커버리지의 신뢰도를 높일 수 있습니다.
  2. CI/CD 파이프라인 연동 및 통합: CI/CD 파이프라인에도 쉽게 통합하여 자동화된 테스트 커버리지 측정 및 모니터링을 구현할 수 있습니다.
  3. 리포트 및 분석 기능: JaCoCo는 3가지 형태(HTML, XML, CSV)의 커버리지 리포트를 제공하여 코드 커버리지 분석을 쉽게 할 수 있습니다. 문서를 통해 코드의 어떤 부분이 테스트되었고, 어떤 부분이 테스트되지 않았는지 명확히 파악할 수 있습니다.

위 이유들로 저희 프로젝트에서는 JaCoCo를 Code coverage test tool로 채택했습니다.

환경 설정

개발 환경

  • Java 17
  • Spring Boot 3.2.4

build.gradle 설정하기

아래 파일은 build.gradle에서 JaCoCo연동을 위해 추가한 코드는 다음과 같습니다.

  plugins {
		//...
    id 'jacoco'
}

tasks.named('test') {
    useJUnitPlatform()
    finalizedBy 'jacocoTestReport'
}

jacoco {
    toolVersion = '0.8.8'
}

jacocoTestReport {
    reports {
        xml.required.set(true)
        csv.required.set(false)
        html.required.set(true)
    }
    finalizedBy 'jacocoTestCoverageVerification'
}

jacocoTestCoverageVerification {
    violationRules {
        rule {
            limit {
                counter = 'LINE'
                value = 'COVEREDRATIO'
                minimum = 0.80
            }
        }
    }
}
  

이제 중요한 부분의 설정들을 알아봅시다.

테스트 작업 설정

  tasks.named('test') {
    useJUnitPlatform()
    finalizedBy 'jacocoTestReport'
}
  

finalizedBy 'jacocoTestReport' 를 통해 test 작업이 완료된 후 jacocoTestReport 작업이 자동으로 실행되도록 설정합니다. jacocoTestReport 작업은 테스트 후 코드 커버리지 리포트를 생성합니다.

리포트 설정

  jacocoTestReport {
    reports {
        xml.required.set(true)
        csv.required.set(false)
        html.required.set(true)
    }
    finalizedBy 'jacocoTestCoverageVerification'
}
  

테스트 후 생성될 코드 커버리지 리포트의 설정을 정의합니다.

reports 블록은 생성할 리포트의 형식을 지정합니다. 사용 가능한 형식은 XML, CSV, HTML이 있습니다.

- `xml.required.set(true)`: XML 형식의 리포트를 생성합니다.
- `csv.required.set(false)`: CSV 형식의 리포트는 생성하지 않습니다.
- `html.required.set(true)`: HTML 형식의 리포트를 생성합니다.

XML형식이 필요한 이유는 Github Action에서 PR 코멘트를 생성할 때 사용되기 때문입니다. HTML 형식은 테스트 실행 후, 개발자가 직접 커버리지를 확인하기 용이하기에 설정했습니다.

마지막으로 finalizedBy 'jacocoTestCoverageVerification'를 통해 jacocoTestReport 작업이 완료된 후 jacocoTestCoverageVerification 작업이 자동으로 실행되도록 설정합니다. 이 작업은 코드 커버리지 기준을 검증합니다.

커버리지 기준 검증 설정

  jacocoTestCoverageVerification {
    violationRules {
        rule {
            limit {
                counter = 'LINE'
                value = 'COVEREDRATIO'
                minimum = 0.80
            }
        }
    }
}
  

코드 커버리지 기준을 검증하는 작업의 설정을 정의합니다.

  • counter = 'LINE': 라인 커버리지를 기준으로 설정합니다.
  • value = 'COVEREDRATIO': 커버리지 비율을 기준으로 설정합니다.
  • minimum = 0.80: 최소 커버리지 비율을 80%로 설정합니다. 즉, 전체 코드의 80% 이상이 커버리지 되어야 합니다.

브랜치 커버리지와 라인 커버리지 중 라인 커버리지를 선택했습니다. 아무래도 라인 커버리지 자체가 명확하고 이해하기 쉽기 때문입니다. 또한 최소 커버리지의 비율을 80%로 선택했는데요, 80% 정도면 대부분이 테스트되고 있어 코드 품질에 대한 높은 신뢰성을 제공할 수 있다고 판단했습니다.

JaCoCo 실행

테스트 실행 후, JaCoCo를 통해 테스트 커버리지가 측정되었음을 확인할 수 있습니다.

리포트는 build/reports/jacoco/test 디렉터리에 생성되게 됩니다.

html 디렉터리 내에 index.html 파일을 확인하면, 각 패키지 별 테스트 커버리지 현황을 확인할 수 있습니다.


Github Action를 이용한 CI pipeline 구축

Jacoco Report

Github Actions App - Jacoco Report란?

앞선 gradle 설정의 Jacoco Profile에 의해 build 시점에 Test가 실행되고, 그 결과가 XML 형식으로 생성됩니다. 그리고 Jacoco Report가 XML 파일 형식의 내용을 적절한 내용으로 변환하여 PR에 코멘트를 생성해주는 Github Action 라이브러리입니다.

실제 적용 사항

백문이 불여일견이라고 하죠. 직접 workflow 적용 코드를 보시는 것이 이해가 빠르겠습니다.

아래는 실제 적용 코드입니다.

  name: BE/CI - Test Coverage 검증

on:
	workflow_dispatch:
  pull_request:
    types: [opened, ready_for_review]
    branches:
      - be/develop

jobs:
  test-coverage-pr-opened:
    defaults:
      run:
        working-directory: ./backend
    if: startsWith(github.head_ref, 'be-')
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

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

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v3
        with:
          cache-write-only: true

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Run tests and generate coverage report
        env:
          JAVA_HOME: ${{ steps.setup-java.outputs.java-home }}
        run: ./gradlew test jacocoTestReport
        continue-on-error: true

      - name: Verify test coverage
        env:
          JAVA_HOME: ${{ steps.setup-java.outputs.java-home }}
        run: ./gradlew jacocoTestCoverageVerification
        continue-on-error: true

      - name: 테스트 커버리지를 PR에 코멘트로 등록
        uses: madrapps/jacoco-report@v1.6.1
        with:
          title: 📌 Test Coverage Report
          paths: ${{ github.workspace }}/**/build/reports/jacoco/test/jacocoTestReport.xml
          token: ${{ secrets.GITHUB_TOKEN }}
          min-coverage-overall: 80
          min-coverage-changed-files: 80
  

실행 조건

  on:
	workflow_dispatch:
  pull_request:
    types: [opened, ready_for_review]
    branches:
      - be/develop
  

저희 팀은 gitflow와 비슷한 흐름의 branch 전략을 채택하고 있어 develop branch로 향하는 PR이 발생할 때마다 해당 코드의 테스트 커버리지 레포트가 생성되게 했습니다.

그리고 한가지 특이(?)한 점이 있다면 ready_for_review event를 사용하는 것인데요.

팀 repo는 개발 과정이 자동화되어있어 Issue를 (발행 + 할당)하면 해당 Issue에 관련된 branch가 자동으로 생성되고, 해당 Issue와 연결된 Draft PR도 함께 생성이 됩니다.

따라서 Draft PR을 통해 push된 작업 내용을 지속적으로 확인할 수 있습니다. 그리고 이런 플로우를 따라간다면 담당자가 구현을 완료했을 때 Github 자체 기능인 Ready For Review 버튼을 누를 수 있고, 이 시점의 코드에 대해서 Test Report가 생성됩니다.

이렇게 했을 때, 지속적인 팔로우를 유지하면서 불필요한 CI 과정을 줄일 수 있기에 꽤나 마음에 드는 pipeline이 구축되었습니다.

이 글은 Jacoco에 대해서 집중적으로 다루고자하니, Github runner의 build 환경을 구축하는 로직은 생략하겠습니다. 요 부분은 다른 포스팅에서 설명하고자 합니다.

Test build 및 실행

아래는 gradle profile을 통한 Test를 실행하는 step들입니다.

  - name: Run tests and generate coverage report
  env:
    JAVA_HOME: ${{ steps.setup-java.outputs.java-home }}
  run: ./gradlew test jacocoTestReport
  continue-on-error: true

- name: Verify test coverage
  env:
    JAVA_HOME: ${{ steps.setup-java.outputs.java-home }}
  run: ./gradlew jacocoTestCoverageVerification
  continue-on-error: true
  

여기서 계속 Actions가 실패하면서 레포트가 생성되지 않던 문제가 있었습니다. 앞서 설명한 저희 Code Coverage 통과 기준은 80% 였습니다. Jacoco 또한 실제로 Gradle로 Build된 Test 중 하나입니다. 그렇기에 저희가 정해놓은 기준을 넘지 못하면 Test가 통과되지 못했고 결과적으로 Build가 실패되었다고 결과가 발생했습니다. 여기서 Build 실패로 runner가 프로세스를 exit하는 바람에 Test 결과 리포트 과정으로 넘어가지 못했습니다.

저희는 테스트 커버리지를 확인하기 위해 Jacoco를 사용하고 있으므로 80%를 넘지 못해도 레포트를 확인할 수 있어야합니다.

그래서

  continue-on-error: true
  

키워드를 삽입하므로써 기준 미달로 Build 실패로 간주되어도 다음 과정으로 넘어갈 수 있게 했습니다.

다만 이 방법을 쓴다면 실제로 Build 자체가 되지 않는 상황에서도 실패를 알리지 못하지 않을까!? 라는 생각이 들었지만, 실제 Build가 실패한 경우에도 Report를 통해 그 여부를 알 수 있으므로 문제가 되지않았습니다.

자 이제 Test Report가 생성되었으니 PR에 Comment로 그 결과를 남겨주도록 합시다.

  - name: 테스트 커버리지를 PR에 코멘트로 등록
  uses: madrapps/jacoco-report@v1.6.1
  with:
    title: 📌 Test Coverage Report
    paths: ${{ github.workspace }}/**/build/reports/jacoco/test/jacocoTestReport.xml
    token: ${{ secrets.GITHUB_TOKEN }}
    min-coverage-overall: 80 # 프로젝트 전체의 test coverage
    min-coverage-changed-files: 80 # PR 기준 commit된 코드 coverage
  

앞서 build.gradle 파일에 추가한 JaCoCo Report가 저장된 디렉토리와 Report XML 파일을 path로 지정해주면, 해당 XML 파일을 보기 좋은 형식으로 변환하여 PR comment로 등록해줍니다.

결과

기본적으로 File Change가 발생한 코드에서의 Coverage를 검증하며, Coverage가 위의 workflow에 명시해둔 기준을 넘지 못하면 X 표시가 되어있는 것을 알 수 있습니다. 또한, change 전/후로 커버리지가 감소하였을 때 - 로 몇 퍼센트나 감소하였는지도 알려줍니다.

(누군진 몰라도 PR의 주인이 Evaluation 클래스의 code양을 2배나 늘렸으면서 테스트 코드는 하나도 작성하지 않았나 봅니다)

마무리

이를 통해 저희 팀 크루루는 새로 작성된 클래스들에 대하여 테스트 커버리지를 자동으로 확인할 수 있게되었습니다. Code coverage 테스트 결과를 merge에 선행되어야하는 필수 조건으로 두자는 의견도 나왔었지만, 스프린트의 호흡이 빠른 것을 감안하여 신속한 개발 프로세스를 위해 참고용으로만 사용하게 되었습니다.

다음 포스팅으로는 CI 과정에서 사용된 Java Code Formatter에 대하여 작성하고자 합니다. 다음 글에서 뵙겠습니다 ; )