Back-end

[Spring Boot] ๋ฉ€ํ‹ฐ๋ชจ๋“ˆ์— JaCoCo + JaCoCo Report Aggregation ์ ์šฉํ•˜๊ธฐ

์„œ์ฑ„๋ฆฌ 2024. 7. 25. 01:11
macOS
IntelliJ IDEA
Java 17
Spring Boot 3.x
Groovy

๐Ÿ“„ JaCoCo

JaCoCo ํ”Œ๋Ÿฌ๊ทธ์ธ ์ ์šฉ

JaCoCo :: Maven Plugin์—์„œ JaCoCo์˜ ์ตœ์‹  ๋ฒ„์ „์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์ฒ˜์Œ์—๋Š” build.gradle์— JaCoCo ํƒœ์Šคํฌ๋ฅผ ์„ค์ •ํ–ˆ์œผ๋‚˜, ๋„ˆ๋ฌด ๋ณต์žกํ•ด์ ธ์„œ ๋ณ„๋„๋กœ jacoco.gradle ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์—ฌ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ž‘์„ฑํ•˜์˜€๋‹ค.

// build.gradle
apply from: 'jacoco.gradle'
// jacoco.gradle
subprojects {
    apply plugin: 'jacoco' // Jacoco Plugin ์ถ”๊ฐ€

    jacoco {
        toolVersion = '0.8.12'
        // reportsDir = ${project.reporting.baseDir}/jacoco
    }

    test {
        useJUnitPlatform()
    }
}

JaCoCo ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ๋ฒ„์ „์„ ์„ค์ •ํ•œ ๋’ค gradle์„ ์ƒˆ๋กœ๊ณ ์นจ ํ•˜๋ฉด JaCoCo ์˜์กด์„ฑ์ด ์ถ”๊ฐ€๋˜๋ฉด์„œ ๊ฐ ๋ชจ๋“ˆ์˜ Tasks/verification์— JaCoCo์˜ Task๊ฐ€ ์ถ”๊ฐ€๋œ๋‹ค.

reportsDir์„ ๋ณ„๋„๋กœ ์„ค์ •ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ, ${project.reporting.baseDir}/jacoco ๊ฐ€ ๊ธฐ๋ณธ๊ฒฝ๋กœ๋กœ ์ž๋™ ์„ค์ •๋œ๋‹ค.

 

๋ณด๊ณ ์„œ ํŒŒ์ผ ์„ค์ •

๋ณด๊ณ ์„œ ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด jacoco.gradle์— ์•„๋ž˜ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•ด ์ค€๋‹ค.

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

JaCoCo๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ณด๊ณ ์„œ์ธ exec ํŒŒ์ผ์„ ์ƒ์„ฑํ•œ๋‹ค. ์ด๋Š” ์‚ฌ๋žŒ์ด ์ฝ์„ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— Jacoco Plugin์˜ jacocoTestReport ํƒœ์Šคํฌ๋ฅผ ํ†ตํ•ด exec ํŒŒ์ผ์„ XML, CSV, HTML ํŒŒ์ผ ํ˜•ํƒœ๋กœ ์ถœ๋ ฅํ•  ์ˆ˜ ์žˆ๋‹ค. 

 

XML๊ณผ CSV ํŒŒ์ผ์€ ์†Œ์Šค ์ฝ”๋“œ ๋ถ„์„์„ ์œ„ํ•ด ์†Œ๋‚˜ํ๋ธŒ ๋“ฑ๊ณผ ์—ฐ๋™ํ•  ๋•Œ ์‚ฌ์šฉ๋˜๋ฉฐ, HTML ํŒŒ์ผ์€ ์‚ฌ๋žŒ์ด ์ง์ ‘ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ํ™•์ธํ•  ๋•Œ ์‚ฌ์šฉ๋œ๋‹ค.

