CI/CD 파이프라인은 코드베이스 전반의 품질 관리, 안정성 확보 및 오류 최소화를 위해 빌드, 테스트, 배포 과정을 자동화하는 중요한 과정입니다. 프로젝트나 조직의 규모가 커질수록 이러한 자동화된 과정은 안정적인 운영을 위해 필수적입니다. 소프트웨어 개발 주기를 효율적으로 관리함으로써 비용을 크게 절감할 수 있습니다. 특히, 지속적인 통합(Continuous Integration)과 지속적인 배포(Continuous Deployment)를 통해 개발 속도를 유지하면서도 높은 품질의 소프트웨어를 제공할 수 있습니다. 이번 글에서는 CI/CD의 기본 개념부터 시작하여, Spring Boot 프로젝트에서 SonarCloud, JaCoCo, Checkstyle, Codecov와 같은 도구들을 활용해 안정적이고 효과적인 CI 파이프라인을 구축하는 과정을 상세히 다루고자 합니다. CI 파이프라인을 통해 프로젝트 또는 조직의 특성에 맞게 코드 품질을 체계적으로 관리하고, 잠재적인 버그와 보안 취약점을 사전에 예방할 수 있습니다.
0. 프로젝트 환경
- Spring Boot 3.3.4
- Gradle 8.11.1 (Kotlin)
- GitHub Actions
1. CI/CD 파이프라인
1-1. 소프트웨어 배포 방식의 변화
소프트웨어 배포는 물리적인 서버에 애플리케이션을 직접 배포하던 전통적인 방식에서 시작하여, 물리적 서버를 가상 머신(Virtual Machine)으로 분할하여 애플리케이션을 배포하는 가상화 배포를 거쳐, 애플리케이션과 그 종속성을 컨테이너(Container)에 패키징하여 배포하는 컨테이너 기반 배포 환경으로 발전했습니다.
컨테이너 기반 배포 환경으로 발전하면서 애플리케이션 배포 주기는 크게 단축되었습니다. 컨테이너는 가상화 기술과 유사하지만 애플리케이션 간에 운영체제(OS)를 공유하여, 가상 머신처럼 독립된 환경을 제공함과 동시에 경량화된 환경을 제공합니다. 이러한 특성은 애플리케이션 실행 환경을 표준화하고, 서버 간 마이그레이션 및 배포를 보다 빠르고 효율적으로 처리할 수 있도록 합니다. 또한, 컨테이너는 종속성을 함께 패키징하여 기존 배포 방식에서 발생하는 환경에 따른 오류를 최소화합니다.
1-2. 배포 프로세스의 변화
전통적인 배포 프로세스
과거 전통적인 소프트웨어에서는 다음 절차에 따라 개발 및 배포가 진행되었습니다.
- 각 개발 팀은 서로 다른 코드 저장소를 유지 관리합니다.
- 빌드 및 통합 팀은 코드 저장소로부터 코드를 통합하고, 이를 패키지로 컴파일합니다.
- 배포에 필요한 지침이 포함된 패키지를 운영팀으로 전송합니다.
- 운영 팀은 패키지를 테스트 환경에 적용하고, 테스트 팀은 이를 검증합니다.
- 모든 검증이 완료되면 애플리케이션을 프로덕션 환경에 배포합니다.
위 절차는 다음과 같은 문제점을 가집니다.
- 코드 통합, 빌드, 테스트, 피드백, 수정, 재검증 과정을 거치며 배포까지 오랜 시간이 소요됩니다.
- 빌드, 테스트, 배포 과정에서 수작업으로 이루어지는 작업이 많아 휴먼 에러가 발생할 가능성이 높습니다.
- 개발 환경과 테스트 환경, 프로덕션 환경 간의 차이로 인해 예기치 못한 오류가 발생할 수 있습니다.
- 테스트 팀에서 개발 팀으로 문제를 알리고 이를 수정한 뒤 다시 검증하는 과정이 반복되며, 의사소통 및 처리 과정이 비효율적으로 작동합니다.
CI/CD 배포 프로세스
전통적인 배포 프로세스에서 발생하는 문제를 해결하기 위해 자동화된 CI/CD 파이프라인이 도입되었습니다. 위 그림에서 지속적인 통합(Continuous Integration)은 Build Pipeline을, 지속적인 배포(Continuous Delivery or Deployment)는 Release Pipeline을 의미합니다.
CI/CD 배포 프로세스는 다음 절차에 따라 개발 및 배포가 진행됩니다.
- 개발자는 공유 저장소에서 코드를 관리하며, 변경 사항을 지속적으로 통합합니다.
- 통합된 코드는 컴파일 과정을 거칩니다.
- 컴파일된 코드는 자동화된 단위 테스트(Unit Testing)와 UI 테스트(UI Testing)를 통해 품질을 검증합니다.
- 운영 팀은 테스트 환경으로 이동하기 위한 자동화된 스크립트를 작성하고 관리합니다.
- 자동화된 테스트 환경에서 소프트웨어의 기능과 안정성을 확인합니다.
- 테스트를 통과하면 승인 과정을 거칩니다.
- 승인된 소프트웨어는 프로덕션 환경에 배포됩니다.
위 절차는 기존 배포 프로세스와 비교해 다음의 이점을 가집니다.
- 코드 통합 및 배포 과정을 자동화함으로써 개발과 배포 주기가 크게 단축됩니다.
- 반복 작업을 자동화하여 수작업으로 인한 오류 발생 가능성을 최소화합니다.
- 자동화된 테스트와 검증 과정을 통해 코드 품질을 일관되게 유지합니다.
- 코드 변경에 대한 테스트 결과와 피드백이 신속하게 제공되어, 개발자와 운영 팀 간의 협업이 원활해집니다.
1-3. 컨테이너 환경에서의 배포 프로세스
컨테이너 환경에서는 기존 배포 방식과 비교하여 컨테이너 이미지 생성 및 이미지 레지스트리 관리가 추가됩니다.
항목 | 전통적인 배포 방식 | CI/CD 파이프라인 |
자동화 여부 | 전담 인력에 의한 수작업 | CI/CD 도구를 활용한 자동 빌드 및 자동 배포 |
패키징 방식 | 배포 담당자가 수작업으로 취합하여 정리 | 형상 관리를 통한 자동 취합, 컨테이너 이미지로 패키징 후 이미지 레지스트리에 업로드 |
배포 시간 | 수십 분 소요 | 수 분 이내 완료 |
무중단 배포 | 배포 담당자가 수작업으로 진행 | 블루-그린 배포, 롤링 업데이트 등 자동화된 무중단 배포 지원 |
롤백 | 일부 파일 교체 및 반자동화된 롤백 | 기존 운영 버전의 컨테이너 이미지로 즉시 롤백 |
휴먼 에러 | 발생 가능성 존재 | 자동화된 프로세스로 휴먼 에러 가능성 최소화 |
배포 검증 | 테스트 및 검증 단계의 자동화 어려움 | 자동화된 테스트 케이스, 정적 분석, 배포 후 헬스 체크 적용 |
2. CI 파이프라인 구축하기
GitHub와의 통합을 위해 GitHub Actions를 기반으로 CI 파이프라인을 작성합니다. GitHub Actions는 퍼블릭 리포지토리에서 무료로 제공되며, 프라이빗 리포지토리의 경우 무료 플랜 기준 월 500 MB의 스토리지와 2000분의 실행시간을 제공합니다.
CI 파이프라인에 사용되는 도구는 다음과 같습니다.
- SonarCloud: 코드의 품질과 보안 취약점을 분석하고 보고합니다.
- Checkstyle: 코드 스타일 규칙을 자동으로 검사하여 일관성을 유지합니다.
- JaCoCo: 테스트 커버리지를 측정하며, 커버리지 기준을 충족하지 못할 경우 빌드를 실패하도록 설정합니다.
- Codecov: 커버리지 리포트를 업로드하고, PR에 커버리지 정보를 자동으로 표시하여 코드 품질을 시각화하고 모니터링합니다.
2-1. SonarQube vs SonarCloud
SonarQube는 온프레미스 환경에서 동작하는 정적 코드 분석 도구로, 코드의 품질과 보안 취약점을 분석하여 유지보수성, 신뢰성, 보안성을 높이는 데 중점을 둡니다. 코드 품질 관리, 테스트 커버리지 분석, 코드 스타일 검사 등 다양한 기능을 제공하며, SonarQube for IDE 플러그인을 통해 개발자가 IDE에서 커밋 전에 즉각적인 피드백을 받을 수 있습니다.
SonarCloud는 SonarQube의 클라우드 기반 SaaS 버전으로, GitHub, GitLab, Bitbucket, Azure DevOps와 통합하여 CI/CD 파이프라인에서 코드 품질을 자동으로 검사합니다. 30개 이상의 언어와 프레임워크, IaC(Infrastructure as Code) 플랫폼을 지원하며, 클라우드 상에서 쉽게 설정할 수 있어 초기 설정과 유지보수가 간편합니다.
단, SonarQube와 달리 SonarCloud는 기본적으로 테스트 커버리지 측정과 코드 스타일 검사 기능을 제공하지 않으므로, 사용자가 추가적으로 설정해야 합니다. 이 글에서는 JaCoCo를 통해 테스트 커버리지를 측정하고, Checkstyle을 사용하여 코드 스타일 검사를 수행함으로써 SonarCloud의 부족한 기능을 보완합니다.
또한, SonarCloud는 퍼블릭 리포지토리에서는 무료로 제공되며, 프라이빗 리포지토리의 경우 사용량에 따라 요금이 부과됩니다.
SonarCloud Supported language versions
항목 | SonarQube | SonarCloud |
운영 환경 | 온프레미스 | 클라우드 기반 SaaS |
초기 설정 | 서버 설치 및 구성 필요 | 클라우드 계정 생성 및 빠른 설정 가능 |
유지보수 | 서버 관리 및 업데이트 필요 | 유지보수 필요 없음 (Sonar에서 관리) |
통합 | 온프레미스 DevOps 플랫폼과 통합 | GitHub, GitLab, Bitbucket, Azure DevOps와 통합 |
테스트 커버리지 | 기본 제공 | 별도 설정 필요 |
코드 스타일 검사 | 기본 제공 | 별도 설정 필요 |
지원 언어 및 플랫폼 | 29개 이상의 언어 및 프레임워크 지원 | 30개의 언어 및 프레임워크 지원 |
비용 | 서버 및 라이선스 비용 발생 | 클라우드 구독 모델로 사용한 만큼 비용 발생 |
확장성 | 서버 확장 필요 | 클라우드 확장 지원 (무제한 저장소 및 프로젝트 가능) |
2-2. GitHub 저장소와 SonarCloud 연동하기
GitHub 저장소와 SonarCloud를 연동하려면 SonarCloud에 로그인해야 합니다. SonarCloud SignUp 페이지에 접속한 후, GitHub 계정을 사용하여 로그인합니다.
SonarCloud를 이용하려면 GitHub에 대한 접근 권한을 허용해야 합니다. `Authorize SonarQubeCloud` 버튼을 클릭하여 권한을 부여합니다.
SonarCloud에서 관리할 저장소를 가져오기 위해 `Import an organization` 버튼을 클릭합니다.
관리할 조직과 저장소를 선택하여 SonarCloud와 연결합니다. 조직 내 모든 저장소를 연결하려면 `All repositories` 옵션을, 특정 저장소만 연결하려면 `Only select repositories` 옵션을 선택합니다. 저장소 선택을 완료한 뒤, `Install` 버튼을 클릭합니다.
- Name: SonarCloud에서 조직을 식별하는 이름입니다. GitHub 조직 이름이 기본값으로 설정되며, 최대 255자까지 입력할 수 있습니다.
- Key: 조직의 고유 식별자로, GitHub 조직 이름이 기본값으로 설정됩니다. 소문자 또는 숫자로 시작해야 하며, 하이픈(`-`)을 포함할 수 있고, 최대 255자까지 입력 가능합니다. 이 키는 API 호출 및 내부 식별에 사용됩니다.
SonarCloud 요금제를 선택합니다. 퍼블릭 리포지토리는 무료로 제공되며, 프라이빗 리포지토리는 프로젝트 규모에 따라 적합한 요금제를 선택해야 합니다. 요금제를 선택한 후, `Create Organization` 버튼을 클릭하여 조직 연결을 완료합니다.
`Analyze a new project` 버튼을 클릭하여 연결된 저장소를 선택하는 페이지로 이동합니다.
SonarCloud에서 관리할 연결된 저장소를 선택한 뒤, `Set Up` 버튼을 클릭합니다.
`Clean as Your Code` 방법론을 설정합니다. 새로운 코드 정의는 프로젝트에서 어떤 부분이 "새로운 코드"로 간주될지를 결정하며, 최근 변경사항에 초점을 맞추고 코드 품질을 점진적으로 개선할 수 있도록 돕습니다.
- Previous version
- 이전 버전과 비교하여 변경된 모든 코드를 새로운 코드로 간주합니다.
- 주기적으로 릴리스나 버전을 관리하는 프로젝트에 적합합니다.
- Number of days
- 최근 X일 이내에 변경된 코드를 새로운 코드로 간주합니다.
- 일정 기간 이후에도 수정되지 않은 문제는 기존 코드로 전환됩니다.
- 지속적인 배포(Continuous Delivery)를 사용하는 프로젝트에 적합합니다.
설정을 완료한 후, `Create project` 버튼을 클릭하여 프로젝트를 생성합니다.
연결이 완료되면 `Installed GitHub Apps`에 `SonarQubeCloud`가 표시됩니다.
SonarCloud를 GitHub Actions와 원활하게 연동하려면 `Administration` > `Analysis Method`로 이동하여, `Automatic Analysis` 설정을 비활성화해야 합니다. 자동 분석(Auto Analysis)이 활성화된 상태에서는 GitHub Actions와의 충돌로 인해 분석 결과가 올바르게 처리되지 않을 수 있습니다. 따라서, 자동 분석을 비활성화하고 CI 기반 분석(CI-based analysis)을 사용하여 정적 코드 분석을 진행하도록 설정해야 합니다.
참고: SonarQube Cloud Automatic Analysis
2-3. SonarCloud에 Checkstyle 연동하기
SonarCloud는 기본적으로 지원하지 않는 코드 스타일 규칙을 개발자가 직접 지정하여 확장할 수 있도록 프로퍼티 설정 기능을 제공합니다. 이를 통해 프로젝트별로 필요에 따라 코드 스타일 규칙을 세부적으로 정의하고 팀의 컨벤션에 맞는 스타일을 유지할 수 있습니다. 현재 SonarCloud에서 공식적으로 지원하는 코드 스타일 플러그인은 Apache에서 관리하는 Checkstyle입니다.
Checkstyle은 자바 프로젝트에서 코드 스타일과 규칙을 검증하기 위한 도구로, 코드의 가독성과 유지보수성을 높이기 위해 사용됩니다. 이를 활용하여 특정 프로젝트나 팀에서 요구하는 컨벤션을 코드 작성 단계에서 강제할 수 있으며, 규칙 위반 시 상세한 리포트를 제공하여 문제를 신속히 해결할 수 있습니다.
주로 사용되는 코드 스타일 가이드
적용한 코드 스타일 및 변경 사항
이 글에서는 Google Java Style Guide(IntelliJ)를 기반으로 하여 일부 규칙을 수정한 코드 스타일을 적용했습니다.
저장소: spring-boot-ci-configs > config > checkstyle
변경 사항
- 블럭 들여쓰기: 2칸에서 4칸으로 변경되었습니다.
- 열 제한: 100자에서 120자로 변경되었습니다.
- 들여쓰기 지속: 줄바꿈 시 들여쓰기를 4칸에서 8칸으로 변경하였습니다.
- 테스트 코드의 메소드명 검사 제외: 테스트 코드에서는 메소드명을 검사하지 않도록 설정하여 한글 메소드명을 허용하였습니다.
- 카멜 케이스 규칙 완화: 테스트 코드에서는 연속된 대문자를 허용하도록 설정하였습니다.
2-3-1. Gradle에 Checkstyle 플러그인 추가하기
이 글은 Gradle Kotlin DSL을 기준으로 진행됩니다. Groovy 문법을 사용하는 경우에도 설정 방법은 동일하며, 문법적인 차이만 존재합니다. Groovy 문법 사용자는 아래 공식 문서를 참고하여 문법 차이를 확인하시기 바랍니다.
참고: Gradle 8.11.1 | The Checkstyle Plugin
Checkstyle을 적용하려면 `build.gradle.kts` 파일에 플러그인을 추가해야 합니다.
build.gradle.kts
plugins {
id("checkstyle") // Checkstyle 플러그인 추가
}
또는
plugins {
checkstyle // Checkstyle 플러그인 추가
}
2-3-2. Checkstyle 파일 설정
기본적으로 Checkstyle 플러그인은 설정 파일이 프로젝트 루트 디렉토리에 위치하도록 설정됩니다. 그러나, 이 글에서는 설정 파일을 효율적으로 관리하기 위해 `{project_root}/config/checkstyle` 디렉토리에 배치합니다.
<root>
└── config
└── checkstyle
└── checkstyle.xml
└── checkstyle-suppressions.xml
`checkstyle.xml`: Checkstyle의 주요 코드 스타일 규칙을 정의하는 파일입니다.
`checkstyle-suppressions.xml`: 특정 코드에 대해 규칙을 예외 처리하는 설정 파일입니다.
2-3-3. Checkstyle 의존성 추가 및 태스크 설정
Checkstyle을 사용하려면 플러그인뿐만 아니라 의존성 설정도 필요합니다. 이 글을 작성하는 시점에서 Checkstyle의 최신 버전은 `10.21.1`이지만, 버전에 따라 문법 차이로 오류가 발생할 가능성이 있으므로 안정성을 위해 `10.20.2` 버전으로 진행합니다.
build.gradle.kts
checkstyle {
toolVersion = "10.20.2" // Checkstyle 버전 지정
configFile = file("${rootProject.projectDir}/config/checkstyle/checkstyle.xml")
configProperties["suppressionsFile"] =
file("${rootProject.projectDir}/config/checkstyle/checkstyle-suppressions.xml")
}
`toolVersion`
- Checkstyle 버전을 지정합니다.
- 최신 버전: Maven Repository > Checkstyle
`configFile`
- Checkstyle 설정 파일의 경로를 지정합니다.
- 이 글에서는 `{project_root}/config/checkstyle/checkstyle.xml`에 설정 파일을 배치하므로, 해당 경로를 작성합니다.
`configProperties["suppressionsFile"]`
- Checkstyle 규칙에 대한 예외를 정의하는 파일 경로를 지정합니다.
- 예외 설정 파일(`checkstyle-suppressions.xml`)이 없는 경우 이 설정을 제거할 수 있습니다.
build.gradle.kts
tasks.withType<Checkstyle>().configureEach {
reports {
xml.required.set(true) // XML 형식의 리포트 생성
html.required.set(true) // HTML 형식의 리포트 생성
}
}
`tasks.withType<Checkstyle>().configureEach`
- 프로젝트의 모든 모듈에 적용되는 Checkstyle 태스크 설정을 지정합니다.
`reports`
- Checkstyle 검사 결과에 따른 리포트 형식을 설정합니다.
- `xml.required.set(true)`: XML 형식의 리포트를 생성합니다. SonarCloud는 XML 리포트를 지원하므로 필수적으로 활성화해야 합니다.
- `html.required.set(true)`: HTML 형식의 리포트를 생성하여, 개발자가 코드 스타일 위반 부분을 쉽게 확인할 수 있도록 합니다.
2-3-4. SonarCloud에 Checkstyle 검사 결과 경로 설정하기
SonarCloud에서 Checkstyle 검사 결과를 분석하려면 Gradle에서 SonarCloud와 연결하고, Checkstyle 리포트 경로를 설정해야 합니다.
build.gradle.kts
plugins {
id("org.sonarqube") version "6.0.1.5171" // SonarCloud 플러그인 추가
}
sonar {
properties {
property("sonar.host.url", "https://sonarcloud.io") // SonarCloud URL
property("sonar.organization", "{organization_name}") // SonarCloud 조직 이름
property("sonar.projectKey", "{project_key}") // 프로젝트 키
property("sonar.java.checkstyle.reportPaths", "build/reports/checkstyle/*.xml") // Checkstyle 리포트 경로
}
}
`plugins`
- SonarCloud와의 통합을 위해 SonarQube Gradle 플러그인을 추가합니다.
- 이 글에서는 최신 버전인 `6.0.1.5171`을 사용합니다.
- 최신 버전: Gradle Plugins > org.sonarqube
`sonar.host.url`
- SonarCloud 서버 URL을 지정합니다.
- 기본값은 `https://sonarcloud.io`입니다.
`sonar.organization`
- SonarCloud에서 사용할 조직 이름을 입력합니다.
- SonarCloud 대시보드에서 생성된 조직 이름과 동일해야 합니다.
`sonar.projectKey`
- SonarCloud 프로젝트의 고유 식별자를 지정합니다.
- SonarCloud에서 프로젝트를 생성할 때 제공된 `Project Key`를 사용합니다.
- SonarQube Cloud 페이지의 `{organization} > {project} > Administration > Update Key`에서 확인할 수 있습니다.
`sonar.java.checkstyle.reportPaths`
- Checkstyle 검사 결과 리포트의 경로를 지정합니다.
- 이 경로는 Gradle 태스크 실행 시 생성된 Checkstyle 리포트 파일의 위치와 일치해야 합니다.
- 리포트 파일의 기본 경로는 `build/reports/checkstyle/*.xml`입니다.
2-4. SonarCloud에 JaCoCo 연동하기
SonarCloud는 SonarQube와 달리 자체적으로 커버리지 보고서를 생성하지 않습니다. 대신, 타사 도구를 사용하여 빌드 프로세스 중에 커버리지 보고서를 생성해야 합니다. 생성된 보고서를 SonarScanner에 전달하여 SonarCloud로 전송하면, 다른 분석 메트릭과 함께 프로젝트 대시보드에 표시됩니다. 자바 프로젝트의 경우, SonarCloud는 JaCoCo 커버리지 도구를 공식적으로 지원합니다.
참고: SonarQube Cloud Java test coverage
2-4-1. Gradle에 JaCoCo 추가 및 리포트 설정하기
build.gradle.kts
plugins {
id("jacoco") // JaCoCo 플러그인 추가
}
jacoco {
toolVersion = "0.8.12" // JaCoCo 버전 지정
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required.set(true) // XML 형식의 리포트 생성
html.required.set(true) // HTML 형식의 리포트 생성
csv.required.set(false) // CSV 형식의 리포트 비활성화
}
}
`plugins`
- Gradle에 JaCoCo 플러그인을 추가하여 테스트 커버리지를 측정할 수 있도록 설정합니다.
`toolVersion`
- JaCoCo 버전을 지정합니다.
- 이 글에서는 최신 버전인 `0.8.12`를 사용합니다.
- 최신 버전: Maven Repository > JaCoCo :: Maven Plugin
`dependsOn(tasks.test)`
- JaCoCo 테스트 보고서를 생성하기 위해 테스트 태스크(`tasks.test`)가 반드시 선행되어야 함을 명시합니다.
- `tasks.test`: 프로젝트에서 작성된 테스트 코드를 실행하고, 테스트 결과를 생성합니다.
- `tasks.jacocoTestReport`: `test` 태스크 실행 결과로 생성된 데이터를 기반으로 테스트 커버리지 리포트를 생성합니다.
`reports`
- JaCoCo 테스트 보고서 형식을 설정합니다.
- `xml.required.set(true)`: XML 형식의 리포트를 생성합니다. SonarCloud는 XML 리포트를 지원하므로 필수적으로 활성화해야 합니다.
- `html.required.set(true)`: HTML 형식의 리포트를 생성하여, 개발자가 시작적으로 확인할 수 있도록 합니다.
- `csv.required.set(false)`: CSV 형식의 리포트를 비활성화합니다.
2-4-2. JaCoCo 테스트 커버리지 기준 설정하기
JaCoCo는 프로젝트에서 테스트 커버리지 기준을 설정하고, 이를 위반할 경우 빌드 실패로 처리할 수 있는 `violationRules` 기능을 제공합니다. 테스트 커버리지 기준은 `element`, `counter`, `value`로 설정 가능합니다.
`element`: 커버리지 체크를 적용할 코드 구조의 단위를 설정합니다.
- BUNDLE (default): 프로젝트 또는 모듈
- PACKAGE: 패키지
- CLASS: 클래스
- SOURCEFILE: 소스 파일
- METHOD: 메소드
`counter`: 커버리지를 측정할 기준(메트릭)을 설정합니다.
- LINE: 실행된 코드 라인 수
- BRANCH: 조건문의 분기 수
- CLASS: 테스트된 클래스 비율
- METHOD: 테스트된 메소드 비율
- INSTRUCTION (default): 자바 바이트코드 명령 수
- COMPLEXITY: 코드 복잡도 (JaCoCo Coverage Counters)
`value`
- TOTALCOUNT: 커버리지 측정 대상의 총 개수
- MISSEDCOUNT: 테스트되지 않은 항목의 개수
- COVEREDCOUNT: 테스트된 항목의 개수
- MISSEDRATIO: 테스트되지 않은 항목의 비율
- COVEREDRATIO (default): 테스트된 항목의 비율
다음은 JaCoCo 테스트 커버리지 검증 테스트(`jacocoTestCoverageVerification`) 설정 예시입니다.
build.gradle.kts
tasks.jacocoTestCoverageVerification {
dependsOn(tasks.jacocoTestReport)
violationRules {
rule {
limit {
minimum = "0.50".toBigDecimal()
}
}
rule {
isEnabled = true
element = "CLASS"
limit {
counter = "BRANCH"
value = "COVEREDRATIO"
minimum = "0.70".toBigDecimal()
}
limit {
counter = "LINE"
value = "COVEREDRATIO"
minimum = "0.50".toBigDecimal()
}
limit {
counter = "LINE"
value = "TOTALCOUNT"
maximum = "300".toBigDecimal()
}
excludes = listOf(
"com.example.**.Test*.*",
)
}
}
}
`dependsOn(tasks.jacocoTestReport)`
- `jacocoTestReport` 태스크 실행 후 `jacocoTestCoverageVerification` 태스크를 실행하도록 설정합니다.
`violationRules`
- 커버리지 기준 위반 규칙을 정의합니다.
기본 커버리지 규칙
- `limit { minimum = "0.50" }`: 프로젝트는 최소 50%의 커버지리 기준을 충족해야 합니다.
클래스 단위 규칙
- `isEnabled = true`: 규칙을 활성화합니다.
- `element = "CLASS"`: 클래스 단위로 커버리지를 검증합니다.
- 분기 기준 커버리지(`BRANCH COVEREDRATIO`): 조건문에서 최소 70% 이상의 커버리지가 있어야 합니다.
- 라인 기준 커버리지(`LINE COVEREDRATIO`): 코드 라인의 최소 50% 이상이 커버되어야 합니다.
- 라인 개수 기준(`LINE TOTALCOUNT`): 각 클래스는 최대 300 라인까지만 허용됩니다.
제외 설정
- `excludes`: 특정 클래스나 파일 패턴을 커버리지 검증에서 제외합니다.
주의사항
커버리지 비율
- 커버리지 비율은 소수점 자리수에 따라 계산 결과가 달라질 수 있습니다.
- 지정한 유효자리수 까지만 반영되며, 초과하는 경우 반올림됩니다.
- 예를 들어, 비율을 `0.4`로 설정한 경우, `0.43`은 `0.4`로 처리됩니다.
- 커버리지 수치는 `BigDecimal`로 강제됩니다.
제외 설정
- 패키지명, 클래스명, 확장자를 모두 명시해야 합니다.
2-4-3. 테스트와 리포트, 커버리지 기준 검증을 통합 실행하는 태스크 작성하기
build.gradle.kts
tasks.register("testCoverage") {
group = "verification"
description = "Runs the unit tests and generates a coverage report"
dependsOn(tasks.test)
dependsOn(tasks.jacocoTestReport)
dependsOn(tasks.jacocoTestCoverageVerification)
tasks["jacocoTestReport"].mustRunAfter(tasks["test"])
tasks["jacocoTestCoverageVerification"].mustRunAfter(tasks["jacocoTestReport"])
}
`tasks.register("testCoverage")`
- 사용자 정의 태스크 `testCoverage`를 생성합니다.
`group`
- 태스크의 카테고리를 지정합니다. Gradle 검증 관련 태스크 그룹 `verification`에 태스크를 추가합니다.
`description`
- 태스크의 설명을 설정하여 Gradle 태스크 목록에서 역할을 쉽게 파악할 수 있도록 합니다.
`dependsOn`
- 사용자 정의 태스크가 실행되기 전에 반드시 실행되어야 할 태스크를 지정합니다.
- `dependsOn(tasks.test)`: 단위 테스트 실행
- `dependsOn(tasks.jacocoTestReport)`: JaCoCo 테스트 커버리지 리포트 생성
- `dependsOn(tasks.jacocoTestCoverageVerification)`: 커버리지 기준 검증
`mustRunAfter`
- 태스크 실행 순서를 명시적으로 지정합니다.
- 실행 순서: `test` -> `jacocoTestReport` -> `jacocoTestCoverageVerification`
2-4-4. SonarCloud에 JaCoCo 커버리지 보고서 설정하기
SonarCloud는 자체적으로 커버리지 데이터를 생성하지 않으므로, JaCoCo에서 생성된 리포트를 SonarCloud에 전달해야 합니다. JaCoCo 커버리지 보고서를 읽어와 분석할 수 있도록 경로를 지정합니다.
build.gradle.kts
sonar {
properties {
property("sonar.java.coveragePlugin", "jacoco")
property("sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacoco/test/jacocoTestReport.xml")
}
}
`sonar.java.coveragePlugin`
- SonarCloud에서 사용할 커버리지 플러그인을 지정합니다.
- `jacoco`로 설정하여 JaCoCo에서 생성된 커버리지 데이터를 SonarCloud가 분석할 수 있도록 설정합니다.
`sonar.coverage.jacoco.xmlReportPaths`
- JaCoCo 리포트 파일의 경로를 지정합니다.
- 리포트 파일의 기본 경로는 `build/reports/jacoco/test/jacocoTestReport.xml`입니다.
2-5. GitHub Acitions CI Workflow 작성하기
CI 파이프라인은 빌드 테스트와 테스트 검증 및 코드 품질 확인 두 단계로 구성됩니다.
빌드 테스트 단계
- Gradle의 `assemble` 태스크를 실행하여 빌드 과정에서 문제가 없는지 확인합니다.
테스트 검증 및 코드 품질 확인 단계
- 애플리케이션의 유닛 테스트, 테스트 커버리지 측정, 코드 스타일 검사, JaCoCo 기준 검증을 실행합니다.
- SonarCloud와 CodeCov를 통해 분석 결과를 시각화하고, PR에 피드백을 제공합니다.
GitHub Actions의 파일 구조는 다음과 같습니다.
root
├── .github
│ ├── actions
│ │ └── setup-java-and-gradlew
│ │ └── action.yml
│ └── workflows
│ ├── build.yml
│ └── test-and-quality-analysis.yml
2-5-1. 공통 설정 액션(`action.yml`) 작성하기
자바 환경 설정과 Gradle Wrapper 실행 권한 부여 작업을 별도의 액션으로 분리하여 관리합니다.
name: "Setup Java and Gradlew"
description: "Sets up Java environment, restores JDK cache, and prepares Gradlew"
inputs:
java-distribution:
description: "Java distribution to set up"
required: true
type: string
java-version:
description: "Java version to set up (8, 11, 16, 17, 21)"
required: true
type: string
java-package:
description: "Java package type (jdk, jre, jdk+fx, jre+fx)"
required: false
default: "jdk"
type: string
outputs: { }
runs:
using: "composite"
steps:
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: ${{ inputs.java-distribution }}
java-version: ${{ inputs.java-version }}
java-package: ${{ inputs.java-package }}
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
shell: bash
`inputs`
- 매개변수 기반의 유연한 설정을 지원합니다.
- 각 매개변수로 전달 가능한 값은 actions/setup-java 공식 문서에서 확인할 수 있습니다.
- `java-distribution`: 설정할 자바 배포판을 지정합니다.
- `java-version`: 자바 버전을 설정합니다.
- `java-package`: 패키지 유형을 지정합니다.
`cache: gradle`
- Gradle 패키지 종속성을 캐싱하여 워크플로우 실행 시간을 단축합니다.
- 기본적으로 캐싱되는 파일 및 디렉토리는 다음과 같습니다.
- `**/*.gradle*`
- `**/gradle-wrapper.properties`
- `buildSrc/**/Versions.kt`
- `buildSrc/**/Dependencies.kt`
- `gradle/*.versions.toml`
- `**/versions.properties`
- 추가로 캐싱이 필요한 파일이 있다면 actions/cache 액션을 활용하여 설정할 수 있습니다.
`chmod +x gradlew`
- Gradle Wrapper 파일(`gradlew`)에 실행 권한을 부여하여 CI 환경에서 정상적으로 실행될 수 있도록 설정합니다.
- Gradle의 버전을 명시적으로 지정하려면 gradle/actions 액션을 사용할 수 있지만, 프로젝트의 Gradle 버전과 일관성을 유지하기 위해 Gradle Wrapper를 사용하는 것을 권장합니다.
2-5-2. 빌드 테스트 워크플로우(`build.yml`) 작성하기
Gradle의 `assemble` 태스크를 통해 빌드 테스트를 수행합니다. 이를 통해 코드가 정상적으로 컴파일되고 의존성 문제가 없는지 확인합니다.
name: Build Spring Boot with Gradle
on:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Java and Gradlew
uses: ./.github/actions/setup-java-and-gradlew
with:
java-distribution: 'liberica'
java-version: '21'
java-package: 'jdk'
- name: Write Application Configuration
run: |
mkdir -p src/main/resources
echo "${{ secrets.APPLICATION_YAML }}" | base64 --decode > src/main/resources/application.yml
- name: Build with Gradle
run: ./gradlew assemble --info --parallel
`Checkout code`
- 리포지토리의 코드를 클론하여 GitHub Actions 러너에서 소스 코드를 사용할 수 있도록 준비합니다.
`Setup Java and Gradlew`
- Liberica JDK 21 버전과 Gradle Wrapper를 사용하여 프로젝트를 빌드할 수 있는 환경으로 만듭니다.
`Write Application Configuration`
- 애플리케이션 실행에 필요한 `application.yml` 파일을 생성합니다.
- `Settings > Secrets and variables > Actions > Repository secrets`에 `APPLICATION_YAML` 시크릿을 등록해야 합니다.
- YAML 파일 내부의 특수 문자로 인해 파일 생성 오류가 발생하지 않도록, 시크릿 값을 `base64`로 인코딩하여 저장합니다.
- 프로젝트에 이미 프로파일 파일이 업로드되어 있는 경우 생략 가능합니다.
`Build with Gradle`
- Gradle의 `assemble` 태스크를 실행하여 프로젝트를 빌드합니다.
- 애플리케이션이 정상적으로 빌드되는지 확인하여, 의존성 문제와 빌드 오류를 사전에 방지합니다.
2-5-3. 테스트 검증 및 코드 품질 확인 워크플로우(`test-and-quality-analysis.yml`) 작성하기
코드의 테스트 검증 및 코드 품질 측정을 자동화합니다. Gradle 태스크를 활용해 유닛 테스트와 커버리지 측정을 수행하며, Checkstyle, JaCoCo, SonarCloud, 그리고 Codecov와 같은 도구를 통해 코드 품질을 검증하고 시각화합니다.
name: Test and Quality Analysis
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
permissions:
pull-requests: write
contents: read
actions: read
checks: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Java and Gradlew
uses: ./.github/actions/setup-java-and-gradlew
with:
java-distribution: 'liberica'
java-version: '21'
java-package: 'jdk'
- name: Run tests with Gradle
run: ./gradlew test jacocoTestReport --info --parallel
env:
SPRING_PROFILES_ACTIVE: test
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: 'build/reports/tests/test/*'
- name: Upload HTML Coverage Report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-html-report
path: 'build/reports/jacoco/test/html'
- name: Upload XML Coverage Report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-xml-report
path: 'build/reports/jacoco/test/jacocoTestReport.xml'
- name: Upload coverage exec data
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-exec-data
path: 'build/jacoco/test.exec'
- name: Test Reporter
id: reporter
uses: dorny/test-reporter@v1
if: always()
with:
name: Spring Boot Tests
path: 'build/test-results/test/*.xml'
reporter: java-junit
only-summary: false
list-suites: all
list-tests: all
max-annotations: 50
fail-on-error: true
fail-on-empty: true
- name: Compute Metrics
if: always()
run: |
total=$(( ${PASSED:-0} + ${FAILED:-0} + ${SKIPPED:-0} ))
time_in_seconds=$(echo "scale=2; ${TIME_MS:-0} / 1000" | bc)
echo "total=$total" >> $GITHUB_ENV
echo "time_in_seconds=$time_in_seconds" >> $GITHUB_ENV
env:
PASSED: ${{ steps.reporter.outputs.passed }}
FAILED: ${{ steps.reporter.outputs.failed }}
SKIPPED: ${{ steps.reporter.outputs.skipped }}
TIME_MS: ${{ steps.reporter.outputs.time }}
- name: Post Test Results to PR
uses: marocchino/sticky-pull-request-comment@v2
if: always()
with:
header: Test Results
recreate: true
message: |
## 🛠️ Test Summary (${{ steps.reporter.outputs.conclusion }})
📄 **[View Detailed Test Logs](${{ steps.reporter.outputs.url_html }})**
| **Metrics** | **Test Result Details** |
|-----------------------|---------------------------------------|
| **Total Tests** | ${{ env.total }} |
| ✅ **Tests Passed** | ${{ steps.reporter.outputs.passed }} |
| ❌ **Tests Failed** | ${{ steps.reporter.outputs.failed }} |
| ⚠️ **Tests Skipped** | ${{ steps.reporter.outputs.skipped }} |
| ⏱️ **Execution Time** | ${{ env.time_in_seconds }}s |
code_quality_analysis:
runs-on: ubuntu-latest
needs: test
if: always()
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download test results
uses: actions/download-artifact@v4
with:
name: test-results
- name: Download HTML coverage report
uses: actions/download-artifact@v4
with:
name: coverage-html-report
- name: Download XML coverage report
uses: actions/download-artifact@v4
with:
name: coverage-xml-report
- name: Download coverage exec data
uses: actions/download-artifact@v4
with:
name: coverage-exec-data
- name: Setup Java and Gradlew
uses: ./.github/actions/setup-java-and-gradlew
with:
java-distribution: 'liberica'
java-version: '21'
java-package: 'jdk'
- name: Cache SonarCloud packages
uses: actions/cache@v4
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Run Checkstyle
run: ./gradlew checkstyleMain checkstyleTest -x test --info
- name: Run JaCoCo Test Coverage Verification
run: ./gradlew jacocoTestCoverageVerification -x test -x jacocoTestReport --info 2>&1 | tee jacoco_verification.log
continue-on-error: true
- name: Parse Jacoco Violations
if: always()
id: parse
run: |
# 'Rule violated for class'가 포함된 라인 추출, 중복 제거
violations=$(grep "Rule violated for class" jacoco_verification.log | grep "\[ant:jacocoReport\]" | sort | uniq || true)
# violations이 없으면 처리 종료
if [ -z "$violations" ]; then
echo "No violations found."
echo "violations_table=" >> $GITHUB_OUTPUT
exit 0
fi
# Markdown Table 초기화
table="| Class | Metric | Actual | Threshold Type | Expected |"
table="$table
|-------|--------|--------|----------------|----------|"
# sed를 이용한 파싱 및 예외 처리
# 패턴 예시:
# [ant:jacocoReport] Rule violated for class com.example.ClassName: branches covered ratio is 0.3, but expected minimum is 0.7
# 그룹화:
# 1: 클래스명
# 2: 메트릭(예: branches covered)
# 3: 실제값(0.3)
# 4: 기준 타입(minimum 또는 maximum)
# 5: 기대값(0.7)
{
echo "$violations" | sed -nE 's/.*Rule violated for class ([^:]*): ([^ ]+ [^ ]+) ratio is ([0-9.]*)[, ] but expected (minimum|maximum) is ([0-9.]*)/| \1 | \2 | \3 | \4 | \5 |/p' > violations_table.md
} || {
echo "Error during sed parsing. Exiting." >&2
exit 1
}
# 테이블 데이터 생성
if [ -s violations_table.md ]; then
while IFS= read -r line; do
table="$table
$line"
done < violations_table.md
else
table="$table
| No violations found | - | - | - | - |"
fi
# Markdown Table을 GitHub Output에 추가
echo "violations_table<<EOF" >> $GITHUB_OUTPUT
echo "$table" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Run SonarCloud Analysis
run: ./gradlew sonar --info
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: |
build/reports/jacoco/test/jacocoTestReport.xml
fail_ci_if_error: false
verbose: true
- name: Post Coverage & Quality Results to PR
if: always()
uses: marocchino/sticky-pull-request-comment@v2
with:
header: Coverage & Quality Results
recreate: true
message: |
## 📉 JaCoCo Coverage Verification Results
${{ steps.parse.outputs.violations_table }}
*(If empty, there are no coverage violations.)*
### HTML Coverage Report
You can download the HTML coverage report from the Artifacts of this run:
[View Coverage HTML Artifacts](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
워크플로우 트리거 설정
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
- `main`과 `develop` 브랜치에 변경 사항이 푸시되거나, PR이 생성될 때 워크플로우가 실행됩니다.
- 이는 CI 파이프라인이 주요 브랜치에 대한 변경사항을 자동으로 확인하고, 코드 품질과 테스트 결과를 검증할 수 있도록 합니다.
워크플로우 권한 설정
permissions:
pull-requests: write
contents: read
actions: read
checks: write
- `pull-requests: write`: PR 코멘트를 작성하기 위한 권한을 부여합니다.
- `contents: read`: 리포지토리의 내용을 읽을 수 있도록 권한을 부여합니다.
- `checks: write`: 테스트 결과를 업로드하고 코드 품질 검사를 통합하기 위한 권한을 부여합니다.
Gradle을 이용한 테스트 실행
- name: Run tests with Gradle
run: ./gradlew test jacocoTestReport --info --parallel
env:
SPRING_PROFILES_ACTIVE: test
- 유닛 테스트를 실행하여 코드가 정상적으로 동작하는지 확인하고 테스트 커버리지 리포트를 생성합니다.
테스트 결과 및 커버리지 리포트 업로드
// build.gradle.kts
tasks.named<Test>("test") {
useJUnitPlatform()
reports {
junitXml.required.set(true) // JUnit 테스트 실행 결과를 XML 형식으로 출력
}
}
- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: test-results
path: 'build/reports/tests/test/*'
- name: Upload HTML Coverage Report
uses: actions/upload-artifact@v4
with:
name: coverage-html-report
path: 'build/reports/jacoco/test/html'
- name: Upload XML Coverage Report
uses: actions/upload-artifact@v4
with:
name: coverage-xml-report
path: 'build/reports/jacoco/test/jacocoTestReport.xml'
- name: Upload coverage exec data
uses: actions/upload-artifact@v4
with:
name: coverage-exec-data
path: '**/build/jacoco/test.exec'
- 테스트 실행 결과 파일과 커버리지 리포트를 업로드합니다.
- JaCoCo의 실행 데이터(`test.exec`)를 업로드하여, 향후 코드 품질 도구에서 자세한 분석을 할 수 있도록 합니다.
테스트 요약 보고서 생성
- name: Test Reporter
id: reporter
uses: dorny/test-reporter@v1
with:
name: Spring Boot Tests
path: 'build/test-results/test/*.xml'
reporter: java-junit
only-summary: false
list-suites: all
list-tests: all
max-annotations: 50
fail-on-error: true
fail-on-empty: true
- JUnit 테스트 결과를 수집하고, 요약 보고서를 생성합니다.
- 테스트 결과가 없거나 실패 시 워크플로우가 실패하도록 설정합니다.
테스트 요약 보고서 PR 게시
- name: Compute Metrics
run: |
total=$(( ${PASSED:-0} + ${FAILED:-0} + ${SKIPPED:-0} ))
time_in_seconds=$(echo "scale=2; ${TIME_MS:-0} / 1000" | bc)
echo "total=$total" >> $GITHUB_ENV
echo "time_in_seconds=$time_in_seconds" >> $GITHUB_ENV
env:
PASSED: ${{ steps.reporter.outputs.passed }}
FAILED: ${{ steps.reporter.outputs.failed }}
SKIPPED: ${{ steps.reporter.outputs.skipped }}
TIME_MS: ${{ steps.reporter.outputs.time }}
- name: Post Test Results to PR
uses: marocchino/sticky-pull-request-comment@v2
with:
header: Test Results
recreate: true
message: |
## 🛠️ Test Summary (${{ steps.reporter.outputs.conclusion }})
📄 **[View Detailed Test Logs](${{ steps.reporter.outputs.url_html }})**
| **Metrics** | **Test Result Details** |
|-----------------------|---------------------------------------|
| **Total Tests** | ${{ env.total }} |
| ✅ **Tests Passed** | ${{ steps.reporter.outputs.passed }} |
| ❌ **Tests Failed** | ${{ steps.reporter.outputs.failed }} |
| ⚠️ **Tests Skipped** | ${{ steps.reporter.outputs.skipped }} |
| ⏱️ **Execution Time** | ${{ env.time_in_seconds }}s |
- 테스트 결과 요약 정보를 계산합니다.
- PR에 테스트 결과 요약을 코멘트로 게시하여, 테스트 통과 및 실패 현황을 직관적으로 확인할 수 있도록 합니다.
테스트 결과 및 커버리지 리포트 다운로드
- name: Download test results
uses: actions/download-artifact@v4
with:
name: test-results
- name: Download HTML coverage report
uses: actions/download-artifact@v4
with:
name: coverage-html-report
- name: Download XML coverage report
uses: actions/download-artifact@v4
with:
name: coverage-xml-report
- name: Download coverage exec data
uses: actions/download-artifact@v4
with:
name: coverage-exec-data
- 이전 단계에서 업로드된 테스트 결과 및 커버리지 리포트를 다운로드합니다.
SonarCloud 패키지 캐싱
- name: Cache SonarCloud packages
uses: actions/cache@v4
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- SonarCloud 분석에 필요한 패키지를 캐싱합니다.
- 네트워크 요청과 다운로드 시간을 줄여 워크플로우 실행 속도를 최적화합니다.
Checkstyle 실행
- name: Run Checkstyle
run: ./gradlew checkstyleMain checkstyleTest -x test --info
- Gradle의 Checkstyle 태스크를 실행하여 코드 스타일 규칙 위반 사항을 검증합니다.
- `-x test`는 테스트 태스크를 제외하고 Checkstyle만 실행하도록 설정합니다.
JaCoCo 커버리지 검증
- name: Run JaCoCo Test Coverage Verification
run: ./gradlew jacocoTestCoverageVerification -x test -x jacocoTestReport --info 2>&1 | tee jacoco_verification.log
continue-on-error: true
- JaCoCo의 `violationRules`에 정의된 커버리지 기준을 검증합니다.
- `jacoco_verification.log`에 커버리지 검증 결과를 저장합니다.
- `continue-on-error: true`로 설정하여 커버리지 기준 미달 시에도 워크플로우를 계속 진행할 수 있도록 합니다.
JaCoCo 위반사항 파싱
- name: Parse Jacoco Violations
if: always()
id: parse
run: |
# 'Rule violated for class'가 포함된 라인 추출, 중복 제거
violations=$(grep "Rule violated for class" jacoco_verification.log | grep "\[ant:jacocoReport\]" | sort | uniq || true)
# violations이 없으면 처리 종료
if [ -z "$violations" ]; then
echo "No violations found."
echo "violations_table=" >> $GITHUB_OUTPUT
exit 0
fi
# Markdown Table 초기화
table="| Class | Metric | Actual | Threshold Type | Expected |"
table="$table
|-------|--------|--------|----------------|----------|"
# sed를 이용한 파싱 및 예외 처리
# 패턴 예시:
# [ant:jacocoReport] Rule violated for class com.example.ClassName: branches covered ratio is 0.3, but expected minimum is 0.7
# 그룹화:
# 1: 클래스명
# 2: 메트릭(예: branches covered)
# 3: 실제값(0.3)
# 4: 기준 타입(minimum 또는 maximum)
# 5: 기대값(0.7)
{
echo "$violations" | sed -nE 's/.*Rule violated for class ([^:]*): ([^ ]+ [^ ]+) ratio is ([0-9.]*)[, ] but expected (minimum|maximum) is ([0-9.]*)/| \1 | \2 | \3 | \4 | \5 |/p' > violations_table.md
} || {
echo "Error during sed parsing. Exiting." >&2
exit 1
}
# 테이블 데이터 생성
if [ -s violations_table.md ]; then
while IFS= read -r line; do
table="$table
$line"
done < violations_table.md
else
table="$table
| No violations found | - | - | - | - |"
fi
# Markdown Table을 GitHub Output에 추가
echo "violations_table<<EOF" >> $GITHUB_OUTPUT
echo "$table" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- `jacoco_verification.log`에서 "Rule violated for class" 메시지를 포함한 라인을 추출하고, 중복을 제거합니다.
- 위반 사항이 없으면 종료하고, 있으면 데이터를 파싱하여 마크다운 표로 변환합니다.
SonarCloud 분석
- name: Run SonarCloud Analysis
run: ./gradlew sonar --info
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- SonarCloud 분석을 실행하여 코드 품질, 보안 취약점, 유지보수성 문제를 검토합니다.
- 프로젝트의 코드 품질 상태를 PR에 시각화하고, 코드베이스를 개선하기 위한 피드백을 제공합니다.
Codecov 업로드
# {project_root}/codecov.yml
coverage:
precision: 2 # 커버리지 결과를 소수점 2자리까지 표시합니다.
round: down # 커버리지 값을 내림 처리하여 표시합니다.
range: '70...100' # 커버리지 기준을 70%에서 100% 사이로 설정합니다.
status:
project:
default:
target: auto # 프로젝트 전체의 기본 목표 커버리지를 자동으로 설정합니다.
threshold: 1% # 목표 커버리지에서 허용되는 편차를 1%로 설정합니다.
patch:
default:
target: auto # 신규 코드의 기본 목표 커버리지를 자동으로 설정합니다.
threshold: 1% # 신규 코드 커버리지에서 허용되는 편차를 1%로 설정합니다.
comment:
layout: 'diff, flags, files' # PR 코멘트에 diff, 플래그, 파일별 커버리지 결과를 표시합니다.
behavior: default # 기존 코멘트를 업데이트하거나, 없으면 새로 작성합니다.
require_changes: true # 변경된 코드가 있어야 코멘트를 게시합니다.
require_base: false # 베이스 브랜치 커버리지가 없어도 코멘트를 게시합니다.
require_head: false # 헤드 브랜치 커버리지가 없어도 코멘트를 게시합니다.
hide_project_coverage: false # 프로젝트 전체 커버리지 정보를 숨기지 않습니다.
codecov:
notify:
require_ci_to_pass: true # CI가 통과해야 Codecov 알림을 활성화합니다.
require_ci_to_pass: true # CI가 통과해야 Codecov의 분석 결과가 게시됩니다.
exclude:
- '**/Test*.java' # 커버리지 분석에서 `Test*.java` 파일을 제외합니다.
max_report_age: '30d' # 최대 30일 동안의 보고서만 유효하다고 간주합니다.
mode: all # 모든 커버리지 보고서를 병합하여 분석합니다.
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: |
build/reports/jacoco/test/jacocoTestReport.xml
fail_ci_if_error: false
verbose: true
- JaCoCo XML 커버리지 리포트를 Codecov에 업로드합니다.
- `fail_ci_if_error: false`: Codecov 업로드 실패 시에도 워크플로우를 계속 진행합니다.
- `verbose: true`: 업로드 과정의 상세 로그를 출력합니다.
JaCoCo 위반사항 PR 게시
- name: Post Coverage & Quality Results to PR
if: always()
uses: marocchino/sticky-pull-request-comment@v2
with:
header: Coverage & Quality Results
recreate: true
message: |
## 📉 JaCoCo Coverage Verification Results
${{ steps.parse.outputs.violations_table }}
*(If empty, there are no coverage violations.)*
### HTML Coverage Report
You can download the HTML coverage report from the Artifacts of this run:
[View Coverage HTML Artifacts](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
- JaCoCo 위반 사항과 HTML 커버리지 리포트 링크를 PR에 게시합니다.
- 팀원이 PR에서 바로 커버리지 상태와 위반 사항을 확인할 수 있도록 시각적 피드백을 제공합니다.
3. CI 파이프라인 적용 후 PR 결과
테스트 요약 보고서
SonarCloud 분석
Codecov 분석
JaCoCo 위반사항
4. 마치며
처음에는 단순히 단위 테스트와 빌드를 실행하는 CI 워크플로우 실행 시간을 줄이고자 시작했던 작업이었습니다. 하지만 점차 욕심이 생겨 SonarCloud, JaCoCo, Checkstyle, Codecov 등 다양한 외부 도구를 하나씩 접목하게 되었고, 결국 코드 품질을 유지하고 개선할 수 있는 체계적인 CI 파이프라인을 구축하게 되었습니다.
처음에는 하루 정도 소요될 작업이라고 예상했으나, 실제로는 일주일가량 걸리며 예상보다 많은 시간이 소요되었습니다. 하지만 그 과정에서 CI/CD와 관련된 다양한 도구와 개념을 깊이 이해할 수 있는 값진 시간이었습니다.
최대한 공식 문서와 기술 블로그를 참고하며 작업을 진행했지만, 아직 미숙한 부분이 있을 수 있습니다. 더 나은 방향으로 개선할 수 있는 점이 있다면 피드백을 주시면 감사하겠습니다.
'DevOps' 카테고리의 다른 글
Docker Multi-stage Builds, Layered JAR로 빌드 성능과 이미지 최적화하기 (6) | 2024.11.24 |
---|