SpringBoot3 SonarQube ์ ์šฉ (w. Jenkins)

์ตœ์ค€ํ˜ธยท2023๋…„ 6์›” 3์ผ
0

Appling

๋ชฉ๋ก ๋ณด๊ธฐ
3/12
post-thumbnail

์ฐธ๊ณ 
์šฐ๋ฆฌ ํŒ€์˜ ์ฝ”๋“œ ํ’ˆ์งˆ์€? ์ •์ ์ฝ”๋“œ ๋ถ„์„๋„๊ตฌ, ์†Œ๋‚˜ํ๋ธŒ ์ ์šฉ๊ธฐ
์ฝ”๋“œ ๋ถ„์„ ๋„๊ตฌ ์ ์šฉ๊ธฐ - 3ํŽธ, SonarQube ์ ์šฉํ•˜๊ธฐ

๐Ÿ”ด SonarQube๋ž€

์ •์  ์ฝ”๋“œ๋ถ„์„๊ธฐ๋กœ ์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•˜์—ฌ ์ฝ”๋“œ ํ’ˆ์งˆ ๊ด€๋ฆฌ์™€ ํ–ฅ์ƒ์— ๋„์›€์ด ๋˜๋Š” ๋„๊ตฌ์ด๋‹ค. ์ด์ „์—๋„ ๋งŽ์ด ๋“ค์—ˆ์ง€๋งŒ ๊ท€์ฐฎ๋‹ค๋Š” ์ด์œ ๋กœ ํ”„๋กœ์ ํŠธ์— ์ž˜ ์ ์šฉํ•˜์ง€ ์•Š๊ณ  ์žˆ์—ˆ๋Š”๋ฐ.

์ด๋ฒˆ ํšŒ์‚ฌ์—์„œ ๊ตฌ๊ธ€ ๊ฐ•์‚ฌ๋ถ„์„ ์ดˆ๋น™ํ•˜์—ฌ ๊ฐ•์˜๋ฅผ ์ง„ํ–‰ํ•˜์˜€์—ˆ๋Š”๋ฐ. ํ•ด๋‹น ๊ฐ•์‚ฌ๋ถ„์ด ์ž์‹ ์˜ ์ฝ”๋“œ๊ฐ€ ๊ต๊ณผ์„œ ๊ฐ™๋‹ค๋Š” ๋ง์„ ๋งŽ์ด ๋“ค์œผ์‹ ๋‹ค๊ณ  ํ•˜์…จ๋‹ค. 
๊ทธ๋Ÿฐ๋ฐ ์ด๋Ÿฌํ•œ ํŠน์ง•์—๋Š” ์ฝ”๋“œ ์ •์  ๋ถ„์„๊ธฐ๊ฐ€ ๋งŽ์€ ๋„์›€์„ ์คฌ๋‹ค๋Š” ๋ง์„ ๋“ฃ๊ณ  ๋ฐ”๋กœ ์ ์šฉํ•ด๋ณด๊ธฐ๋กœ ๋งˆ์Œ์„ ๋จน์—ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ํŒ€๋‚ด์—์„œ๋„ ์†Œ๋‚˜ํ๋ธŒ๋ฅผ ์ ์šฉ์ค‘์ด๊ธฐ๋„ ํ•˜๋‹ค. ๋‚ด๊ฐ€ ๋ณผ์ˆ˜ ์žˆ๋Š”๊ฑด ์—†์ง€๋งŒ... ๊ทธ๋ž˜๋„ ์•Œ๊ณ  ์žˆ์œผ๋ฉด ๋„์›€์ด ๋ ๊ฑฐ๋‹ˆ๊นŒ!

๐Ÿ”ต SonarQube ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ

์†Œ๋‚˜ ํ๋ธŒ๋Š” ์  ํ‚จ์Šค์™€ ๋น„์Šทํ•˜๊ฒŒ ํ•˜๋‚˜์˜ ์›น์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด์„œ ์„ธํŒ…ํ•ด์ฃผ๋Š” ๋ฐฉ์‹์ด๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ํ•˜๋‚˜์˜ ์„œ๋ฒ„๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ๋‚˜๋Š” ๊ฐœ์ธ Raspberry pi4๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์„œ ํ•ด๋‹น ์„œ๋ฒ„์— ์„ค์น˜๋ฅผ ์ง„ํ–‰ํ–ˆ๋‹ค.