subprojects {
    apply plugin: 'jacoco'

    jacoco {
        toolVersion = '0.8.12'
    }

    test {
        useJUnitPlatform()
    }

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

 

์ปค๋ฒ„๋ฆฌ์ง€ ๊ธฐ์ค€ ์„ค์ •

JaCoCo ํ”Œ๋Ÿฌ๊ทธ์ธ์—์„œ๋Š” jacocoTestCoverageVerification ํƒœ์Šคํฌ๋กœ ์ตœ์†Œ ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ์ˆ˜์ค€์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๊ณ , ๊ธฐ์ค€์„ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•  ๊ฒฝ์šฐ ํƒœ์Šคํฌ๊ฐ€ ์‹คํŒจํ•˜๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

 

jacocoTestCoverageVerification์˜ violationRules ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์ปค๋ฒ„๋ฆฌ์ง€ ๊ธฐ์ค€์„ ์„ค์ •์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ๊ณ , ๊ฐ๊ฐ์˜ ๋ฃฐ์— ๋Œ€ํ•œ ์„ค์ •์€ violationRules ๋ฉ”์„œ๋“œ์— ์ „๋‹ฌํ•  rule ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

jacocoTestCoverageVerification {
    violationRules {
        rule {
            enabled = true
            element = "CLASS"
            limit {
                counter = "LINE"
                value = "COVEREDRATIO"
                minimum = 0.8
            }
            excludes = [
                    '*.*Application'
            ]
        }
        rule {
            // ์—ฌ๋Ÿฌ๊ฐœ์˜ ๊ทœ์น™์ด ๊ฐ€๋Šฅํ•˜๋‹ค
        }
    }
}
  • enabled
    ๊ทœ์น™์˜ ํ™œ์„ฑํ™” ์—ฌ๋ถ€๋ฅผ boolean์œผ๋กœ ๋‚˜ํƒ€๋‚ธ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ true์ด๋‹ค.
  • element
    ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์ฒดํฌํ•  ๊ธฐ์ค€(๋‹จ์œ„)์„ ์„ค์ •ํ•œ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ BUNDLE์ด๋‹ค.
    • BUNDLE: ํŒจํ‚ค์ง€ ๋ฒˆ๋“ค (์ „์ฒด ํ”„๋กœ์ ํŠธ)
    • CLASS: ํด๋ž˜์Šค
    • GROUP: ๋…ผ๋ฆฌ์  ๋ฒˆ๋“ค ๊ทธ๋ฃน
    • METHOD: ๋ฉ”์„œ๋“œ
    • PACKAGE: ํŒจํ‚ค์ง€
    • SOURCEFILE: ์†Œ์Šค ํŒŒ์ผ
  • includes
    ํ•ด๋‹นํ•˜๋Š” rule์˜ ์ ์šฉ๋Œ€์ƒ์„ package ์ˆ˜์ค€์œผ๋กœ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ ์ „์ฒด package์ด๋‹ค.
  • counter
    ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์ธก์ •ํ•  ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ์ง€ํ‘œ์ด๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ INSTRUCTION์ด๋‹ค.
    • LINE: ๋นˆ ์ค„์„ ์ œ์™ธํ•œ ์‹ค์ œ ์ฝ”๋“œ์˜ ๋ผ์ธ ์ˆ˜, ๋ผ์ธ์ด ํ•œ ๋ฒˆ์ด๋ผ๋„ ์‹คํ–‰๋œ๋‹ค๋ฉด ์‹คํ–‰๋œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผ
    • BRANCH: ์กฐ๊ฑด๋ฌธ ๋“ฑ์˜ ๋ถ„๊ธฐ ์ˆ˜
    • CLASS: ํด๋ž˜์Šค ์ˆ˜, ๋‚ด๋ถ€ ๋ฉ”์„œ๋“œ๊ฐ€ ํ•œ ๋ฒˆ์ด๋ผ๋„ ์‹คํ–‰๋œ๋‹ค๋ฉด ์‹คํ–‰๋œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผ
    • METHOD: ๋ฉ”์„œ๋“œ ์ˆ˜, ๋ฉ”์„œ๋“œ๊ฐ€ ํ•œ ๋ฒˆ์ด๋ผ๋„ ์‹คํ–‰๋œ๋‹ค๋ฉด ์‹คํ–‰๋œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผ
    • COMPLEXITY: ๋ณต์žก๋„
    • INSTRUCTION: ์ž๋ฐ” ๋ฐ”์ดํŠธ์ฝ”๋“œ ๋ช…๋ น ์ˆ˜
  • value
    ์ธก์ •ํ•œ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ๋ณด์—ฌ์ค„ ๊ฒƒ์ธ์ง€๋ฅผ ๋งํ•˜๋ฉฐ limit ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ COVEREDRATIO์ด๋‹ค.
    • COVEREDCOUNT: ์ปค๋ฒ„๋œ ๊ฐœ์ˆ˜
    • COVEREDRATIO: ์ปค๋ฒ„๋œ ๋น„์œจ, 0-1 ์‚ฌ์ด์˜ ์ˆซ์ž๋กœ 1์ด 100%
    • MISSEDCOUNT: ์ปค๋ฒ„๋˜์ง€ ์•Š์€ ๊ฐœ์ˆ˜
    • MISSEDRATIO: ์ปค๋ฒ„๋˜์ง€ ์•Š์€ ๋น„์œจ, 0-1 ์‚ฌ์ด์˜ ์ˆซ์ž๋กœ 1์ด 100%
    • TOTALCOUNT: ์ „์ฒด ๊ฐœ์ˆ˜
  • minimum
    counter ๊ฐ’์„ value์— ๋งž๊ฒŒ ํ‘œํ˜„ํ–ˆ์„ ๋•Œ ์ตœ์†Ÿ๊ฐ’์„ ๋งํ•˜๋ฉฐ limit ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด ๊ฐ’์„ ํ†ตํ•ด jacocoTestCoverageVerification์˜ ์„ฑ๊ณต ์—ฌ๋ถ€๊ฐ€ ๊ฒฐ์ •๋œ๋‹ค.
    ํ•ด๋‹น ๊ฐ’์€ BidDecimal ํƒ€์ž…์ด๊ณ  ํ‘œ๊ธฐํ•œ ์ž๋ฆฟ์ˆ˜๋งŒํผ value๊ฐ€ ์ถœ๋ ฅ๋œ๋‹ค. ๋งŒ์•ฝ ์ปค๋ฒ„๋ฆฌ์ง€ ์„ค์ •์„ 80%์„ ์›ํ–ˆ์„ ๋•Œ, 0.80์ด ์•„๋‹Œ 0.8 ์ž…๋ ฅ ์‹œ ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ 0.87์ด์–ด๋„ 0.8๋กœ ํ‘œ์‹œ๋œ๋‹ค.
  • excludes
    ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์ธก์ •ํ•  ๋•Œ ์ œ์™ธํ•  ํด๋ž˜์Šค๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ํŒจํ‚ค์ง€ ๋ ˆ๋ฒจ์˜ ๊ฒฝ๋กœ๋กœ ์ง€์ •ํ•˜์—ฌ์•ผ ํ•˜๊ณ  ๊ฒฝ๋กœ์—๋Š” *์™€ ?๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

JaCoCo ํ”Œ๋Ÿฌ๊ทธ์ธ ํƒœ์Šคํฌ ์ˆœ์„œ ์„ค์ •

ํ…Œ์ŠคํŠธ →  ๋ณด๊ณ ์„œ ์ƒ์„ฑ

 

JaCoCo ํ”Œ๋Ÿฌ๊ทธ์ธ์˜ User Guide ๋ฌธ์„œ๋ฅผ ๋ณด๋ฉด ํ…Œ์ŠคํŠธ๋Š” ๋ณด๊ณ ์„œ ์ƒ์„ฑ(jacocoTestReport ํƒœ์Šคํฌ) ์ „์— ์‹คํ–‰๋˜์–ด์•ผ ํ•˜๋Š”๋ฐ, jacocoTestReport ํƒœ์Šคํฌ๋Š” ํ…Œ์ŠคํŠธ ํƒœ์Šคํฌ์— ์˜์กดํ•˜์ง€ ์•Š์•„ ํ•ญ์ƒ jacocoTestReport๋ฅผ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์ „์— test ํƒœ์Šคํฌ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์‹คํ–‰ํ•ด์•ผ ๋œ๋‹ค๊ณ  ํ•œ๋‹ค.

 

๋”ฐ๋ผ์„œ ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚˜๋ฉด ๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํƒœ์Šคํฌ๊ฐ€ ์ž๋™์œผ๋กœ ์‹คํ–‰๋˜๋„๋ก jacoco.gradle์— ์•„๋ž˜ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•ด ์ค€๋‹ค.

test {
    useJUnitPlatform()
    finalizedBy 'jacocoTestReport'
}

 

finalizedBy ์‚ฌ์šฉ ์‹œ ์ด์ „ ํƒœ์Šคํฌ(test)์˜ ์„ฑ๊ณต ์—ฌ๋ถ€์™€ ๊ด€๊ณ„์—†์ด ๋ช…์‹œํ•œ ํƒœ์Šคํฌ(jacocoTestReport)๋ฅผ ์ด์–ด ์‹คํ–‰ํ•˜๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๋ณด๊ณ ์„œ ์ƒ์„ฑ → ์ปค๋ฒ„๋ฆฌ์ง€ ๊ธฐ์ค€ ๋งŒ์กฑ ์—ฌ๋ถ€ ํ™•์ธ

๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•˜๋Š” jacocoTestReport์™€ ์„ค์ •ํ•œ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋งŒ์กฑํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” jacocoTestCoverageVerification์˜ ์ˆœ์„œ๋ฅผ ์ง€์ •ํ•˜์ง€ ์•Š์•„ jacocoTestCoverageVerification ํƒœ์Šคํฌ๊ฐ€ jacocoTestReport ํƒœ์Šคํฌ๋ณด๋‹ค ๋จผ์ € ์‹คํ–‰๋˜์–ด ์„ค์ •ํ•œ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•˜๋ฉด gradle ๋นŒ๋“œ๊ฐ€ ๋ฉˆ์ถ”๊ฒŒ ๋œ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ๋˜๋ฉด jacocoTestReport ํƒœ์Šคํฌ๋Š” ์‹คํ–‰๋˜์ง€ ์•Š๊ฒŒ ๋˜๊ณ , ๋ณด๊ณ ์„œ๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š์•„ ์ด์ „ ํ…Œ์ŠคํŠธ์—์„œ ์ƒ์„ฑ๋œ ๋ณด๊ณ ์„œ๋ฅผ ๋ณด๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

jacocoTestReport {
    reports { ... }
    afterEvaluate { ... }
    finalizedBy 'jacocoTestCoverageVerification'
}

 

๋”ฐ๋ผ์„œ finalizedBy ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ test → jacocoTestReport → jacocoTestCoverageVerification ์ˆœ์„œ๋กœ ํƒœ์Šคํฌ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ๋ชจ๋“  ์กฐ๊ฑด์„ ๋งŒ์กฑํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ถ„์„ ๋Œ€์ƒ ์ œ์™ธ

ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜์ง€ ์•Š์•„๋„ ๋˜๋Š” ์˜ˆ์™ธ ํด๋ž˜์Šค, DTO ํด๋ž˜์Šค ๋“ฑ์„ ํฌํ•จํ•˜์—ฌ ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๊ณ„์‚ฐํ•˜๋ฉด ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ ๋‚ฎ๊ฒŒ ๋‚˜์˜ค๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ํŠน์ • ํด๋ž˜์Šค๋“ค์„ ๋ถ„์„ ๋Œ€์ƒ์—์„œ ์ œ์™ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๋ณด๊ณ ์„œ์— ํฌํ•จ๋  ํŠน์ • ํด๋ž˜์Šค · ํŒจํ‚ค์ง€ ์ œ์™ธ

jacocoTestReport ํƒœ์Šคํฌ์—์„œ ๋ณด๊ณ ์„œ์— ํ‘œ์‹œ๋˜๋Š” ํด๋ž˜์Šค ์ค‘ ์ผ๋ถ€๋ฅผ ์ œ์™ธํ•  ์ˆ˜ ์žˆ๋‹ค. ์ œ์™ธ ๋Œ€์ƒ ํŒŒ์ผ๋“ค์€ Ant ์Šคํƒ€์ผ ํŒจํ„ด์œผ๋กœ ์ž‘์„ฑํ•œ๋‹ค.

jacocoTestReport {
    reports {
        // ...
    }
    afterEvaluate {
        classDirectories.setFrom(
                files(classDirectories.files.collect {
                    fileTree(dir: it, excludes: [
                            '**/common/**',
                            '**/config/**',
                            '**/*Application*',
                            '**/*DetailService*',
                    ])
                }))
    }
    finalizedBy 'jacocoTestCoverageVerification'
}

๐Ÿ“Œ jacocoTestReport์—์„œ๋Š” ๊ฒฝ๋กœ๋ฅผ '/'๋กœ ๊ตฌ๋ถ„ํ•˜๊ณ , ์•„๋ž˜์˜ jacocoTestCoverageVerification์€ ๊ฒฝ๋กœ๋ฅผ '.'๋กœ ๊ตฌ๋ถ„ํ–ˆ์„ ๋•Œ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์šฉ๋œ๋‹ค.

 

์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ๊ฒ€์ฆ ๋Œ€์ƒ์—์„œ ํŠน์ • ํด๋ž˜์Šค · ํŒจํ‚ค์ง€ ์ œ์™ธ

jacocoTestCoverageVerification ํƒœ์Šคํฌ์—์„œ๋Š” ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋งŒ์กฑํ•˜๋Š”์ง€ ํ™•์ธํ•  ๋Œ€์ƒ ์ค‘ ์ผ๋ถ€ ํŒจํ‚ค์ง€๋ฅผ ์ œ์™ธํ•  ์ˆ˜ ์žˆ๋‹ค. ์ œ์™ธ ๋Œ€์ƒ ํŒŒ์ผ๋“ค์€ ์ (.)์œผ๋กœ ๊ตฌ๋ถ„๋œ ์ž๋ฐ” ํด๋ž˜์Šค์™€ ํŒจํ‚ค์ง€ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•œ๋‹ค.

jacocoTestCoverageVerification {
    violationRules {
        rule {
            // ...
            excludes = [
                    "**.common.*",
                    "**.config.*",
                    '**.infra.**',
                    '**.*Application*',
                    '**.*DetailService*',
            ]
        }
    }
}

 

๋ณด๊ณ ์„œ๊ฐ€ ์ƒ์„ฑ๋˜๋Š” ์œ„์น˜๋ฅผ ๋”ฐ๋กœ ์„ค์ •ํ•ด์ฃผ์ง€ ์•Š์€ ๊ฒฝ์šฐ ๊ธฐ๋ณธ ๊ฒฝ๋กœ์ธ build/reports/jacoco/test/html/index.html์— ์ €์žฅ๋˜๋ฉฐ ์ƒ์„ฑ๋œ html ๋ณด๊ณ ์„œ๋Š” ๊ฐ ์ปค๋ฒ„๋ฆฌ์ง€ ํ•ญ๋ชฉ๋งˆ๋‹ค ์ด ๊ฐœ์ˆ˜์™€ ๋†“์นœ ๊ฐœ์ˆ˜๋ฅผ ํ‘œ์‹œํ•ด ์ค€๋‹ค. 

 

Element๋ฅผ ํƒ€๊ณ  ๋“ค์–ด๊ฐ€๋ฉด ์–ด๋–ค ๋ฉ”์„œ๋“œ๊ฐ€ ํ…Œ์ŠคํŠธ๋˜์ง€ ์•Š์•˜๋Š”์ง€๋„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ œ์™ธ ํด๋ž˜์Šค ์ž๋™ํ™”

ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ ํ•˜์œ„์— coverage-exclude.asap ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  JaCoCo์—์„œ ์ œ์™ธํ•  ํŒŒ์ผ์ด๋‚˜ ํŒจํ‚ค์ง€๋ฅผ ์ž…๋ ฅํ•œ๋‹ค.

exclude com/chaewsstore/core/infra/*
exclude com/chaewsstore/*/common/*
exclude com/chaewsstore/*/common/*/*
exclude com/chaewsstore/*/config/*
exclude com/chaewsstore/*/config/*/*
exclude com/chaewsstore/*/*Application*
exclude com/chaewsstore/*/apis/*/service/*DetailService*

 

jacoco.gradle์— coverage-exclude.asap ํŒŒ์ผ์˜ ๋‚ด์šฉ์„ excludeFromCoverage ๋ฆฌ์ŠคํŠธ์— ์ €์žฅํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

import java.util.stream.Collectors

def excludeFromCoverage = new ArrayList<String>()
file('coverage-exclude.asap').withInputStream() { it ->
    excludeFromCoverage.addAll(new BufferedReader(new InputStreamReader(it))
            .lines()
            .parallel()
            .map(s -> s.substring(7).strip())
            .collect(Collectors.toList()))
}

 

 jacocoTestReport์™€ jacocoTestCoverageVerification์˜ excludes ๋ถ€๋ถ„์„ ์•„๋ž˜์™€ ๊ฐ™์ด ์ˆ˜์ •ํ•œ๋‹ค.

jacocoTestReport {
    reports { ... }
    afterEvaluate {
        classDirectories.setFrom(
                files(classDirectories.files.collect {
                    fileTree(dir: it, excludes: excludeFromCoverage.stream()
                            .map(s -> s + ".class")
                            .collect(Collectors.toList()))
                })
        )
    }
    finalizedBy 'jacocoTestCoverageVerification'
}

jacocoTestCoverageVerification {
    violationRules {
        rule {
            ...
            excludes += excludeFromCoverage.stream()
                    .map(s -> s.replace("/", "."))
                    .collect(Collectors.toList())
        }
    }
}

 

JaCoCo ์„ค์ • ์ „์ฒด ์ฝ”๋“œ

import java.util.stream.Collectors

def excludeFromCoverage = new ArrayList<String>()
file('coverage-exclude.asap').withInputStream() { it ->
    excludeFromCoverage.addAll(new BufferedReader(new InputStreamReader(it))
            .lines()
            .parallel()
            .map(s -> s.substring(7).strip())
            .collect(Collectors.toList()))
}

subprojects {
    apply plugin: 'jacoco'

    jacoco {
        toolVersion = '0.8.12'
    }

    test {
        useJUnitPlatform()
        finalizedBy 'jacocoTestReport'
    }

    jacocoTestReport {
        reports {
            html.required.set(true)
            xml.required.set(false)
            csv.required.set(false)
        }
        afterEvaluate {
            classDirectories.setFrom(
                    files(classDirectories.files.collect {
                        fileTree(dir: it, excludes: Qdomains + excludeFromCoverage.stream()
                                .map(s -> s + ".class")
                                .collect(Collectors.toList()))
                    })
            )
        }
        finalizedBy 'jacocoTestCoverageVerification'
    }

    jacocoTestCoverageVerification {
        violationRules {
            rule {
                enabled = true
                element = "CLASS"
                limit {
                    counter = "LINE"
                    value = "COVEREDRATIO"
                    minimum = 0.80
                }
                excludes += excludeFromCoverage.stream()
                        .map(s -> s.replace("/", "."))
                        .collect(Collectors.toList())
            }
        }
    }
}

 

๐Ÿ“‘ JaCoCo Report Aggregation

๋ฉ€ํ‹ฐ ๋ชจ๋“ˆ๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•  ๋•Œ, ๋ฃจํŠธ build.gradle์˜ allProjects ํ˜น์€ subProjects๋กœ ๋ชจ๋“  ๋ชจ๋“ˆ์— JaCoCo ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ ์šฉํ•  ๊ฒฝ์šฐ JaCoCo์˜ test code coverage reports๊ฐ€ ๋ชจ๋“ˆ๋ณ„๋กœ ์ƒ์„ฑ๋œ๋‹ค. ์ด๋ ‡๊ฒŒ ๋˜๋ฉด ํŠน์ • ๋ชจ๋“ˆ์˜ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ณด๊ณ ์„œ๋ฅผ ๋ณด๊ธฐ ์œ„ํ•ด์„œ๋Š” ํ•ด๋‹น ๋ชจ๋“ˆ์˜ build๋ฅผ ์—ด์–ด๋ด์•ผ ํ•œ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ด๋ ‡๊ฒŒ ๋ถ„๋ฆฌ๋œ jacoco coverage reports ํŒŒ์ผ์„ ํ•ฉ์น˜๊ธฐ ์œ„ํ•ด์„œ jacoco-aggregation ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

Report Aggregation ํ”Œ๋Ÿฌ๊ทธ์ธ ์ ์šฉ

๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ ํ•˜์œ„์— support/jacoco ๋ชจ๋“ˆ์„ ๋งŒ๋“ค์–ด :support:jacoco ํ”„๋กœ์ ํŠธ์—์„œ JaCoCo ๋ฆฌํฌํŠธ ์ง‘๊ณ„๋ฅผ ์„ค์ •ํ•œ๋‹ค.

project(':support:jacoco') {
    apply plugin: 'jacoco-report-aggregation'
}
โ”œโ”€โ”€ chaewsstore-admin  
โ”œโ”€โ”€ chaewsstore-app
โ”œโ”€โ”€ chaewsstore-core
โ”œโ”€โ”€ global-utils
โ”œโ”€โ”€ support
โ”‚       โ””โ”€โ”€ jacoco
โ”‚
โ”œโ”€โ”€ build.gradle
โ””โ”€โ”€ jacoco.gradle

 

์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ฆฌํฌํŠธ ์„ค์ •

JaCoCo ์„ค์ • ์ค‘ jacocoTestReport์˜ afterEvaluate๋ฅผ ํ†ตํ•ด ๋ณด๊ณ ์„œ์— ํ‘œ์‹œ๋˜๋Š” ํด๋ž˜์Šค ์ค‘ ์›ํ•˜๋Š” ํด๋ž˜์Šค๋ฅผ ์ œ์™ธํ•  ์ˆ˜ ์žˆ์—ˆ๋Š”๋ฐ, jacoco-aggregation์œผ๋กœ ํ†ตํ•ฉ๋œ report์—๋Š” ์—ฌ์ „ํžˆ exclude ํ•œ ํด๋ž˜์Šค๋“ค์ด ๋ณด์ด๋Š” ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธด๋‹ค.

 

์ด๋Š” jacoco.gradle ํŒŒ์ผ์—์„œ ์•„๋ž˜ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด jacoco-aggregation์—์„œ๋„ ํด๋ž˜์Šค๋ฅผ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๋‹ค.

testCodeCoverageReport {
    getClassDirectories().setFrom(files(allProjects
            .collect {
                it.fileTree(dir: "${it.buildDir}/classes/java/main", exclude:
                        excludeFromCoverage.stream()
                                .map(s -> s + ".class")
                                .collect(Collectors.toList()))
            }
    ))
}

 

์ข…์†์„ฑ ์ถ”๊ฐ€

support/jacoco ๋ชจ๋“ˆ์—์„œ ๋‹ค๋ฅธ ํ•˜์œ„ ๋ชจ๋“ˆ๋“ค์„ ์ข…์†์„ฑ์œผ๋กœ ์ถ”๊ฐ€ํ•ด jacoco-report-aggregation์ด ์ด ํ•˜์œ„ ๋ชจ๋“ˆ๋“ค์˜ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ฆฌํฌํŠธ๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  ์ง‘๊ณ„ํ•  ์ˆ˜ ์žˆ๋‹ค.

dependencies {
    implementation project(':chaewsstore-admin')
    implementation project(':chaewsstore-app')
    implementation project(':chaewsstore-core')
}

 

์ข…์†์„ฑ ์ถ”๊ฐ€ ์ž๋™ํ™”

์œ„์—์„œ์ฒ˜๋Ÿผ ์ข…์†์„ฑ์„ ํ•˜๋“œ์ฝ”๋”ฉํ•  ๊ฒฝ์šฐ ์ƒˆ๋กœ์šด ๋ชจ๋“ˆ์ด ์ƒ์„ฑ๋  ๋•Œ๋งˆ๋‹ค jacoco-aggregation์— ์ถ”๊ฐ€๋ฅผ ํ•ด์ฃผ์–ด์•ผ ํ•˜๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ƒˆ๋กœ์šด ๋ชจ๋“ˆ ์ƒ์„ฑ ์‹œ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•ด jacoco-aggregation ๋Œ€์ƒ์œผ๋กœ ๋“ฑ๋กํ•ด ์ฃผ๋Š” ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด ์ฃผ์—ˆ๋‹ค.

def allProjects = getAllprojects().stream()
        .filter(p -> !p.getDisplayName().contains('root project'))
        .collect(Collectors.toList())

project(':support:jacoco') {
    apply plugin: 'jacoco-report-aggregation'

    testCodeCoverageReport { ... }

    def allProjectsExcludeJacoco = allProjects.stream()
            .filter(p -> !p.getDisplayName().contains('jacoco')
                    && !p.getDisplayName().contains('root project'))
            .collect(Collectors.toList())

    dependencies {
        allProjectsExcludeJacoco.each { project ->
            implementation project
        }
    }
}

 

JaCoCo Aggregation ์„ค์ • ์ „์ฒด ์ฝ”๋“œ

def allProjects = getAllprojects().stream()
        .filter(p -> !p.getDisplayName().contains('root project'))
        .collect(Collectors.toList())

project(':support:jacoco') {
    apply plugin: 'jacoco-report-aggregation'

    testCodeCoverageReport {
        getClassDirectories().setFrom(files(allProjects
                .collect {
                    it.fileTree(dir: "${it.buildDir}/classes/java/main", exclude:
                            excludeFromCoverage.stream()
                                    .map(s -> s + ".class")
                                    .collect(Collectors.toList()))
                }
        ))
    }

    def allProjectsExcludeJacoco = allProjects.stream()
            .filter(p -> !p.getDisplayName().contains('jacoco')
                    && !p.getDisplayName().contains('root project'))
            .collect(Collectors.toList())

    dependencies {
        allProjectsExcludeJacoco.each { project ->
            implementation project
        }
    }
}

 

๐Ÿ” ์ถ”๊ฐ€์ ์ธ ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ถ„์„ ๋Œ€์ƒ ์ œ์™ธ

QClass

JPQ์™€ QueryDSL์„ ์‚ฌ์šฉํ•˜๋ฉด Q domain์ด ์ƒ๊ธฐ๋Š”๋ฐ ํ•ด๋‹น ๋ถ€๋ถ„์€ ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ถ„์„ ๋Œ€์ƒ์—์„œ ์ œ์™ธํ•ด์•ผ ํ•œ๋‹ค.

jacoco.gradle์— ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , excludes์— Qdomains๋ฅผ ์ถ”๊ฐ€ํ•ด ์ค€๋‹ค.

def Qdomains = []
for(qPattern in "**/QA" .. "**/QZ"){
    Qdomains.add(qPattern+"*")
}

 

