[DevOps]SonarQube와 GitLab 통합으로 코드 품질 검사 자동화 구축하기

Inung_92·2025년 1월 11일
1

DevOps

목록 보기
4/5
post-thumbnail

SonarQube + GitLab + Java(Spriong) + Gradle에 대한 내용입니다.

GitLab 프로젝트 연동

SonarQube Project와 GitLab Project를 연결하는 방법에 대해서 알아보자.

아키텍처

프로젝트 연결

Configuration

Administration > Configuration > DevOps Platform Integrations > GitLab > 'Create configuration' 클릭

  • Configuration name : 사용자가 지정한 설정 이름
  • GitLab API URL : http://[gitlab URL]:[port]/api/v4
  • Personal Access Token : GitLab에서 개인이 발급받은 Token을 사용

정상적으로 설정이 완료되면 아래와 같은 화면이 출력된다.

Project 생성

메인 화면에서 Create Project를 누르면 다음과 같이 GitLab으로부터 프로젝트를 가져올 수 있다.

연결한 GitLab에 존재하는 모든 프로젝트가 리스트로 출력되며 체크를 통해 Import를 할 수 있다.

Import를 완료한 프로젝트들은 메인 화면에 다음과 같이 출력된다.

환경 변수 설정

SonarQube와 연동한 GitLab 프로젝트는 1:1로 매핑된 SonarQube의 프로젝트 정보에 대한 환경 변수를 설정한다.

GitLab Variables

GitLab > 해당 프로젝트 > Settings > CI/CD > Variables

SonarQube 프로젝트 정보

GitLab 프로젝트를 선택하고 Import를 클릭한다.

새로운 코드 인식에 대한 설정을 수행한다. 이 부분에 대해 잘 모른다면 여기를 확인하자.

With GitLab CI를 선택한다.

  • SONAR_TOKEN : 해당 SonarQube 프로젝트의 토큰
  • SONAR_HOST_URL : SonarQube Server의 URL 정보
  • GitLab의 Variables에 추가

주의 할 점은 모든 환경 변수는 위와 같이 Protect Variable에 대한 체크는 해제하고, Mask Variable 체크를 반드시 해주어야한다.

또한, SONAR_TOKEN의 경우는 이전 버전에서 SONAR_LOGIN으로 사용되었기 때문에 해당 부분은 오류가 발생한다면 버전에 맞는 Key 값을 사용하도록 하자.

Vairables 등록 완료

위 방법 외에 접속 정보를 설정 할 수 있는 방법은 다음과 같다.

  • .gitlab-ci.yml에 정보 입력
  • 각 프로젝트 설정 파일에 접속 정보를 설정하고, GitLab에 push
  • 서버 자체 환경 변수로 등록

이런 방법들은 작동에는 큰 무리가 없지만 보안상의 취약점이 발생할 수 있기 때문에 GitLab 자체에서 프로젝트 단위로 환경 변수를 보유하는 것이 효율적이면서도 보안 측면에서도 뛰어나다고 할 수 있다.

GitLab Runner 설정

GitLab Runner는 설치가 되어있다는 가정하에 등록 및 설정 방법에 대해서 알아보자.

특징

Runner는 다음과 같이 분류된다.

러너특징관리
공유 러너여러 그룹, 프로젝트에서 공용 사용
GitLab 인스턴스 전체에 설치
GitLab 관리자
그룹 러너특정 그룹에 할당되어 사용되는 러너그룹 관리자
프로젝트 러너단일 프로젝트 내에서만 사용가능한 러너
프로젝트별로 독립되어 운용
프로젝트 관리자

Runner가 Job을 실행하는 방식(환경)은 다음과 같다.

  • Shell : 호스트 머신의 로컬 셸 환경에서 Job 실행
  • Docker : Docker 컨테이너에서 Job을 실행
  • Docker + Machine : 클라우드에 동적으로 Docker 실행 환경 생성으로 Job 실행
  • SSH : 원격 서버에서 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가 동작을 수행한다.


CI

프로젝트끼리의 연동과 Runner 설정을 통해 GitLab과의 Integration 기반은 마련되었으니 파이프라인 구성을 해보자.

SonarQube 설정

SonarQube 프로젝트를 생성하면 UI에서 다음과 같은 화면으로 프로젝트 설정에 대한 안내를 제공한다.

하지만 내용이 간략하고, 제대로 코드 분석을 수행하기에 추가해야하는 설정들이 많이 있다. 아래 build.gradle 파일을 통해서 각 설정 내용들을 알아보자.

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.projectKeyProject Information에 명시된 Project Key필수
sonar.projectName프로젝트 생성 시 작성한 이름
sonar.token프로젝트 생성 시 발급한 토큰으로 환경 변수의 SONAR_TOKEN에 해당필수
sonar.host.urlSonarQube Server의 URL로 SONAR_HOST_URL에 해당필수
sonar.language분석할 대상 프로그래밍 언어필수
sonar.sourceEncoding소스 대상 파일의 인코딩 정보(UTF-8로 설정)
sonar.sources코드 분석을 수행할 경로를 지정필수
sonar.java.binariesJava에 대한 분석을 수행할 때 필수적으로 설정해야하는 값으로 .class파일을 제공해야 제대로된 분석을 수행필수
sonar.qualitygate.waitSonarQube의 Qualirty Gate가 통과하지 않을 경우 Job 실패로 지정할지 여부(true 권장)
sonar.exclusions분석에서 제외할 패키지를 설정필수

