SonarQube + GitLab + Java(Spriong) + Gradle에 대한 내용입니다.
SonarQube Project와 GitLab Project를 연결하는 방법에 대해서 알아보자.
Administration > Configuration > DevOps Platform Integrations > GitLab > 'Create configuration' 클릭
http://[gitlab URL]:[port]/api/v4
정상적으로 설정이 완료되면 아래와 같은 화면이 출력된다.
메인 화면에서 Create Project를 누르면 다음과 같이 GitLab으로부터 프로젝트를 가져올 수 있다.
연결한 GitLab에 존재하는 모든 프로젝트가 리스트로 출력되며 체크를 통해 Import를 할 수 있다.
Import를 완료한 프로젝트들은 메인 화면에 다음과 같이 출력된다.
SonarQube와 연동한 GitLab 프로젝트는 1:1로 매핑된 SonarQube의 프로젝트 정보에 대한 환경 변수를 설정한다.
GitLab > 해당 프로젝트 > Settings > CI/CD > Variables
GitLab 프로젝트를 선택하고 Import를 클릭한다.
새로운 코드 인식에 대한 설정을 수행한다. 이 부분에 대해 잘 모른다면 여기를 확인하자.
With GitLab CI
를 선택한다.
SONAR_TOKEN
: 해당 SonarQube 프로젝트의 토큰SONAR_HOST_URL
: SonarQube Server의 URL 정보주의 할 점은 모든 환경 변수는 위와 같이 Protect Variable
에 대한 체크는 해제하고, Mask Variable
체크를 반드시 해주어야한다.
또한, SONAR_TOKEN
의 경우는 이전 버전에서 SONAR_LOGIN
으로 사용되었기 때문에 해당 부분은 오류가 발생한다면 버전에 맞는 Key 값을 사용하도록 하자.
위 방법 외에 접속 정보를 설정 할 수 있는 방법은 다음과 같다.
.gitlab-ci.yml
에 정보 입력이런 방법들은 작동에는 큰 무리가 없지만 보안상의 취약점이 발생할 수 있기 때문에 GitLab 자체에서 프로젝트 단위로 환경 변수를 보유하는 것이 효율적이면서도 보안 측면에서도 뛰어나다고 할 수 있다.
GitLab Runner는 설치가 되어있다는 가정하에 등록 및 설정 방법에 대해서 알아보자.
Runner는 다음과 같이 분류된다.
러너 | 특징 | 관리 |
---|---|---|
공유 러너 | 여러 그룹, 프로젝트에서 공용 사용 GitLab 인스턴스 전체에 설치 | GitLab 관리자 |
그룹 러너 | 특정 그룹에 할당되어 사용되는 러너 | 그룹 관리자 |
프로젝트 러너 | 단일 프로젝트 내에서만 사용가능한 러너 프로젝트별로 독립되어 운용 | 프로젝트 관리자 |
Runner가 Job을 실행하는 방식(환경)은 다음과 같다.
내가 설정한 방식은 공유 러너를 Shell을 이용하여 구성하였다.
GitLab > Admin Area > CI/CD > Runners > New instance runner
Optional 항목을 제외하고, 위 두가지에 대해서만 작성하면 된다. Tags는 Runner를 묶을 수 있는 항목이기 때문에 아무렇게나 작성하지 말고, 의미를 가지고 차후에 그룹지어 호출하는 경우를 상정하자.
Runner가 생성되면 위와 같이 목록으로 표시되는데 최초에는 Never contacted
라는 문구로 지정되어있다.
Runner는 GitLab이 구성된 환경에서 직접 등록을 해주어야한다.
Runner를 생성하면 위와 같이 생성된 Runner의 정보가 나올텐데 해당 정보를 아래를 참고하여 등록하면된다.
sudo gitlab-runner register
--url http://[ip]:[port]
--token [token value]
등록이 완료되면 다음 명렁어를 통해 Runner의 목록을 볼 수 있다.
sudo gitlab-runner list
여기까지 진행하면 정상적으로 등록이 되어 위에 사진처럼 초록색으로 Online
표시가 나올 것이다.
만약 그렇지 않다면 다음 명렁어로 Runner를 재실행하자.
sudo gitlab-runner restart
sudo gitlab-runner status
모든 절차대로 Runner가 등록되었다면 어느 프로젝트를 들어가도 Shared runners에 해당 Runner가 표시될 것이다.
여기서 중요한 부분이 하단에 푸른색으로 보이는 sonarqube
이다. 저 부분에 출력되는 것이 위에서 설명한 Runner의 Tags 항목이기 때문에 차후 CI 등록을 수행할 때 해당 Tags를 정확히 매핑해주어야 Runner가 동작을 수행한다.
프로젝트끼리의 연동과 Runner 설정을 통해 GitLab과의 Integration 기반은 마련되었으니 파이프라인 구성을 해보자.
SonarQube 프로젝트를 생성하면 UI에서 다음과 같은 화면으로 프로젝트 설정에 대한 안내를 제공한다.
하지만 내용이 간략하고, 제대로 코드 분석을 수행하기에 추가해야하는 설정들이 많이 있다. 아래 build.gradle
파일을 통해서 각 설정 내용들을 알아보자.
plugins {
id "org.sonarqube" version "6.0.1.5171"
}
sonar {
properties {
property "sonar.projectKey", "[해당 프로젝트 키]"
property "sonar.host.url", System.getProperty("sonar.host.url") ?: System.getenv("SONAR_HOST_URL")
property "sonar.token", System.getProperty("sonar.token") ?: System.getenv("SONAR_TOKEN")
property "sonar.projectName", "[프로젝트 명]"
property "sonar.language", "java"
property "sonar.sourceEncoding", "UTF-8"
property "sonar.sources", "src"
property "sonar.java.binaries", "${buildDir}/classes"
property "sonar.qualitygate.wait", true
property "sonar.exclusions", "**/resources/static/**, **/test/**"
}
}
각 설정 정보에 대한 설명은 다음과 같다.
구분 | 설명 | 비고 |
---|---|---|
sonar.projectKey | Project Information에 명시된 Project Key | 필수 |
sonar.projectName | 프로젝트 생성 시 작성한 이름 | |
sonar.token | 프로젝트 생성 시 발급한 토큰으로 환경 변수의 SONAR_TOKEN 에 해당 | 필수 |
sonar.host.url | SonarQube Server의 URL로 SONAR_HOST_URL 에 해당 | 필수 |
sonar.language | 분석할 대상 프로그래밍 언어 | 필수 |
sonar.sourceEncoding | 소스 대상 파일의 인코딩 정보(UTF-8로 설정) | |
sonar.sources | 코드 분석을 수행할 경로를 지정 | 필수 |
sonar.java.binaries | Java에 대한 분석을 수행할 때 필수적으로 설정해야하는 값으로 .class 파일을 제공해야 제대로된 분석을 수행 | 필수 |
sonar.qualitygate.wait | SonarQube의 Qualirty Gate가 통과하지 않을 경우 Job 실패로 지정할지 여부(true 권장) | |
sonar.exclusions | 분석에서 제외할 패키지를 설정 | 필수 |
추가적으로 설명이 필요한 부분은 ${buildDir}/classes
이다.
기본적으로 SonarQube에서는 Java에 대한 분석을 수행할 때 .java
에서는 주석 등의 일부 정보만 참고하고, .class
파일에 있는 코드를 통해 분석을 수행한다.
이런 이유로 build/classed
경로를 sonar.java.binaries
경로로 설정해주어야 정확한 분석이 보장된다.
sonar.host.url
과 sonar.token
의 경우 gradle을 통해 빌드를 수행할 때 동작을 보장하기 위해서 로컬 프로젝트 루트 경로에도 설정이 존재해야한다.
다음과 같이 gradle.properties
파일을 프로젝트 루트 경로에 생성하고, 값을 설정하자.
systemProp.sonar.host.url=http://[ip]:[port]
systemProp.sonar.token=[token value]
여기서 systemProp
키워드는 gradle에서 정보를 읽을 때 System.getProperty()
값으로 읽을 수 있도록 해주는 키워드이다.
test
패키지에 대한 부분은 아래에서 설명하겠다.
gradle 설정을 통해 SonarQube에 대한 정보를 세부적으로 설정하였으니 파이프라인이 수행할 Job에 대한 구성을 수행하자.
# Docker 이미지 지정
image: gradle:8.2.0-jdk17-jammy
# 파이프라인 실행 시 사용할 환경 변수 정의
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
# Git fetch 깊이 설정 - SonarQube는 전체 이력이 필요하므로
0GIT_DEPTH: "0"
# 파이프 라인에서 실행될 작업 단계 정의
stages:
- sonar-analysis
# 작업 정의
sonar-analysis:
stage: sonar-analysis
tags:
- sonarqube
cache:
policy: pull-push
# 현재 브랜치 이름을 캐시에 포함하여 브랜치별로 캐시 분리
key: "sonar-cache-$CI_COMMIT_REF_SLUG"
paths:
- "${SONAR_USER_HOME}/cache"
# 파이프 라인에서 실행할 명령 정의
script:
- chmod +x ./gradlew
- ./gradlew sonar
allow_failure: true
rules:
# master는 merge 시 코드 분석
- if: $CI_COMMIT_BRANCH == 'master' && $CI_PIPELINE_SOURCE == 'merge_request_event'
# master를 제외한 브랜치는 push 시 코드 분석
- if: $CI_COMMIT_BRANCH != 'master' && $CI_PIPELINE_SOURCE == 'push'
여기서 주의 해야할 부분은 다음과 같다.
stages
에 작성한 내용과 작업 정의 시 stage
가 올바르게 매핑Tags
와 파일의 tags
를 동일하게 매핑master
브랜치는 Merge Request
가 발생하면 파이프 라인이 동작하고, 그 외 브랜치는 push
가 발생하면 파이프 라인이 동작하도록 설정을 하였다.
push
를 통해 동작을 수행시켜보자.
git add .
git commit -m "sonarqube 코드 분석 파이프 라인 구성"
git push origin [브랜치명]
Merge Request의 Pipelines를 클릭하면 아래와 같이 상태가 출력된다. #53
등의 Job 번호를 클릭하면 해당 Job이 어떻게 수행되었는지 세부적으로 확인이 가능하다.
SonarQube의 프로젝트를 클릭하면 아래와 같이 파이프 라인 작업이 완료되어 코드 분석이 끝난 화면을 볼 수 있다.
지금까지 설명한 내용을 잘 따라왔지만 한가지 다른 부분이 있을 것이다. 바로 Coverage
에 대한 결과가 나오지 않을텐데 이 부분은 언어별로 별도로 설정이 필요하다.
Java의 커버리지를 분석하기 위해서는 jacoco
라는 플러그인이 사용되며, 설정에 아래 내용들을 추가해주면 된다.
plugins {
id "jacoco"
id "org.sonarqube" version "6.0.1.5171"
}
jacoco {
toolVersion = "0.8.10"
}
tasks.named('test') {
useJUnitPlatform ()
// 테스트 후 리포트 생성
finalizedBy jacocoTestReport
}
jacocoTestReport {
dependsOn test
reports {
// SonarQube XML 리포트 활성화
xml.required.set(true)
}
}
sonar {
properties {
//...생략
// Test Coverage
property "sonar.exclusions", "**/resources/static/**, **/test/**"
property "sonar.test.inclusions", "**/*Test.java"
property "sonar.java.coveragePlugin", "jacoco"
property "sonar.coverage.jacoco.xmlReportPaths", "${buildDir}/reports/jacoco/test/jacocoTestReport.xml"
}
}
sonar.test.inclusions
: 테스트 코드 분석의 참고를 위한 .java
파일을 포함sonar.java.coveragePlugin
: jacoco
가 커버리지 플러그인이라고 명시sonar.coverage.jacoco.xmlReportPaths
: jacoco
가 테스트를 수행하고, 결과 보고서를 저장하는 경로jacoco
는 별도의 gradle 태스크를 수행하여 테스트가 완료된 결과를 ${buildDir}/reports/jacoco/test/jacocoTestReport.xml
에 저장한다. SonarQube의 태스크는 위 작업이 끝난 다음에 수행되면서 해당 리포트 정보를 Coverage
참조로 사용하여 결과를 만들어내는 것이다.아래 항목들은 SonarQube 및 테스트 커버리지 분석에 필요한 내용들이기 때문에 동일하게 적용해주면 된다.
!build/reports/jacoco/test/jacocoTestReport.xml
!build/classes/
gradle.properties
마지막으로 테스트를 파이프 라인에서 수행하기 위해 아래와 같이 추가 및 수정을 해주면 된다.
#...생략
cache:
policy: pull-push
# 현재 브랜치 이름을 캐시에 포함하여 브랜치별로 캐시 분리
key: "sonar-cache-$CI_COMMIT_REF_SLUG"
paths:
- "${SONAR_USER_HOME}/cache"
# 이 부분 추가
- build/reports/jacoco/test/jacocoTestReport.xml # 리포트 캐시
script:
- chmod +x ./gradlew
# 이 부분 수정
- ./gradlew clean test jacocoTestReport sonar
GitLab + SonarQube를 통합하면서 정말 많은 시행 착오를 겪었다. 느낀점은 다음과 같다.
SonarQube를 통해 Java 코드를 분석하는 것은 설정에 따라 어느 정도는 차이가 발생할 것 같다.
왜냐하면 어떤 개발자는 Dto
, ErrorCode
, Exception
에 대한 코드 검사가 불필요 할 수 있고, 또 다른 개발자는 해당 검사까지 필요할 수도 있는 노릇이다.
Junit을 통해 테스트를 수행할 때 로컬에서 수행하다보니 실제 DB 정보 등에 대해서 고민을 해본 적이 없었고, 설정 파일은 코드 버전 관리에서 제외하면 되겠다는 생각만 하고 있었는데 다음과 같은 문제들이 있었다.
application.yaml
파일이 GitLab에 같이 push되지 않다보니 jacoco
가 수행하는 테스트에서 예외 발생이런 부분들은 고려하다보니 @Tag
어노테이션을 통해 local
과 remote
로 테스트를 분리하고, 파이프 라인에서 수행되는 테스트는 remote
에 대한 테스트만 수행하도록 설정하였다.
테스트와 관련된 부분은 코드 검사가 아니더라도 지속적으로 고민을 많이 해야할 문제인 것 같다.