Generated ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€

lombok ๊ด€๋ จ ๋ฉ”์„œ๋“œ

lombok์œผ๋กœ ์ž๋™ ์ƒ์„ฑ๋˜๋Š” Getter, NoArgsConstructor, Builder ๋“ฑ์˜ ๋ฉ”์„œ๋“œ๋„ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€์— ์ธก์ •๋˜์–ด ๋นŒ๋“œ์— ์‹คํŒจํ•  ์ˆ˜ ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ lombok ๊ด€๋ จ ๋ฉ”์„œ๋“œ๋“ค์„ ์ธก์ •์—์„œ ์ œ์™ธํ–ˆ๋‹ค.

 

ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ ํ•˜์œ„์— lombok.config ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ์•„๋ž˜์˜ ์„ค์ •์„ ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

lombok.addLombokGeneratedAnnotation = true

์ด ์„ค์ •์€ ๋ชจ๋“  lombok์œผ๋กœ ์ƒ์„ฑ๋œ ๋ฉ”์„œ๋“œ์— ๋Œ€ํ•ด 'Generated' ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์ธ๋‹ค๋Š” ์„ค์ •์ด๋‹ค.

 

์ด์™ธ ๋ฉ”์„œ๋“œ

ํ”„๋กœ์ ํŠธ์—์„œ ์žฌ์ •์˜ํ•œ equals์™€ hasoCode๋Š” ํ…Œ์ŠคํŠธํ•  ํ•„์š”๊ฐ€ ์—†์–ด ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์— ์ถ”๊ฐ€ํ•˜์ง€ ์•Š์•˜๋Š”๋ฐ, ์ด๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€์— ์ธก์ •๋˜์–ด ๋นŒ๋“œ๊ฐ€ ์‹คํŒจํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค. ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์œ„ํ•ด ๋ถˆํ•„์š”ํ•˜๊ฒŒ ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ๋ฉ”์„œ๋“œ๋ฅผ ์ธก์ •์—์„œ ์ œ์™ธํ•˜๋Š” ๊ฒƒ์ด ๋‚ซ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.

 

