Skip to content

Commit a857ca5

Browse files
authored
chore: JaCoCo와 SonarCloud를 연동한 Gradle 빌드 및 품질 관리 시스템 구축 (#30)
* [BOOK-94] refactor: root - 프로젝트 root 이름 변경 * [BOOK-94] chore: SonarCloud와 JaCoCo를 활용한 코드 품질 및 테스트 커버리지 관리 기능 추가 * [BOOK-94] refactor: CI 워크플로우에 fullCheck 태스크 적용 * [BOOK-94] fix: executionData 설정 방식을 각 하위 모듈의 build/jacoco/test.exec 파일 경로를 지연된 방식으로 참조하도록 수정 * [BOOK-94] refactor: 코드레빗 리뷰 반영 * [BOOK-94] chore: (임시) info -> debug로 빌드 실패 원인 파악 * [BOOK-94] chore: debug -> info로 변경 * [BOOK-94] refactor: classesDirs를 사용하여 JaCoCo 분석 대상 경로 명시 - 현재 프로젝트는 Kotlin으로만 작성되어 'build/classes/java/main' 디렉토리가 존재하지 않습니다. - 하지만 JaCoCo는 기본적으로 이 경로를 확인하려 시도하여 불필요한 "not found" 경고가 발생했습니다. - 이를 해결하기 위해, classDirectories가 실제로 컴파일된 클래스 파일이 있는 경로('classesDirs')만 참조하도록 설정을 최적화하여 경고 로그를 제거했습니다. * [BOOK-94] fix: 중복 인덱싱 방지 로직 추가 * [BOOK-94] fix: 테스트하지 않는 코드 패턴을 명시하여 중복 인덱싱 방지 * [BOOK-94] fix: classes/kotlin/main로 경로 명확히 지정 * [BOOK-94] fix: 자동 탐지와 수동 설정 충돌 해결 * [BOOK-94] chore: 주석 제거 * [BOOK-94] chore: 코드레빗 리뷰 반영 * [BOOK-94] chore: 코드레빗 리뷰 반영 * [BOOK-94] fix: 패턴 수정 * [BOOK-94] refactor: 코드레빗 리뷰 반영
1 parent cb55e09 commit a857ca5

File tree

5 files changed

+180
-18
lines changed

5 files changed

+180
-18
lines changed

.github/workflows/ci-pr.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ concurrency:
1717
jobs:
1818
build-validation:
1919
runs-on: ubuntu-24.04
20-
timeout-minutes: 10
20+
timeout-minutes: 15
2121

2222
steps:
2323
- name: Checkout code
@@ -44,8 +44,18 @@ jobs:
4444
with:
4545
gradle-home-cache-cleanup: true
4646

47+
- name: Cache SonarCloud packages
48+
uses: actions/cache@v4
49+
with:
50+
path: ~/.sonar/cache
51+
key: ${{ runner.os }}-sonar
52+
restore-keys: ${{ runner.os }}-sonar
53+
4754
- name: Grant execute permission for gradlew
4855
run: chmod +x gradlew
4956

50-
- name: Run Gradle check (fast validation)
51-
run: ./gradlew check --parallel --build-cache
57+
- name: Run full check and SonarCloud analysis
58+
env:
59+
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
60+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
61+
run: ./gradlew fullCheck --parallel --build-cache --info --stacktrace

build.gradle.kts

Lines changed: 162 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ plugins {
88
kotlin(Plugins.Kotlin.Short.SPRING) version Versions.KOTLIN
99
kotlin(Plugins.Kotlin.Short.JPA) version Versions.KOTLIN
1010
id(Plugins.DETEKT) version Versions.DETEKT
11-
id(Plugins.KOVER) version Versions.KOVER
11+
id(Plugins.JACOCO)
12+
id(Plugins.SONAR_QUBE) version Versions.SONAR_QUBE
1213
}
1314

1415
allprojects {
@@ -19,29 +20,41 @@ allprojects {
1920
}
2021
}
2122

23+
// 테스트하지 않는 코드 패턴 (JaCoCo + SonarQube 커버리지 + CPD 공통)
24+
val testExclusionPatterns = listOf(
25+
"**/*Application*",
26+
"**/config/**",
27+
"**/*Config*",
28+
"**/exception/**",
29+
"**/*Exception*",
30+
"**/*ErrorCode*",
31+
"**/dto/**",
32+
"**/*Request*",
33+
"**/*Response*",
34+
"**/*Entity*",
35+
"**/annotation/**",
36+
"**/generated/**"
37+
)
38+
39+
// SonarQube 전체 분석 제외 패턴 (분석 자체가 의미 없는 파일들)
40+
val sonarGlobalExclusions = listOf(
41+
"**/build/**",
42+
)
43+
2244
subprojects {
2345
apply(plugin = Plugins.SPRING_BOOT)
2446
apply(plugin = Plugins.SPRING_DEPENDENCY_MANAGEMENT)
2547
apply(plugin = Plugins.Kotlin.SPRING)
2648
apply(plugin = Plugins.Kotlin.JPA)
2749
apply(plugin = Plugins.Kotlin.JVM)
50+
apply(plugin = Plugins.JACOCO)
2851

2952
java {
3053
toolchain {
3154
languageVersion.set(JavaLanguageVersion.of(Versions.JAVA_VERSION.toInt()))
3255
}
3356
}
3457

35-
dependencyManagement {
36-
imports {
37-
mavenBom("org.springframework.cloud:spring-cloud-dependencies:2025.0.0")
38-
}
39-
}
40-
41-
tasks.withType<Test> {
42-
useJUnitPlatform()
43-
}
44-
4558
plugins.withId(Plugins.Kotlin.ALLOPEN) {
4659
extensions.configure<org.jetbrains.kotlin.allopen.gradle.AllOpenExtension> {
4760
annotation("jakarta.persistence.Entity")
@@ -53,14 +66,150 @@ subprojects {
5366
// Configure Kotlin compiler options
5467
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
5568
kotlinOptions {
56-
freeCompilerArgs = listOf("-Xjsr305=strict")
69+
freeCompilerArgs += listOf(
70+
"-Xjsr305=strict",
71+
"-Xconsistent-data-class-copy-visibility"
72+
)
5773
jvmTarget = Versions.JAVA_VERSION
58-
freeCompilerArgs += "-Xconsistent-data-class-copy-visibility"
5974
}
6075
}
6176
}
6277

78+
// 루트 프로젝트에서 모든 JaCoCo 설정 관리
79+
configure(subprojects) {
80+
jacoco {
81+
toolVersion = Versions.JACOCO
82+
}
83+
84+
tasks.withType<Test> {
85+
useJUnitPlatform()
86+
finalizedBy("jacocoTestReport")
87+
88+
testLogging {
89+
events("passed", "skipped", "failed")
90+
showStandardStreams = false
91+
}
92+
}
93+
94+
// 각 서브모듈의 JaCoCo 테스트 리포트 설정
95+
tasks.withType<JacocoReport> {
96+
dependsOn("test")
97+
reports {
98+
xml.required.set(true)
99+
csv.required.set(false)
100+
html.required.set(true)
101+
}
102+
103+
classDirectories.setFrom(fileTree(layout.buildDirectory.dir("classes/kotlin/main")) {
104+
exclude(testExclusionPatterns)
105+
})
106+
107+
executionData.setFrom(fileTree(layout.buildDirectory) {
108+
include("jacoco/*.exec")
109+
})
110+
}
111+
}
112+
63113
tasks {
64114
withType<Jar> { enabled = true }
65115
withType<BootJar> { enabled = false }
66116
}
117+
118+
// 루트 프로젝트 JaCoCo 통합 리포트 설정
119+
tasks.register<JacocoReport>("jacocoRootReport") {
120+
description = "Generates an aggregate report from all subprojects"
121+
group = "reporting"
122+
123+
dependsOn(subprojects.map { it.tasks.named("test") })
124+
125+
sourceDirectories.setFrom(subprojects.map { it.the<SourceSetContainer>()["main"].allSource.srcDirs })
126+
classDirectories.setFrom(subprojects.map { subproject ->
127+
subproject.fileTree(subproject.layout.buildDirectory.get().asFile.resolve("classes/kotlin/main")) {
128+
exclude(testExclusionPatterns)
129+
}
130+
})
131+
executionData.from(subprojects.map { subproject ->
132+
subproject.fileTree(subproject.layout.buildDirectory.dir("jacoco")) {
133+
include("**/*.exec")
134+
}
135+
})
136+
137+
reports {
138+
xml.required.set(true)
139+
csv.required.set(false)
140+
html.required.set(true)
141+
}
142+
}
143+
144+
// SonarQube 설정을 루트에서 모든 서브모듈에 대해 설정
145+
sonar {
146+
properties {
147+
property("sonar.projectKey", "YAPP-Github_26th-App-Team-1-BE")
148+
property("sonar.organization", "yapp-github")
149+
property("sonar.host.url", "https://sonarcloud.io")
150+
property(
151+
"sonar.coverage.jacoco.xmlReportPaths",
152+
"${layout.buildDirectory.get()}/reports/jacoco/jacocoRootReport/jacocoRootReport.xml"
153+
)
154+
property("sonar.kotlin.coveragePlugin", Plugins.JACOCO)
155+
property("sonar.kotlin.version", Versions.KOTLIN)
156+
property("sonar.exclusions", sonarGlobalExclusions.joinToString(","))
157+
property("sonar.cpd.exclusions", testExclusionPatterns.joinToString(","))
158+
property("sonar.coverage.exclusions", testExclusionPatterns.joinToString(","))
159+
}
160+
}
161+
162+
// SonarQube 태스크가 통합 JaCoCo 리포트에 의존하도록 설정
163+
tasks.named("sonar") {
164+
dependsOn("jacocoRootReport")
165+
}
166+
167+
/**
168+
* CI용 - 전체 품질 검증 파이프라인을 실행합니다. (테스트, 커버리지, SonarQube 분석)
169+
* GitHub Actions에서 이 태스크 하나만 호출합니다.
170+
* 사용 예: ./gradlew fullCheck
171+
*/
172+
tasks.register("fullCheck") {
173+
description = "Runs all tests, generates reports, and performs SonarQube analysis"
174+
group = "Verification"
175+
dependsOn("testAll", "jacocoTestReportAll")
176+
finalizedBy("sonar")
177+
}
178+
179+
/**
180+
* 로컬용 - SonarQube 분석 없이 빠르게 테스트 커버리지만 확인합니다.
181+
* 사용 예: ./gradlew checkCoverage
182+
*/
183+
tasks.register("checkCoverage") {
184+
description = "Runs tests and generates coverage reports without SonarQube analysis"
185+
group = "Verification"
186+
dependsOn("testAll", "jacocoTestReportAll")
187+
}
188+
189+
/**
190+
* 로컬용 - 빌드 과정에서 생성된 모든 리포트를 삭제합니다.
191+
* 사용 예: ./gradlew cleanReports
192+
*/
193+
tasks.register("cleanReports") {
194+
description = "Cleans all generated reports"
195+
group = "Cleanup"
196+
doLast {
197+
subprojects.forEach { subproject ->
198+
delete(subproject.layout.buildDirectory.dir("reports"))
199+
}
200+
delete(layout.buildDirectory.dir("reports"))
201+
}
202+
}
203+
204+
tasks.register("testAll") {
205+
description = "Runs tests in all subprojects"
206+
group = "Verification"
207+
dependsOn(subprojects.map { it.tasks.named("test") })
208+
}
209+
210+
tasks.register("jacocoTestReportAll") {
211+
description = "Generates JaCoCo test reports for all subprojects and creates aggregate report"
212+
group = "Verification"
213+
dependsOn(subprojects.map { it.tasks.named("jacocoTestReport") })
214+
finalizedBy("jacocoRootReport")
215+
}

buildSrc/src/main/kotlin/Plugins.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ object Plugins {
33
const val SPRING_DEPENDENCY_MANAGEMENT = "io.spring.dependency-management"
44
const val DETEKT = "io.gitlab.arturbosch.detekt"
55
const val KOVER = "org.jetbrains.kotlinx.kover"
6+
const val JACOCO = "jacoco"
7+
const val SONAR_QUBE = "org.sonarqube"
68

79
object Kotlin {
810
const val ALLOPEN = "org.jetbrains.kotlin.plugin.allopen"

buildSrc/src/main/kotlin/Versions.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ object Versions {
33
const val SPRING_DEPENDENCY_MANAGEMENT = "1.1.7"
44
const val KOTLIN = "1.9.25"
55
const val DETEKT = "1.23.1"
6-
const val KOVER = "0.9.1"
6+
const val SONAR_QUBE = "6.2.0.5505"
7+
const val JACOCO = "0.8.13"
78
const val JAVA_VERSION = "21"
89
}

settings.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
rootProject.name = "multi-module-test"
1+
rootProject.name = "reed"
22

33
include(
44
"admin",

0 commit comments

Comments
 (0)