[쿠링] SonarCloud와 CI 도입
해당 글은 개인 프로젝트를 개선해 나가면서 내용을 정리하는 글입니다.
1. 도입 배경
기존의 쿠링의 배포 과정에서 CI 과정이 없다는 점이 아쉬웠다.
따라서 이번에는 SonarCloud를 통한 정적분석과 코드 커버리지 측정을 위한 CI과정을 추가해야겠다는 생각이 들었다!
SonarCloud공식문서를 보면서 하나하나 해결해봅시다~
https://docs.sonarcloud.io/advanced-setup/ci-based-analysis/github-actions-for-sonarcloud/
2. 도입 과정
2 - 1) SonarCloud 도입
먼저 SonarCloud에서 분석할 Repository의 상위 Organization account에 SonarCloud를 설치해야 합니다.
본인의 코드가 아님에도 불구하고, 코드분석을 요구하게 될 경우 보안적인 이슈가 많을 것이므로 많은 보안 정책을 따라주어야 해요.
따라서 SonarCloud에서 분석을 진행하기 위해서는 repository를 관리하는 상위단의 Organization account에 SonarCloud를 설치해 두는 것이죠.
따라서 여러 사람이 함께 작업하는 Organization이라면, 해당 Organization에서 admin권한이 있으셔야 합니다!
이후 SonarCloud 공식 홈페이지의 dashboard로 들어가서 로그인을 한 후, 우리가 원하는 organization을 선택하여 줍니다!
이후 Organization의 이름과 key를 지정한 후, plan을 선택해야 하는데, 저는 개인용으로 사용할 거라 무료를 선택하였습니다!
이후 직전에 만들어준 organization을 선택한 후, 원하는 Repository를 선택하여 set up을 눌러줍니다!
이렇게 하면 간단하게 도입 까지는 완료되게 됩니다!
분석을 진행할 repository를 선택하면 자동으로 Automatic Analysis가 진행됩니다.
고칠점이 상당하게 많은것 같네요.... 향후 개선 해 나가도록 해봅시다 ㅎㅎ
2 - 2) CI based Analsis로 전환하기
저희는 Automatic Analysis를 사용하는 것이 아닌 CI-based Anaylsis를 사용하기 때문에 해당 옵션을 꺼주어야 해요.
(SonarCloud에서는 두 가지의 Anaylsis를 중복해서 사용하는 것을 막고 있으므로 Automatic Anaylsis를 꺼주어야 합니다.)
또한 Jacoco를 통한 커버리지 측정 과정을 수행하지 못합니다! 이 과정 또한 추가해 봅시다!
위해서 생성한 SonarCloud의 Project로 이동하면 다음과 같이 CI-based analysis를 적용할지 물어보는데, 클릭해 줍시다!
이후 Method에서 Github Actions을 클릭해 줍시다!
위 사진에서 Github를 누르면 다음과 같이 name과 key가 발급되는데 하라는 데로 지정해 주면 됩니다!
이후 위와 같이 Name, Value 형태로 `SONAR_TOKEN`을 제공합니다.
이때 해당 토큰의 경우 SonarCloud로 전달하는 키가 되기 때문에 Guthub Actions에서 트리거를 확인하고 리포트를 전달하기 위해서는 해당 토큰이 필요해요.
따라서 제공되어 있는 Repository의 `Settings > Secrets` 경로에 가서 해당 토큰을 추가하는 것으로 튜토리얼을 시작합니다.
이후에는 각자가 사용하는 build 환경에 맞게 스크립트를 제공하므로 적절한 환경에서 사용하면 될 것 같아요. 저 같은 경우 Gradle을 선택하여 줬습니다!
이때 주의해야 할 점은 SonarCloud로 리포트를 만들어내기 위해서 sonar에 대한 속성값을 지정해주어야 합니다.
Gradle의 경우 build.gradle에 명시해 주면 됩니다.
plugins {
id "org.sonarqube" version "3.5.0.2730"
}
sonarqube {
properties {
property "sonar.projectKey", "사용할 project key"
property "sonar.organization", "project id"
property "sonar.host.url", "host url"
}
}
자세한 건 공식 docs 링크를 참고하면 다양한 옵션을 추가할 수도 있습니다!
이렇게 추가한 후 PR을 날리면 다음과 같이 PR에 정적분석 decoration이 추가되게 됩니다.
하지만, 아직 테스트코드의 커버리지가 나오지 않는 점이 아쉽습니다. 이 부분 또한 해결해 봅시다!
2 - 3) Jacoco를 통한 테스트 커버리지 분석
다음과 같이 build.gradle 파일에서 Jacoco를 플러그인으로 가져오고, 버전을 설정해 줍니다.
plugins {
// ...
id 'jacoco'
}
추가로 build.gradle에 다음과 같이 설정을 더해줍니다.
간략하게 설명하면 QueryDsl이 생성하는 Q파일들은 분석에서 제외시키고, 그 이외의 DTO나 key 파일 같은 것 또한 제외시킨 다음
분석하여 결과 report를 만들도록 설정해 주었습니다.
test 태스크가 끝난 다음에 jacocoTestReport를 실행하는 2번의 과정은 조금 번거롭습니다.
아래와 같이 finalizedBy를 설정하면, 테스트가 끝나면 곧바로 jacocoTestReport가 실행되게 만들 수 있습니다.
아래와 같이 test task에 추가합니다.
// -- Jacoco 설정 -------------------------------------------------------
test {
jacoco {
destinationFile = file("$buildDir/jacoco/jacoco.exec")
}
useJUnitPlatform()
finalizedBy 'jacocoTestReport'
}
2-3-1) 순서 결정하기
커버리지를 측정하기 위해서는 당연히 테스트가 진행된 이후에 Task가 동작해야 합니다.
하지만 플러그인은 test Task와의 의존성이 설정되어 있지 않기 때문에 직접 연결해줘야 합니다.
test {
useJUnitPlatform()
finalizedBy 'jacocoTestReport'
}
jacocoTestReport {
...
finalizedBy 'jacocoTestCoverageVerification'
}
jacocoTestCoverageVerification {
...
}
이는 finalizedBy를 통해서 연결할 수 있습니다.
결과적으로 test -> jacocoTestReport -> jacocoTestCoverageVerification 순서로 Task를 실행하게 됩니다.
jacoco {
// jacoco version
toolVersion = '0.8.8'
}
jacocoTestReport {
dependsOn test
reports {
html.enabled true // 개발자용 html
xml.enabled true // 소나큐브용
csv.enabled true
html.destination file("src/jacoco/jacoco.html")
}
// QueryDSL은 분석에서 제외시켜야 함
def Qdomains = []
for (qPattern in '**/QA'..'**/QZ') {
Qdomains.add(qPattern + '*')
}
afterEvaluate {
classDirectories.setFrom(
files(classDirectories.files.collect {
fileTree(dir: it, excludes: [
'**/*Request.*',
'**/*Response.*',
'**/dto/**',
'**/*Interceptor.*',
'**/*Exception.*',
'**/*Storage.*',
'**/KuringApplication.*',
'**/*Token.*'
] + Qdomains)
})
)
}
finalizedBy 'jacocoTestCoverageVerification'
}
jacocoTestCoverageVerification {
def Qdomains = []
for (qPattern in '*.QA'..'*.QZ') {
Qdomains.add(qPattern + '*')
}
violationRules {
rule {
limit {
element = 'CLASS'
minimum = 0.0 // 첫 빌드가 성동되는지 보기위해서 일당 0%로 지정
}
excludes = [
'**.*Request.*',
'**.*Response.*',
'**/dto/**',
'**.*Interceptor.*',
'**.*Exception.*',
'**.*Storage.*',
'**.KuringApplication.*',
'**.*Token.*'
] + Qdomains
}
}
}
task testCoverage(type: Test) {
group 'verification'
description 'Runs the unit tests with coverage'
dependsOn(':test',
':jacocoTestReport',
':jacocoTestCoverageVerification')
tasks['jacocoTestReport'].mustRunAfter(tasks['test'])
tasks['jacocoTestCoverageVerification'].mustRunAfter(tasks['jacocoTestReport'])
}
자세한 설정에 대한 글은 참고에 추가해 둔 글을 읽어주시면 감사하겠습니다!
여기까지 진행해서 pr을 날리면 pr에 테스트 커버리지도 성공적으로 추가되는 것을 확인할 수 있게 되었습니다!
3. 참고