JaCoCo ์„ค์ •์—์„œ exclude ๊ฐ€๋Šฅํ•œ ํ•ญ๋ชฉ์€ element์—์„œ ์ •์˜๋œ CLASS์ด๋‹ค. ํ•˜์ง€๋งŒ JaCoCo 0.8.2๋ถ€ํ„ฐ๋Š”

  1. 'Generated'๋ผ๋Š” ๋‹จ์–ด๊ฐ€ ์ด๋ฆ„์— ํฌํ•จ
  2. RetentionPolicy๊ฐ€ 'CLASS' ๋˜๋Š” 'Runtime'

์„ ์ถฉ์กฑํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ์œผ๋ฉด ํ•ด๋‹น ํƒ€๊ฒŸ์€ JaCoCo ์ธก์ •์—์„œ ์ œ์™ธํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ์œ„ ๊ธฐ์ค€์„ ์ถฉ์กฑํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์„ ๋งŒ๋“ค๊ณ , ํ…Œ์ŠคํŠธ๊ฐ€ ๋ถˆํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ์— ํ•ด๋‹น ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•ด ์ฃผ์—ˆ๋‹ค.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Generated {

}
@Override
@Generated
public final boolean equals(Object o) {
    ...
}

@Override
@Generated
public final int hashCode() {
    ...
}