๊ฐœ์ธ PC์— ์„ค์น˜ํ•ด์„œ ์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ• ๋•Œ๋งŒ ์‹คํ–‰์‹œ์ผœ์„œ ์‚ฌ์šฉํ•ด๋„ ๊ดœ์ฐฎ์„๊ฑฐ ๊ฐ™๋‹ค!

๐ŸŸข Docker ์ž‘์„ฑ

version: "3"
services:
  sonarqube:
    image: sonarqube:community
    hostname: sonarqube
    container_name: sonarqube
    depends_on:
      - qube-db
    environment:
      SONAR_JDBC_URL: jdbc:postgresql://qube-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"
  qube-db:
    image: postgres:13
    hostname: postgresql
    container_name: postgresql
    environment:
      POSTGRES_USER: sonar
      POSTGRES_PASSWORD: sonar
      POSTGRES_DB: sonar
    volumes:
      - postgresql:/var/lib/postgresql
      - postgresql_data:/var/lib/postgresql/data

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

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ›„์— docker compose ์‹คํ–‰ ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ์‹คํ–‰๋งŒ ํ•˜๋ฉด ๋ฐ”๋กœ ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰๋œ๋‹ค.

ํ•˜์ง€๋งŒ ๊ฐœ์ธ์„œ๋ฒ„์˜ ๊ฒฝ์šฐ ์ตœ์†Œ ์‚ฌ์–‘์„ ๋งž์ถ”์ง€ ๋ชปํ•ด ์‹คํ–‰์ด ์•ˆ๋˜๋Š” ๊ฒฝ์šฐ๋„ ์กด์žฌํ•˜๋Š”๋ฐ ๊ทธ๋Ÿด๋• ์„œ๋ฒ„์˜ ์„ธํŒ…์„ ํ™•์ธ ํ›„ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ์ตœ์†Œ ์‚ฌ์–‘์— ๋งž๋Š” ์„œ๋ฒ„๋กœ ๊ต์ฒดํ•ด์•ผํ•œ๋‹ค ใ…œใ…œ

๐ŸŸข ์‚ฌ์–‘ ์ฒดํฌ

์ตœ์†Œ ์‚ฌ์–‘

๋ฆฌ๋ˆ…์Šค - vm.max_map_count 524288
๋ฆฌ๋ˆ…์Šค - fs.file-max 131072
ํŒŒ์ผ ๋””์Šคํฌ๋ฆฝํ„ฐ ๊ฐœ์ˆ˜ - 131072
์Šค๋ ˆ๋“œ ๊ฐœ์ˆ˜ - 8192

sysctl vm.max_map_count
sysctl fs.file-max
ulimit -n
ulimit -u

ํ™•์ธ ํ›„ ์‚ฌ์–‘์— ๋ฏธ๋‹ฌํ•œ๋‹ค๋ฉด ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ์‚ฌ์–‘์„ ์ˆ˜์ •ํ•ด์ฃผ์ž.

sysctl -w vm.max_map_count=524288
sysctl -w fs.file-max=131072
ulimit -n 131072
ulimit -u 8192

๐ŸŸข ์„œ๋ฒ„ ์‹คํ–‰

docker compose up -d --build

๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ™”๋ฉด์ด ์‹คํ–‰๋œ๋‹ค.

๐Ÿ”ต ํ”„๋กœ์ ํŠธ ์„ธํŒ…

๐ŸŸข gradle ์„ธํŒ…

plugins {
    ...
    id 'jacoco'
    id 'org.sonarqube' version '3.3'
}


dependencies {
    ...
    implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3'
}

tasks.named('test') {
    outputs.dir snippetsDir
    useJUnitPlatform()
    finalizedBy 'jacocoTestReport'
}

tasks.named('asciidoctor') {
    inputs.dir snippetsDir
    dependsOn test
}

// jacoco
jacoco {
    toolVersion = '0.8.8'
}

jacocoTestReport {
    reports {
        xml.enabled true    // sonarqube ๋ถ„์„ ํŒŒ์ผ
        csv.enabled false
        html.enabled false

        xml.destination file("${buildDir}/jacoco/index.xml")	// ํŒŒ์ผ ๊ฒฝ๋กœ ๋ณ€๊ฒฝ
        csv.destination file("${buildDir}/jacoco/index.csv")
        html.destination file("${buildDir}/jacoco/index.html")
    }

    afterEvaluate {
        classDirectories.setFrom(
                files(classDirectories.files.collect {
                    fileTree(dir: it, excludes: [
                            '**/*Application*',
                            '**/*Exception*',
                            '**/*Advice*',
                            '**/dto/**',
                            '**/vo/**',
                            '**/enums/**',
                            '**/api/**',
                            '**/config/**',
                            // ...
                    ])
                })
        )
    }

    finalizedBy 'jacocoTestCoverageVerification'
}