추가적으로 설명이 필요한 부분은 ${buildDir}/classes이다.
기본적으로 SonarQube에서는 Java에 대한 분석을 수행할 때 .java에서는 주석 등의 일부 정보만 참고하고, .class파일에 있는 코드를 통해 분석을 수행한다.

이런 이유로 build/classed 경로를 sonar.java.binaries경로로 설정해주어야 정확한 분석이 보장된다.

gradle.properties

sonar.host.urlsonar.token의 경우 gradle을 통해 빌드를 수행할 때 동작을 보장하기 위해서 로컬 프로젝트 루트 경로에도 설정이 존재해야한다.

다음과 같이 gradle.properties 파일을 프로젝트 루트 경로에 생성하고, 값을 설정하자.

systemProp.sonar.host.url=http://[ip]:[port]
systemProp.sonar.token=[token value]

여기서 systemProp 키워드는 gradle에서 정보를 읽을 때 System.getProperty()값으로 읽을 수 있도록 해주는 키워드이다.

test 패키지에 대한 부분은 아래에서 설명하겠다.

.gitlab-ci.yml

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가 올바르게 매핑
  • Runner에 입력한 Tags와 파일의 tags를 동일하게 매핑

master 브랜치는 Merge Request가 발생하면 파이프 라인이 동작하고, 그 외 브랜치는 push가 발생하면 파이프 라인이 동작하도록 설정을 하였다.

GitLab Push

push를 통해 동작을 수행시켜보자.

git add .
git commit -m "sonarqube 코드 분석 파이프 라인 구성"
git push origin [브랜치명]

Merge Request의 Pipelines를 클릭하면 아래와 같이 상태가 출력된다. #53등의 Job 번호를 클릭하면 해당 Job이 어떻게 수행되었는지 세부적으로 확인이 가능하다.

SonarQube의 프로젝트를 클릭하면 아래와 같이 파이프 라인 작업이 완료되어 코드 분석이 끝난 화면을 볼 수 있다.

Coverage

지금까지 설명한 내용을 잘 따라왔지만 한가지 다른 부분이 있을 것이다. 바로 Coverage에 대한 결과가 나오지 않을텐데 이 부분은 언어별로 별도로 설정이 필요하다.

Java의 커버리지를 분석하기 위해서는 jacoco라는 플러그인이 사용되며, 설정에 아래 내용들을 추가해주면 된다.

build.gradle

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 참조로 사용하여 결과를 만들어내는 것이다.

.gitignore

아래 항목들은 SonarQube 및 테스트 커버리지 분석에 필요한 내용들이기 때문에 동일하게 적용해주면 된다.

!build/reports/jacoco/test/jacocoTestReport.xml  
!build/classes/  
gradle.properties

.gitlab-ci.yml

마지막으로 테스트를 파이프 라인에서 수행하기 위해 아래와 같이 추가 및 수정을 해주면 된다.

#...생략
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를 통합하면서 정말 많은 시행 착오를 겪었다. 느낀점은 다음과 같다.

  • Chat GPT가 DevOps 구성을 위해 주는 정보의 대부분은 신뢰도가 떨어진다.
  • SonarQube 공식 문서가 불친절하다.
  • UI에서 제공하는 힌트들이 너무 빈약하고, 추가적인 정보가 없다.
  • Gradle에 대한 지식이 부족했다.
  • 파이프 라인 구성에 대한 이해가 어느 정도 되었다.

설정 차이

SonarQube를 통해 Java 코드를 분석하는 것은 설정에 따라 어느 정도는 차이가 발생할 것 같다.

왜냐하면 어떤 개발자는 Dto, ErrorCode, Exception에 대한 코드 검사가 불필요 할 수 있고, 또 다른 개발자는 해당 검사까지 필요할 수도 있는 노릇이다.

테스트 관련

Junit을 통해 테스트를 수행할 때 로컬에서 수행하다보니 실제 DB 정보 등에 대해서 고민을 해본 적이 없었고, 설정 파일은 코드 버전 관리에서 제외하면 되겠다는 생각만 하고 있었는데 다음과 같은 문제들이 있었다.

  • application.yaml파일이 GitLab에 같이 push되지 않다보니 jacoco가 수행하는 테스트에서 예외 발생
  • 실제 DB 정보를 파이프 라인 동작을 위해 포함시키거나 환경 변수로 설정하자니 보안성 우려
  • 테스트 환경을 구축하기에는 제조 단계에서 발생하는 데이터들을 복사하여 옮기기에는 제한
  • 파이프 라인에서 실제 DB까지 테스트하면 코드 분석에 장시간을 소요

이런 부분들은 고려하다보니 @Tag 어노테이션을 통해 localremote로 테스트를 분리하고, 파이프 라인에서 수행되는 테스트는 remote에 대한 테스트만 수행하도록 설정하였다.

테스트와 관련된 부분은 코드 검사가 아니더라도 지속적으로 고민을 많이 해야할 문제인 것 같다.

profile
서핑하는 개발자🏄🏽

0개의 댓글