Spring

[Spring Boot 3] 소나큐브(SonarQube), JaCoCo 연동하여 정적 분석

테런 2024. 7. 5. 13:46
Overview

 

Prerequisite
* Spring Boot 3.2.7
* Java 17
* Gradle 8.1
* Sonarqube 5.0.0.4638
* Jacoco 0.8.11
* Docker

 

테스트 애플리케이션
// 테스트할 간단한 Spring Boot 3버전 코드
$ git clone https://github.com/hyunkwanko/spring-boot-3.x-demo.git

 

소나큐브(SonarQube) 설치
$ sudo su -
$ mkdir sonarqube && cd sonarqube

// https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html
$ sysctl -w vm.max_map_count=262144

$ vi sonarqube-compose.yml
version: "3"

services:
  sonarqube:
    image: sonarqube:community # 9.8 이후 버전은 Java 17만 지원
    depends_on:
      - db
    environment:
      SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar
      SONAR_JDBC_USERNAME: sonar
      SONAR_JDBC_PASSWORD: sonar
    volumes:
      - sonarqube_data:/opt/sonarqube/data
      - sonarqube_extensions:/opt/sonarqube/extensions
      - sonarqube_logs:/opt/sonarqube/logs
    ports:
      - "9000:9000"
    ulimits:
      nproc: 65535
      nofile:
        soft: 262144
        hard: 262144
  db:
    image: postgres:12
    environment:
      POSTGRES_USER: sonar
      POSTGRES_PASSWORD: sonar
    volumes:
      - postgresql:/var/lib/postgresql
      - postgresql_data:/var/lib/postgresql/data

volumes:
  sonarqube_data:
  sonarqube_extensions:
  sonarqube_logs:
  postgresql:
  postgresql_data:

$ docker-compose -f sonarqube-compose.yml up -d

 

소나큐브(SonarQube) 프로젝트 생성

초기 계정: admin / admin

 

Create a local project 클릭

 

Project 정보 입력

 

Global Setting (변경 사항에만 분석)

 

Locally 클릭

 

인증 토큰 생성

 

소나큐브 프로젝트 생성 완료

 

Spring Boot에 소나큐브(SonarQube), JaCoCo 연동 (build.gradle)
plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.7'
	id 'io.spring.dependency-management' version '1.1.5'
	id "org.sonarqube" version "5.0.0.4638"
	id "jacoco"
}

sonar {
	properties {
		property "sonar.projectName", "demo"
		property "sonar.projectKey", "demo"
		property "sonar.language", "java"
		property "sonar.sourceEncoding", "UTF-8"
		property "sonar.sources", "src/main"
		property "sonar.tests", "src/test"
		property "sonar.java.binaries", "${buildDir}/classes"
		property "sonar.test.inclusions", "**/*Test.java"
		property 'sonar.coverage.jacoco.xmlReportPaths', "${buildDir}/reports/jacoco/test/jacocoTestReport.xml"
	}
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	// Spring boot
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-validation'

	// Test
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

	// Lombok
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	// H2
	runtimeOnly 'com.h2database:h2'
}

tasks.named('test') {
	useJUnitPlatform()
	finalizedBy 'jacocoTestReport' // test 태스크가 끝난 후 실행
}

// jacoco 정보
jacoco {
	toolVersion = "0.8.11"
	layout.buildDirectory.dir("reports/jacoco")
}

// jacoco Report 생성
jacocoTestReport {
	dependsOn test // test 종속성 추가

	reports {
		xml.required = true
		csv.required = false
		html.required = true
	}

	def QDomainList = []
	for (qPattern in '**/QA'..'**/QZ') { // QClass 대응
		QDomainList.add(qPattern + '*')
	}

	afterEvaluate {
		classDirectories.setFrom(files(classDirectories.files.collect {
			fileTree(dir: it, exclude: [
					'**/dto/**',
					'**/event/**',
					'**/*InitData*',
					'**/*Application*',
					'**/exception/**',
					'**/service/alarm/**',
					'**/aop/**',
					'**/config/**',
					'**/MemberRole*'
			] + QDomainList)
		}))
	}

	finalizedBy 'jacocoTestCoverageVerification' // jacocoTestReport 태스크가 끝난 후 실행
}

// jacoco Test 유효성 확인
jacocoTestCoverageVerification {
	def QDomainList = []
	for (qPattern in '*.QA'..'*.QZ') { // QClass 대응
		QDomainList.add(qPattern + '*')
	}

	violationRules {
		rule {
			enabled = true // 규칙 활성화 여부
			element = 'CLASS' // 커버리지를 체크할 단위 설정

			// 코드 커버리지를 측정할 때 사용되는 지표
			limit {
				counter = 'LINE'
				value = 'COVEREDRATIO'
				minimum = 0.30
			}

			limit {
				counter = 'BRANCH'
				value = 'COVEREDRATIO'
				minimum = 0.30
			}

			excludes = [
				'**.dto.**',
				'**.event.**',
				'**.*InitData*',
				'**.*Application*',
				'**.exception.**',
				'**.service.alarm.**',
				'**.aop.**',
				'**.config.**',
				'**.MemberRole*'
			] + QDomainList
		}
	}
}

 

정적 분석 테스트
// gradlew 권한 설정
$ sudo chmod +x gradlew

// gradle build
$ ./gradlew build

// 소나큐브(SonarQube) 프로젝트에서 명령어 확인
$ ./gradlew sonar \
  -Dsonar.projectKey={projectKey} \
  -Dsonar.projectName={projectName} \
  -Dsonar.host.url={SonarQube URL} \
  -Dsonar.token={Token}

성공적으로 Build가 되면 소나큐브(SonarQube)에서 위 그림과 같은 분석 데이터를 확인할 수 있습니다.
JaCoCo도 무사히 연동됐다면 그림처럼 Coverage에 데이터가 확인됩니다.