[์ฝ๋๋ถ์๋๊ตฌ]#2 Jacoco ์ ์ฉํ๊ธฐ
by JiwonDev์ธ๋ค์ผ์ ๋ฑํ ๋ฃ์๊ฒ ์์ด์ ์ ํจ์ค๋ฅผ ๋ฃ์๋ค. ๊ทธ๋ฅ CI ๋๊ตฌ๋ผ๋ ๊ด์ ์์..?
JaCoCo is a free code coverage library for Java, which has been created by the EclEmma team based on the lessons learned from using and integration existing libraries for many years
- JaCoCo
Jacoco๋ Java Code์ ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์ฒดํฌํ๋๋ฐ ์ฌ์ฉ๋๋ ์คํ์์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
# Jacoco์ ํน์ง
์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง์ ์ต์ํ์ง ์์ ๋๋ ์ปค๋ฒ๋ฆฌ์ง ๊ธฐ์ค์ ๋ฎ์ถฐ์ ์ฌ์ฉํ๋ค๊ฐ ์ ์ ๊ธฐ์ค์ ๋์ด๋ ๊ฒ์ ์ถ์ฒํ๋ค. Jacoco๋ ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ํ๊ฐํ๊ณ ํ๋ก์ ํธ ๋น๋ ์ฑ๊ณต/์คํจ๋ฅผ ๊ฒฐ์ ํ ์ ์๊ฒ ๋ง๋ค์ด์ค๋ค.
- ๋ผ์ธ ์ปค๋ฒ๋ฆฌ์ง(๋ชจ๋ ์ฝ๋๊ฐ ์คํ๋๋๊ฐ)์ ๋ธ๋ ์น ์ปค๋ฒ๋ฆฌ์ง(๋ชจ๋ ์กฐ๊ฑด๋ถ๊ธฐ๋ฅผ ์ฌ์ฉํ๋๊ฐ)๋ฅผ ์ ๊ณต.
- ํ ์คํธ์ฝ๋๋ฅผ ๋๋ฆฌ๊ณ ์ปค๋ฒ๋ฆฌ์ง ๊ธฐ์ค์ ๋ง์กฑํ๋์ง ํ์ธํ ์ ์๋ค.
- ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง ๊ฒฐ๊ณผ๋ฅผ ์ ๋ฆฌ๋ ํ์ผ(html, xml, csv๋ฑ)์ผ๋ก ์ ์ฅํ ์ ์๋ค.
Jacoco๋ฅผ ํตํด ์ฝ๋์ปค๋ฒ๋ฆฌ์ง ๊ฒฐ๊ณผ๋ฅผ csv, xml๋ก ์ ์ฅํ๋ฉด ์ด๋ฅผ ์๋ํ๋ธ(SonarQube) ๊ฐ์ ๋๊ตฌ์ ์ ๋ฌํด ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ์ปค๋ฐ์ฑ๊ณต/์คํจ๋ฅผ ๊ฒฐ์ ํ๋ ์์ผ๋ก ์ฌ์ฉ๋๋ค. ์ฐธ๊ณ ๋ก html์ ์ฌ์ฉํ๋ฉด ์ด๋ ๋ถ๋ถ์ด ๋ฌธ์ ์ธ์ง ๊ฐ๋ฐ์๊ฐ ํ๋์ ํ์ ํ๊ธฐ ์ฝ๊ฒ ๋ณด๊ณ ์๋ก ๋ง๋ค์ด์ค๋ค.
# Jacoco ์ ์ฉํ๊ธฐ
Java + SpringBoot + Gradle๋ก ์ด๋ฃจ์ด์ง ํ๋ก์ ํธ์ ์ ์ฉํด๋ณด์. ์คํ๋ง๋ถํธ๋ฅผ ์ฌ์ฉํ๋ฉด build.gradle ์ค์ ๋ง์ ์ด์ฉํด์ ์ฝ๊ฒ ์ ์ฉ๊ฐ๋ฅํ๋ค.
1. Jacoco๋ฅผ ์ ์ฒด ํ๋ก์ ํธ๋ฅผ ํ๋๋ก ์ผ๊ด์ ์ฉํ๊ณ ์ถ๋ค๋ฉด ์๋์ ๊ฐ์ดํ๋ฉด ๋๋ค.
//build.gradle plugins { id 'java' id 'org.springframework.boot' version '2.3.1.RELEASE' ... id 'jacoco' // ์ถ๊ฐ! } jacoco { // JaCoCo ๋ฒ์ toolVersion = '0.8.6' // ํ
์คํธ๊ฒฐ๊ณผ ๋ฆฌํฌํธ๋ฅผ ์ ์ฅํ ๊ฒฝ๋ก // ๋ณ๊ฒฝํ๊ณ ์ ํ๋ฉด ์๋์ ๊ฐ์ด ์์ฑ default๋ "${project.reporting.baseDir}/jacoco" reportsDir = file("$buildDir/customJacocoReportDir") }
2. ๊ทธ๊ฒ ์๋๋ผ ๊ฐ ๋ชจ๋๋ณ๋ก ๋ฐ๋ก Jacoco๋ฅผ ์ ์ฉํ๊ณ ์ถ๋ค๋ฉด, ์๋์ ๊ฐ์ด ์ค์ ํด์ฃผ์.
// build.gradle plugins { id 'java' id 'org.springframework.boot' version '2.3.1.RELEASE' ... } allprojects {...} subprojects { apply plugin: 'java' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' apply plugin: 'org.asciidoctor.convert' apply plugin: 'jacoco'// ์ฌ๊ธฐ์ ์ถ๊ฐ! ... } jacoco {...}
๊ทธ๋ฌ๋ฉด ์๋์ ๊ฐ์ด ๊ฐ ๋ชจ๋์ Gradle ๋๋ ํ ๋ฆฌ์ Jacoco ์์กด์ฑ์ด ์ถ๊ฐ๋๋ค.
- jacocoTestReport : ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์งํํ๊ณ ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์ฌ๋์ด ์ฝ๊ธฐ ์ข์ ๋ฆฌํฌํธ๋ก ์ ์ฅํด์ฃผ๋ ๋๊ตฌ
- jacocoTestCoverageVerification : ์ํ๋ ์ปค๋ฒ๋ฆฌ์ง ๊ธฐ์ค์ ๋ง์กฑํ๋์ง ํ์ธํด์ฃผ๋ ๊ฒ์ฆ ๋๊ตฌ