์ด ๋ฐฉ๋ฒ•์€ ํ•„์š”ํ•œ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋งŒ ์ปค๋ฒ„๋ฆฌ์ง€ ์ธก์ •์„ ํ•จ์œผ๋กœ์จ ์ •ํ™•ํ•œ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ถ„์„์„ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด ์ฃผ์ง€๋งŒ ์„œ๋น„์Šค ์ฝ”๋“œ์— ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋กœ์ง์ด ๋“ค์–ด๊ฐ„๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.

 

๐Ÿค” Generated ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์œผ๋ฉด ์™œ report์—์„œ ์ œ์™ธ๋ ๊นŒ?

lombok ๊ด€๋ จ ๋ฉ”์„œ๋“œ์™€ ํ…Œ์ŠคํŠธ๊ฐ€ ๋ถˆํ•„์š”ํ•œ ๋ฉ”์„œ๋“œ์— @Generated ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์—ฌ ์ธก์ •์—์„œ ์ œ์™ธํ•  ์ˆ˜ ์žˆ๋Š” ์ด์œ ๋Š” lombok์˜ ๋ฉ”์„œ๋“œ ์ƒ์„ฑ ์‹œ์ ๊ณผ JaCoCo์˜ ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ์ธก์ • ๋ฐฉ์‹์— ์žˆ๋‹ค.

 