jacocoTestCoverageVerification {
    violationRules {
        rule {
            enabled = true
            element = 'CLASS'

            excludes = [
                    '*.*Application',
                    '*.*Exception',
                    '*.*Advice',
                    '*.dto.*',
                    '*.vo.*',
                    '*.enums.*',
                    '*.api.*',
                    '*.common.*',
                    '*.security.*',
                    '*.entity.*',
                    '*.entity.*.Q*',
            ]

            limit {
                counter = 'METHOD'
                value = 'COVEREDRATIO'
                minimum = 0.50
            }
        }

        rule {
            enabled = true
            element = 'CLASS'

            excludes = [
                    '*.*Application',
                    '*.*Exception',
                    '*.*Advice',
                    '*.dto.*',
                    '*.vo.*',
                    '*.enums.*',
                    '*.api.*',
                    '*.common.*',
                    '*.security.*',
                    '*.entity.*',
                    '*.entity.*.Q*',
            ]

            limit {
                counter = 'LINE'
                value = 'COVEREDRATIO'
                minimum = 0.50
            }
        }
    }
}

sonarqube {
    properties {
        property "sonar.host.url", "{sonarqube server url}"
        property "sonar.sources", "src"
        property "sonar.language", "java"
        property "sonar.projectKey", "appling-api"
        property "sonar.projectName", "appling-api-prod"
        property "sonar.sourceEncoding", "UTF-8"
        property "sonar.java.binaries", "${buildDir}/classes"
        property "sonar.test.inclusions", "**/*Test.java, **/*Docs.java"
        property "sonar.exclusions", "**/resources/static/**, **/Q*.class, **/test/**"
        property "sonar.coverage.jacoco.xmlReportPaths", "${buildDir}/jacoco/index.xml"
    }
}

์ •์ ๋ถ„์„์€ SonarQube๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ๋™์ ๋ถ„์„์€ jacoco๋ฅผ ์‚ฌ์šฉํ•˜์˜€๋‹ค. ํ•ด๋‹น ์„ค์ •์œผ๋กœ ์†Œ์Šค์˜ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ”ต Jenkins ์„ค์ •

๋‹ค๋ฅธ ์  ํ‚จ์Šค์˜ ์ž๋™ ์„ค์ •์ด๋‚˜ ๊นƒํ—™ ์•ก์…˜์„ ํ†ตํ•œ ์ž๋™์„ค์ •๋„ ๋งŽ์€๋ฐ ๋‚˜๋Š” ๊ทธ๋ƒฅ ๋ช…๋ น์–ด๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ์‹คํ–‰ํ•˜๊ณ ์ž ํ•œ๋‹ค. ์•„์ง ํ”„๋กœ์ ํŠธ๊ฐ€ ์—„์ฒญ ๋ณต์žกํ•œ ๊ฒƒ๋„ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์—!

๐ŸŸข ํŒŒ์ดํ”„๋ผ์ธ ์ž‘์„ฑ