์ด ์ค์ ์ build.gradle์์ ํ ์ ์๋๋ฐ, ํ๊ฐ์ง ์ฃผ์ํ ์ ์ jacoco๋ ํ ์คํธ ์ดํ ์ ์ฉ๋๋ ํ๋ฌ๊ทธ์ธ์ด๋ผ๋ ์ ์ด๋ค. ์ฆ [ Junit Test -> jacocoTestReport -> jacoco...CoverageVerification ] ์์ผ๋ก ์คํ๋์ด์ผ ํ๊ธฐ ๋๋ฌธ์ ์๋์ ๊ฐ์ด ํ ์คํธ ์ค์ ์ ๊ณ ์ ์์ผ์ฃผ์. ์ด๋ฅผ ์๋ตํ๊ณ ์ํด์ฃผ๋ฉด ์ด์ ํ ์คํธ๊ฒฐ๊ณผ๋ฅผ ๋ฐ๋ณตํด์ ์ฝ์ด ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ๊ณ์ ํต๊ณผ ๋ชปํ๋ ๊ฒฝ์ฐ๊ฐ ๋ฐ์ํ ์ ์๋ค.
//build.gradle test { useJUnitPlatform() // JUnit5๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํ ์ค์ finalizedBy 'jacocoTestReport' // Test ์ดํ ์ปค๋ฒ๋ฆฌ์ง๊ฐ ๋์ํ๋๋ก finalizedBy ์ถ๊ฐ } jacoco { toolVersion = '0.8.6' } jacocoTestReport { reports { html.enabled true csv.enabled true xml.enabled false } finalizedBy 'jacocoTestCoverageVerification' // ์ปค๋ฒ๋ฆฌ์ง ์๋ ์ดํ ๊ฒ์ฆํ๋๋ก ์ค์ }
# jacocoTestReport (์ปค๋ฒ๋ฆฌ์ง ์ธก์ )
build.gradle์์ jacocoTestReport {...}์ ์ค์ ๊ฐ๋ฅํ๋ค. ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง ๊ฒฐ๊ณผ๋ฅผ ์ด๋ป๊ฒ ์ ์ฅํ ์ง ์ค์ ํ๋ค.
//build.gradle ... jacoco { toolVersion = '0.8.6' } jacocoTestReport { reports { csv.enabled true // csv ์ค์ xml.enabled false // xml ๋ฏธ์ค์ html.enabled true // html ์ค์ // ํ์ผ ํ์์ ๋ฐ๋ผ ์ ์ฅ๊ฒฝ๋ก๋ฅผ ๋ฐ๋ก ์ง์ ํด์ค์๋ ์๋ค. html.destination file('build/reports/myReport.html') ... } }
๋ฆฌํฌํธ๋ฅผ ์์ฑํ ๋ ํน์ ํ์ผ์ ์ ์ธํ๊ณ ์ถ๋ค๋ฉด, ์๋์ ๊ฐ์ด ์์ฑํ๋ฉด ๋๋ค.
jacocoTestReport { reports { html.enabled true csv.enabled true xml.enabled false } def Qdomains = [] for (qPattern in '**/QA'..'**/QZ') { // qPattern = '**/QA', '**/QB', ... '*.QZ' Qdomains.add(qPattern + '*') // ~/QA* , QA๋ก ์์ํ๋ ๋ชจ๋ ํด๋์ค } // ์ฌ๊ธฐ๋ถํฐ afterEvaluate { classDirectories.setFrom( files(classDirectories.files.collect { fileTree(dir: it, excludes: [] + Qdomains) }) ) } finalizedBy 'jacocoTestCoverageVerification' }
- afterEvaluate๋ gradle์ ๋น๋ ๋ผ์ดํ ์ฌ์ดํด์ ๋ํ ๋ฉ์๋. ํ๋ก์ ํธ๊ฐ ํ๊ฐ๋ ํ ์คํํ ์ ์๋๋ก ๋์์ค๋ค.
- classDirectories๋ ์ปค๋ฒ๋ฆฌ์ง๊ฐ ๋ฆฌํฌํธ๋ก ์์ฑํ ์์ค ํ์ผ์ ๋งํ๋ค. (์ฌ๊ธฐ์๋ setFrom ๋ฉ์๋๋ฅผ ํตํด ์ด๋ฅผ ์ค์ )
- files๋ ์ง์ ๋ ํ์ผ์ ํฌํจํ๋ ConfigurableFileCollection ํ์ ์ ๋ฐํ.
์ด๋ฐ์์ผ๋ก ๋ฉ์๋๋ฅผ ํตํด ๋ฆฌํฌํธ์ ์ ์ธํ excludes ๋ชฉ๋ก์ ์ถ๊ฐ๋ก ์ง์ ํด์ค ์ ์๋ค.
# jacocoTestCoverageVerification ( ์ ์ํ๊ฐ, ๊ฒ์ฆ )
์ต์ ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์ค์ ํ ์ ์๊ณ , ํต๊ณผํ์ง ๋ชปํ ๊ฒฝ์ฐ ํด๋น Task๊ฐ ์คํจํ๊ฒ ๋๋ค.
jacoco...verification { violationRules{~} } ๋ฅผ ํตํด ์ปค๋ฒ๋ฆฌ์ง ๊ธฐ์ค ๊ท์น์ ์ ํ ์ ์๋ค.
- rule { ... } ๊ท์น์ ์ ๋ ๊ณต๊ฐ. ์ฌ๋ฌ๊ฐ๋ฅผ ์์ฑํ ์ ์๊ณ enable ์ต์ ์ผ๋ก on/off ํ ์ ์๋ค.
- element = ์ ์ฉํ ํ์
๋๋ ๋์์ ์ง์ ํ๋ค (์ ์ฒด, ํด๋์ค, ๋ฉ์๋, ํ ํ์ผ ๋ฑ๋ฑ..)
- BUNDLE(๊ธฐ๋ณธ๊ฐ) : ํจํค์ง ๋ฒ๋ค(ํ๋ก์ ํธ ๋ชจ๋ ํ์ผ์ ํฉ์น ๊ฒ)
- CLASS : ํด๋์ค
- GROUP : ๋ ผ๋ฆฌ์ ๋ฒ๋ค ๊ทธ๋ฃน
- METHOD : ๋ฉ์๋
- PACKAGE : ํจํค์ง
- SOURCEFILE : ์์ค ํ์ผ
- includes, exclude = ์์ฑํ๋ฉด ์ ์ธ ๋์/์ ์ฉ ๋์์ ํด๋์ค๋ ํจํค์ง ๊ฒฝ๋ก๋ก ํ์ ํ ์ ์๋ค.
- ์ ๊ทํํ์์ธ * ์ ? ๋ฅผ ์ฌ์ฉํ ์ ์๋ค. import ํ๋ฏ์ด ๋ฆฌ์คํธ๋ฅผ ์ ์ผ๋ฉด ๋๋ค.
jacocoTestCoverageVerification { violationRules { rule { // 1๋ฒ ๊ท์น enable = true // ๊ท์น ํ์ฑํ์ฌ๋ถ element = 'CLASS' // ์ ์ฉ ๋์์ ์ง์ ํ๋ค. ๊ธฐ๋ณธ๊ฐ์ BUNDLE (์ ์ฒด ํจํค์ง) // BUNDLE, CLASS, GROUP(๋
ผ๋ฆฌ์ ๋ฒ๋ค๊ทธ๋ฃน), METHOD, PACKAGE, SOURCEFILE ๋ฑ // ํด๋น ๊ท์น์ ์ ์ฉ๋์์ ํด๋์ค, ํจํค์ง ๋จ์๋ก ์ง์ ๊ฐ๋ฅ // includes = [], ๋จ include๋ ์๋ตํ๋ฉด ์ ์ฒด ํจํค์ง์ ์ผ๊ด ์ ์ฉ // excludes = [], ์ ์ธํ ํจํค์ง/ํด๋์ค๋ฅผ ์ง์ limit { counter = 'BRANCH' // ์ปค๋ฒ๋ฆฌ์ง ์ข
๋ฅ value = 'COVEREDRATIO' // ๊ฐ (์ ์ฒด ๋น์จ) minimum = 0.90 // ์ต์ ๊ธฐ์ค 90% } limit { counter = 'LINE' value = 'COVEREDRATIO' minimum = 0.80 // ์ต์ ๊ธฐ์ค 80% } limit { counter = 'LINE' value = 'TOTALCOUNT' // ๊ฐ(์ ์ฒด ๊ฐ์) maximum = 200 // ์ต์ ๊ธฐ์ค 200์ค ์ดํ } } // ์ฌ๋ฌ rule์ ์์ฑํ ์ ์์ต๋๋ค. rule { // 2๋ฒ ๊ท์น ... } } }
- limit = ์ปค๋ฒ๋ฆฌ์ง ์ธก์ ์ต์๋จ์๋ฅผ ์ง์ ํ๋ค.
limit { counter = 'BRANCH' // ์ปค๋ฒ๋ฆฌ์ง ์ข
๋ฅ value = 'COVEREDRATIO' // ๊ฐ (์ ์ฒด ๋น์จ) minimum = 0.90 // ์ต์ ๊ธฐ์ค 90% }
Counter
- BRANCH : ์กฐ๊ฑด๋ฌธ ๋ฑ์ ๋ถ๊ธฐ ์
- CLASS : ํด๋์ค ์, ๋ด๋ถ ๋ฉ์๋๊ฐ ํ ๋ฒ์ด๋ผ๋ ์คํ๋๋ค๋ฉด ์คํ๋ ๊ฒ์ผ๋ก ๊ฐ์ฃผํ๋ค.
- COMPLEXITY : ์ฝ๋ ๋ณต์ก๋(๋งํฌ ์ฐธ์กฐ)
- INSTRUCTION(๊ธฐ๋ณธ๊ฐ) : Java ๋ฐ์ดํธ์ฝ๋ ๋ช ๋ น ์
- METHOD : ๋ฉ์๋ ์, ๋ฉ์๋๊ฐ ํ ๋ฒ์ด๋ผ๋ ์คํ๋๋ค๋ฉด ์คํ๋ ๊ฒ์ผ๋ก ๊ฐ์ฃผํ๋ค.
- LINE : ๋น ์ค์ ์ ์ธํ ์ค์ ์ฝ๋์ ๋ผ์ธ ์, ๋ผ์ธ์ด ํ ๋ฒ์ด๋ผ๋ ์คํ๋๋ฉด ์คํ๋ ๊ฒ์ผ๋ก ๊ฐ์ฃผํ๋ค.
Value
- COVEREDCOUNT : ์ปค๋ฒ๋ ๊ฐ์
- COVEREDRATIO(๊ธฐ๋ณธ๊ฐ) : ์ปค๋ฒ๋ ๋น์จ, 0๋ถํฐ 1์ฌ์ด์ ์ซ์๋ก [1 = 100%]์ด๋ค.
- MISSEDCOUNT : ์ปค๋ฒ๋์ง ์์ ๊ฐ์
- MISSEDRATIO : ์ปค๋ฒ๋์ง ์์ ๋น์จ, 0๋ถํฐ 1์ฌ์ด์ ์ซ์๋ก [1 = 100%]์ด๋ค.
- TOTALCOUNT : ์ ์ฒด ๊ฐ์
Minimum
- ์์ Value๋ก ์ง์ ํ ํ์ ์ ์ต์๊ธฐ์ค๊ฐ์ ์ ๋ ฅํ๋ฉด ๋๋ค. jacocoTest์ ์ฑ๊ณต์ฌ๋ถ๊ฐ ๊ฒฐ์ ๋๋ ๊ธฐ์ค๊ฐ์ด๋ฉฐ ํ๊ธฐํ ์๋ฆฟ ์๋งํผ value๊ฐ ์ธ๋ฐํ๊ฒ ์ธก์ ๋๋ค. ์ฆ 0.80์ ์ ๋ ฅํ๋ฉด ์์ซ์ ๋์งธ์๋ฆฌ๊น์ง, 0.8์ ์ ๋ ฅํ๋ฉด ์ฒซ์งธ ์๋ฆฌ๊น์ง๋ง ์ธก์ ํ๋ฉฐ ๊ทธ ์ดํ์ ๊ฐ๋ค์ ๋ฒ๋ ค์ง๋ค. (๋ฐ์ฌ๋ฆผ ์๋)
# Test ์ค์ ํ๊ธฐ
๋ชจ๋ ํ ์คํธ ํ์ ์ JacocoTaskExtension์ ์ถ๊ฐํ๊ณ , test {...}์์ ์ค์ ์ ๋ณ๊ฒฝํ ์ ์๋ค.
์ด๋ ๊ฒ ์ค์ ํ Jacoco ์ปค๋ฒ๋ฆฌ์ง๋ ๊ธฐ์ค์ ๋ง์กฑํ์ง๋ชปํ๋ฉด ํ๋ก์ ํธ๊ฐ gradle์ ๋น๋๋์ง ์๋๋ค.
//build.gradle test { // ๊ฐ ์ค์ ๊ฐ์ ์ค๋ฒ๋ผ์ด๋๊ฐ ๊ฐ๋ฅํ๋ค. jacoco { enabled = true destinationFile = file("$buildDir/jacoco/${name}.exec") includes = [] excludes = [] excludeClassLoaders = [] includeNoLocationClasses = false sessionId = "<auto-generated value>" dumpOnExit = true classDumpDir = null output = JacocoTaskExtension.Output.FILE address = "localhost" port = 6300 jmx = false } }



๋ง์ฝ ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง์ ์ ์ธํ๊ณ ์ถ์ ์ด๋ฆ๊ท์น์ด ์๋ค๋ฉด, ๊ทธ๋ฃจ๋น ๋ฌธ๋ฒ์ ์ฌ์ฉํด์ exclude= [...]์ ์ ์ฉ์์ผ์ฃผ์.
jacocoTestCoverageVerification { def Qdomains = [] // ์ง์ ํ๋ํ๋ ์ง์ ํด์ค๋ ๋๊ณ , ์ด๋ฐ์์ผ๋ก ํจํด์ ์ด์ฉํด๋ ๋๋ค. for (qPattern in '*.QA'..'*.QZ') { // qPattern = '*.QA', '*.QB', ... '*.QZ' Qdomains.add(qPattern + '*') } violationRules { rule { ... excludes = [] + Qdomains // ์ ์ธํ Qdomains ํจํด ์ถ๊ฐ } } } }
# lombok์ผ๋ก ์์ฑ๋ ์ฝ๋ ์ ์ธํ๊ธฐ
๊ทธ๋ฅ lombok ์ค์ ํ๋ ์ถ๊ฐํด์ฃผ๋ฉด ๋๋ค. ๊ฐ๋จ
//build.gradle lombok.config{ lombok.addLombokGeneratedAnnotation = true }
# ์ ์ฒด ์ค์ ํ๋์ ๋ณด๊ธฐ
์ฐธ๊ณ ๋ก ๋ชจ๋์ ์ฌ๋ฌ๊ฐ ์ฌ์ฉํ ๋, Jacoco๊ฐ ์คํ๋์ค ์ปค๋ฒ๋ฆฌ์ง ๊ธฐ์ค๋ฏธ๋ฌ์ธ ๋ชจ๋์ ๋ฐ๊ฒฌํ๋ฉด ๋ฐ๋ก ์คํ์ ๋ฉ์ถ๋ค. ๊ทธ๋์ report๊ฐ ์์ฑ๋์ง ์๋ ๊ฒฝ์ฐ๋ ์๋๋ฐ ๋ง์ฝ ์คํจ์ฌ๋ถ์ ์๊ด์์ด ๊ฐ๋ณ์ ์ผ๋ก ์ ๋ถ ๋๋ ค๋ณด๊ณ ์ถ๋ค๋ฉด /gradlew test์ --continue๋ฅผ ์ถ๊ฐํ๋ฉด ๋๋ค.
/*...*/ subprojects { apply plugin: 'java' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' apply plugin: 'org.asciidoctor.convert' apply plugin: 'jacoco' // ๋ฉํฐ ๋ชจ๋ ์ ์ฉ sourceCompatibility = '1.8' repositories { mavenCentral() } test { useJUnitPlatform() finalizedBy 'jacocoTestReport' // test ์ดํ ์ปค๋ฒ๋ฆฌ์ง ์คํ } jacoco { toolVersion = '0.8.6' } jacocoTestReport { reports { html.enabled true csv.enabled true xml.enabled false } def Qdomains = [] for (qPattern in '**/QA'..'**/QZ') { // qPattern = '*.QA', '*.QB', ... '*.QZ' Qdomains.add(qPattern + '*') } afterEvaluate { // report ์ ์ธ๋์ ์ง์ classDirectories.setFrom( files(classDirectories.files.collect { fileTree(dir: it, excludes: [] + Qdomains) }) ) } finalizedBy 'jacocoTestCoverageVerification' // ์ปค๋ฒ๋ฆฌ์ง ์ดํ ํ๊ฐ } jacocoTestCoverageVerification { // ์ปค๋ฒ๋ฆฌ์ง ๊ธฐ์ค๊ฐ ์์ธ์ค์ def Qdomains = [] for (qPattern in '*.QA'..'*.QZ') { // qPattern = '*.QA', '*.QB', ... '*.QZ' Qdomains.add(qPattern + '*') } violationRules { rule { enabled = true element = 'CLASS' limit { counter = 'LINE' value = 'COVEREDRATIO' minimum = 0.00 } limit { counter = 'BRANCH' value = 'COVEREDRATIO' minimum = 0.00 } excludes = [] + Qdomains // ์ปค๋ฒ๋ฆฌ์ง ํ๊ฐ ์ ์ธ ๋์ } } } } /*...*/
๋ค์ ๊ธ์์๋ Jacoco์ ์ฝ๋์ปค๋ฒ๋ฆฌ์ง ๊ฒฐ๊ณผ๋ฅผ ์ด์ฉํด์ ์ ์ ๋ถ์ CI ๋๊ตฌ์ธ SonarQube์ ์ฐ๋ํด๋ณด๋๋ก ํ๊ฒ ๋ค. ์ ์ ๋ถ์๋๊ตฌ์ Jacoco์ ๋ค๋ฅธ์ ์, Jacoco๋ ๋จ์ํ ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง ๊ฒฐ๊ณผ๋ฅผ ์ด์ฉํด์ ๋น๋ ์ฑ๊ณต/์คํจ์ฌ๋ถ๋ฅผ ์ธก์ ํ๋๊ฒ ๋์ด๋ผ๋ฉด SonarQube๋ ์ฝ๋ ํ์ง๊ฒ์ฌ๋ฅผ ํตํด ๋ฒ๊ทธ, ์ ๋ํ ์คํธ, ์ฃผ์, ๋ณต์ก๋, ํ์ค ์ปจ๋ฒค์ (์ฝ๋ Smell ์ธก์ )๋ฑ์ ๋ถ์ํด ์ทจ์ฝ์ ๊ณผ ๋ณด๊ณ ์๋ฅผ ์ ๊ณตํ๋ค.
๋ธ๋ก๊ทธ์ ์ ๋ณด
JiwonDev
JiwonDev