๐Ÿซง lombok ๋ฉ”์„œ๋“œ ์ƒ์„ฑ ์‹œ์ 

lombok ๋ฉ”์„œ๋“œ๋Š” ์ปดํŒŒ์ผ ์‹œ ์ƒ์„ฑ(annotation processing)๋œ๋‹ค. ์ฆ‰, ์†Œ์Šค ์ฝ”๋“œ๊ฐ€ ์ปดํŒŒ์ผ๋  ๋•Œ lombok์ด ์ž๋™์œผ๋กœ ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š”๋ฐ, ์ด๋•Œ ์ด ๋ฉ”์„œ๋“œ์— @Generated ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์ธ๋‹ค.

 

๐Ÿซง JaCoCo ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ์ธก์ • ๋ฐฉ์‹

JaCoCo๋Š” Java Agent๋กœ ์‹คํ–‰๋˜์–ด ๋ฐ”์ดํŠธ ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ๊ด€๋ฆฌํ•˜๊ณ , ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ค‘ ์–ด๋–ค ์ฝ”๋“œ ๋ผ์ธ์ด ์ˆ˜ํ–‰๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋ฉฐ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์ธก์ •ํ•œ๋‹ค. JaCoCo๋Š” ์‹คํ–‰๋˜๋Š” ๋ฐ”์ดํŠธ ์ฝ”๋“œ์˜ ๊ฐ ๋ผ์ธ์„ ์ถ”์ ํ•˜์—ฌ ์ปค๋ฒ„๋ฆฌ์ง€ ๋น„์œจ์„ ๊ณ„์‚ฐํ•˜๋Š”๋ฐ, ์ด ๊ณผ์ •์—์„œ @Generated๊ฐ€ ๋ถ™์€ ๋ฉ”์„œ๋“œ๋ฅผ ์ธ์‹ํ•˜๊ณ  ์ด๋ฅผ ์ธก์ • ๋Œ€์ƒ์—์„œ ์ œ์™ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

 

๐Ÿ“š ์ฐธ๊ณ 

The JaCoCo Plugin

Jacoco ๋ณด๊ณ ์„œ์—์„œ ์ œ์™ธ

์ขŒ์ถฉ์šฐ๋Œ Jacoco ์ ์šฉ๊ธฐ

์ฝ”๋“œ ๋ถ„์„ ๋„๊ตฌ ์ ์šฉ๊ธฐ - 2ํŽธ, JaCoCo ์ ์šฉํ•˜๊ธฐ

์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ํˆด ์ ์šฉ๊ธฐ(feat. JaCoCo)

[Jacoco] ๋ฉ€ํ‹ฐ ๋ชจ๋“ˆ์˜ Jacoco report ๋ฅผ ํ•˜๋‚˜๋กœ ํ•ฉ์น˜๊ธฐ

JaCoCo ํ™œ์šฉํ•ด๋ณด๊ธฐ