pipeline {
    agent any

    stages {
        stage('pull') {
            steps {
                git branch: 'main', credentialsId: 'git', url: 'https://github.com/appling-c/appling-api.git'
            }
        }
        stage('build') {
            steps {
                sh './gradlew clean build'
            }
        }
        
        stage('sonar') {
            steps {
                sh './gradlew sonarqube -Dsonar.login={sonarqube loing user token}'
            }
        }
        
        
        stage('clean') {
            steps {
                sshPublisher(
                    publishers: [
                        sshPublisherDesc(
                            configName: 'normal', 
                            transfers: [
                                sshTransfer(
                                    cleanRemote: false, 
                                    excludes: '', 
                                    execCommand: 'sudo rm -r /home/ubuntu/appling/api/build', 
                                    execTimeout: 120000, 
                                    flatten: false, 
                                    makeEmptyDirs: false, 
                                    noDefaultExcludes: false, 
                                    patternSeparator: '[, ]+', 
                                    remoteDirectory: '', 
                                    remoteDirectorySDF: false, 
                                    removePrefix: '', 
                                    sourceFiles: ''
                                    )
                                ], 
                                usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
            }
        }
        
        stage('transfer') {
            steps {
                sshPublisher(
                    publishers: [
                        sshPublisherDesc(
                            configName: 'normal', 
                            transfers: [
                                sshTransfer(
                                    cleanRemote: false, 
                                    excludes: '', 
                                    execCommand: '', 
                                    execTimeout: 120000, 
                                    flatten: false, 
                                    makeEmptyDirs: false, 
                                    noDefaultExcludes: false, 
                                    patternSeparator: '[, ]+', 
                                    remoteDirectory: '/home/ubuntu/appling/api', 
                                    remoteDirectorySDF: false, 
                                    removePrefix: '', 
                                    sourceFiles: 'build/libs/appling-1.0.jar'
                                    )
                                ], 
                                usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
            }
        }
        
        stage('up') {
            steps {
                sshPublisher(
                    publishers: [
                        sshPublisherDesc(
                            configName: 'normal', 
                            transfers: [
                                sshTransfer(
                                    cleanRemote: false, 
                                    excludes: '', 
                                    execCommand: 'cd /home/ubuntu/appling/api && sudo docker compose down ; sudo docker compose up -d --build', 
                                    execTimeout: 120000, 
                                    flatten: false, 
                                    makeEmptyDirs: false, 
                                    noDefaultExcludes: false, 
                                    patternSeparator: '[, ]+', 
                                    remoteDirectory: '', 
                                    remoteDirectorySDF: false, 
                                    removePrefix: '', 
                                    sourceFiles: ''
                                    )
                                ], 
                                usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
            }
        }
    }
}

clean ๋ถ€ํ„ฐ๋Š” ๊ฐœ์ธ ์„ค์ •์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ณด์ง€ ์•Š์•„๋„ ๋˜๊ณ  sonar๊นŒ์ง€ ์ฐธ๊ณ ํ•ด์„œ ์ž‘์„ฑํ•˜๋ฉด ์ข‹์„๊ฑฐ ๊ฐ™๋‹ค.
๋”ฐ๋กœ ํฐ ์„ค์ •์—†์ด gradle ์„ค์ •์„ ํ†ตํ•ด์„œ ์†Œ๋‚˜ํ๋ธŒ์— ๋ฐ”๋กœ ์ ์šฉ์ด ๊ฐ€๋Šฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์—„์ฒญ ๊ฐ„๋‹จํ•˜๋‹ค.

๊ทธ๋ฆฌ๊ณ  sonarqube.loing= ๋“ค์–ด๊ฐ€๋Š” ํ† ํฐ๊ฐ’์€ ์†Œ๋‚˜ํ๋ธŒ์— ๋กœ๊ทธ์ธ ํ›„ user token์„ ๋ฐœ๊ธ‰ ๋ฐ›์•„์„œ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

๊ทธ ํ›„ ํ”„๋กœ์ ํŠธ์— ๋“ค์–ด๊ฐ€๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •๋Œ€๋กœ ๋‚ด ์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•ด์„œ ์•Œ๋ ค์ค€๋‹ค...
๋ˆˆ๋ฌผใ…œ

์ด์ œ ์†Œ์Šค ๋ถ„์„์— ๋งž์ถฐ์„œ ์†Œ์Šค๋ฅผ ์ˆ˜์ •ํ•ด๊ฐ€๋ฉฐ ์ฆ๊ฒ๊ฒŒ ์ฝ”๋”ฉ์„ ํ•ด๋ณด์ž!

profile
์ฝ”๋”ฉ์„ ๊น”๋”ํ•˜๊ฒŒ ํ•˜๊ณ  ์‹ถ์–ดํ•˜๋Š” ์ดˆ๋ณด ๊ฐœ๋ฐœ์ž (ํŽธํ•˜๊ฒŒ ๊ธ€์„ ์“ฐ๊ธฐ์œ„ํ•ด ๋ฐ˜๋ง์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค! ์–‘ํ•ด ๋ถ€ํƒ๋“œ๋ ค์š”!) ํ˜„์žฌ KakaoVX ๊ทผ๋ฌด์ค‘์ž…๋‹ˆ๋‹ค!

0๊ฐœ์˜ ๋Œ“๊ธ€