Spring

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

테런 2024. 7. 5. 14:56
Overview

 

Prerequisite
* Spring Boot 2.7.16
* Java 11
* Gradle 7.1.1
* Sonarqube 3.5.0.2730
* Jacoco 0.8.11
* Docker

 

테스트 애플리케이션
// 테스트할 간단한 Spring Boot 2버전 코드
$ git clone https://github.com/hyunkwanko/spring-boot-2.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:9.8.0-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

 

Manually 클릭

 

Project 정보 입력

 

Locally 클릭

 

인증 토큰 생성
소나큐브 프로젝트 생성 완료

 

Spring Boot에 소나큐브(SonarQube), JaCoCo 연동 (build.gradle)
plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.16'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    id "org.sonarqube" version "3.5.0.2730"
    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 = 'sample'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '11'
}

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'

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

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

//tasks.named('test') {
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.host.url={SonarQube URL} \
  -Dsonar.login={Token}

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