본문 바로가기

프로젝트/트러블슈팅

[트러블 슈팅] 멀티모듈에서 JaCoCo와 SonarCloud를 적용하여 코드 품질 높이기

도입

어느 날 팀원이 SonarCloud를 적용한 글을 공유했다. 글을 읽어보니, 빠르고 간단하게 코드 품질을 높일 수 있을 것 같아 원데이히어로 프로젝트에 도입하기로 했다. 원데이히어로 프로젝트는 멀티모듈이다. 이번 글에서는 멀티모듈에서 jacoco와 SonarCloud를 어떻게 적용했는지 왜 도입했는지 이야기 해보겠다.

 

SonarCloud란?

 

SonarCloud은 정적 코드 분석 도구 중 하나인 SonarQube의 SaaS 버전이다. 여기서 정적 코드 분석이란, 코드 레벨에서 발견할 수 있는 코드 스멜, 잠재적 결함, 컨벤션 체크, 보안 취약점 등을 분석해서 보고해준다.

 

SonarCloud는 SonarQube와 달리 서버에 설치하지 않아도 되지만, SonarQube만큼 많은 기능을 제공하지 않는다. 하지만 원데이히어로처럼 작은 프로젝트에서는 기본적인 기능만으로 충분했기에 SonarCloud를 적용하기로 했다.

 

더 나아가서 우리 팀은 기능을 개발하면 단위 테스트와 통합 테스트가 필수인만큼 테스트 커버리지까지 분석해주길 원했다. 하지만 SonarCloud의 자동분석은 테스트 커버리지는 지원을 해주지 않기 때문에 CI 기반의 분석을 사용해야 한다. 참고로 이미 github action을 사용하고 있었기 때문에 어떤 CI 툴을 쓸지 고민하지는 않았다.

 

적용

 

이제 CI 기반 분석을 사용하기로 결정했으니 내 프로젝트에서 왼쪽 아래의 Administration > Analysis Method로 들어가 자동 분석을 꺼주기로 하자. 그리고 아래 With Github Actions에 들어가면 토큰을 받는다.

 

그렇게 SonarCloud로 부터 받은 토큰을 repo내 Secrets로 등록한다. 이름은 제안해준대로 SONAR_TOKEN으로 해주자.

 

다음으로 프로젝트에 JaCoCo를 적용해보겠다. JaCoCo란 Java 코드의 커버리지를 측정해주는 라이브러리이다. 멀티 모듈이므로 서브 모듈마다 테스트 코드를 실행하고 리포트를 html이나 xml, csv 파일로 만들어야 한다.

 

root에 있는 build.gradle 파일에서 다음과 같이 스크립트를 작성했다. 간단히 살펴보자면, jacoco 플러그인을 적용한 뒤, 서브 프로젝트 내 테스트 코드를 실행하면 xml 형태의 테스트 결과를 생성한다. 

 

subprojects {
    group = 'com.sixheroes'
    version = '0.0.1-SNAPSHOT'

    java {
        sourceCompatibility = '17'
    }

    apply plugin: 'java'
    apply plugin: 'java-library'
    apply plugin: 'io.spring.dependency-management'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'jacoco' // 플러그인 

    test {
        finalizedBy jacocoTestReport // test 후 jacoco 실행
    }

    jacocoTestReport { 
        reports {
            xml.required = true // xml 형태의 리포트 생성
        }
    }
 }

 

test 테스크를 실행시키면 서브 모듈 내 build/reports/jacoco/test/html/index.html을 열어보면 해당 모듈의 커버리지를 확인할 수 있다. 나름 테스트 코드를 놓치지 않고 잘 작성했음에도 커버리지가 낮을 것이다. 커버리지 측정에 포함하지 않아도 될 클래스들이 포함되어 있기 때문이다. 이제 그러한 코드들을 제외시켜보자.

 

참고로 위와 반복되는 코드는 생략했다. 먼저 프로젝트에서 QueryDsl을 사용하고 있으므로 Q클래스를 제외했다. 그 외에도 요청과 응답 DTO, 설정 클래스, 유틸 클래스 등을 제외했다. 

 

 jacocoTestReport {
    afterEvaluate {
        classDirectories.setFrom(files(classDirectories.files.collect {
            fileTree(dir: it, excludes: [
                "**/Q*",
                "**/request/*", 
                "**/response/*",
                "**/dto/*",
                "**/config/*",
                "**/global/**",
                "**/*Converter*",
                "**/healthcheck/*"
            ])
        }))
    }
}

 

JaCoCo는 테스트 실행 후 리포트를 생성하는 jacocoTestReport 테스크 외에 커버리지가 원하는 만큼 도달했는지 확인해주jacocoTestCoverageVerification 테스크도 있다. 하지만 SonarCloud에서도 제공해주는 기능으로 테스크를 작성하지 않고 넘어가겠다.

 

다음으로 SonarCloud를 위한 빌드 스크립트를 작성했다. root에 있는 build.gradle의 subproject에서 sonarqube 플러그인을 추가했다. 그리고 각 모듈에 만들어진 커버리지 분석 리포터의 경로(default 경로를 넣어줌)를 설정했다.

subprojects {
    apply plugin: 'org.sonarqube' // 플러그인
    
    sonar {
        properties {
            property 'sonar.coverage.jacoco.xmlReportPaths', "build/reports/jacoco/jacocoTestReport.xml"
        }
    }
}

 

그리고 subprojects 바깥에 sonar 테스크를 하나 더 작성해주었다. 위에서 작성해줬던 것과 마찬가지로 커버리지에서 제외할 클래스들을 설정해줬다.

 

plugins {
    id "org.sonarqube" version "4.4.1.3373"
}

sonar {
    properties {
        property "sonar.projectKey", "prgrms-web-devcourse_Team-6Heroes-OneDayHero-BE"
        property "sonar.organization", "prgrms-web-devcourse"
        property "sonar.host.url", "https://sonarcloud.io"
        property 'sonar.exclusions', '''
            **/Q*, **/request/*, **/response/*, **/dto/*, **/config/*, **/global/**, **/*Converter*, **/healthcheck/*
        '''
    }
}

 

마지막으로 SonarCloud가 프로젝트를 분석하도록 github action 스크립트를 작성했다. 간단히 살펴보자면, 모든 브랜치에서 Pull Request가 발생할 때마다 아래 정의된 작업이 실행된다.

 

그리고 Test란 이름의 단계에서  ./gradlew test가 실행되는데, 앞에서 적었던 build 스크립트에 따라 테스트 후 jacocoTestReport 테스크가 실행된다. 다음 단계를 살펴보면, /gradlew sonar --info가 실행된다. 이때 앞서 만들어진 xml 형태의 분석 리포트를 가지고 프로젝트의 테스트 커버리지를 수치화 해준다. 참고로 이 단계는 앞의 단계에서 테스트가 실패해도 실행되도록 스크립트를 작성했다.

 

name: Report Sonar Cloud With Jacoco

on:
  pull_request:
    branches: [ "**" ]
    paths:
      - '**.java'
      - '**/build.gradle*'
      - '**/settings.gradle*'
      - '**/application*.yml'

jobs:
  build:

    runs-on: ubuntu-latest

    timeout-minutes: 10

    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'corretto'

      - name: Cache SonarCloud packages
        uses: actions/cache@v3
        with:
          path: ~/.sonar/cache
          key: ${{ runner.os }}-sonar
          restore-keys: ${{ runner.os }}-sonar

      - name: Cache Gradle packages
        uses: actions/cache@v3
        with:
          path: ~/.gradle/caches
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
          restore-keys: ${{ runner.os }}-gradle
      
      - name: Test
        run: ./gradlew test

      - name: Analyse
        if: always() # 테스트 실패해도 실행
        env: 
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}  # Needed to get PR information, if any
           SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        run: ./gradlew sonar --info

 

SonarCloud에서  Administration > Quality Gates로 이동하면 코드 품질을 유지하기 위해 규칙을 설정할 수 있다. default는 Sonar way로 되어 있다. 열어보면, 최소 커버리지 기준 뿐만 아니라 허용할 중복 라인 비율, 최소 보안 등급 등 다양한 규칙이 있다.

 

Sonar way는 우리 프로젝트에겐 기준이 높으므로 프로젝트에 맞게 규칙을 생성한 뒤 적용해주었다. 최소 커버리지는 60%로 허용할만한 수준으로 설정했고 나머지 규칙은 정해주지 않았다. 

 

PR을 올려보니 코멘트로 SonarCloud의 분석 리포트가 도착했다! 

 

적용 후기

 

원데이히어로의 테스트 커버리지는 73.2%로 나쁘지 않은 수치이지만, 그렇다고 이상적인 수치는 아니다. 왜 이상적인 수치가 나오지 못했을까?

 

단순히 생각해보자면, 첫 번째는 시간 부족이라고 생각한다. 개발할 수록 프로덕션 코드는 늘어나고 마감 기한은 얼마 남지 않았다. 기능 구현이 먼저였고 테스트 코드 작성은 후순위였다. 해피 케이스와 예외 케이스 둘 다 작성했던 처음과 달리 후반에는 해피 케이스의 테스트만 작성하게 됐다.

 

두 번째는 테스트하기 어려운 영역이 나타났다는 점이다. MongoDB, Redis 등을 도입하게 되었고 이러한 인프라 영역을 테스트하기 어려워졌다. 물론 Mock 테스트를 수행할 수 있었지만, 팀원과 이야기 나눈 결과 "개발자가 인풋과 아웃풋을 모두 제어할 수 있으면 왜 테스트하는거지? 인프라 레이어는 Mock 테스트 하지말자"로 결론났다. 그리고 차후 Testcontainer를 이용해 통합 테스트 환경을 구축하여 인프라를 테스트하기로 했다.

 

참고로 나는 80%를 이상적인 수치로 생각하는데, 오히려 테스트 커버리지가 너무 높으면 테스트의 질이 낮지 않을까 의심하게 된다😅 단순히 수치를 올리기 위해 작성된 질 낮은 테스트가 많아진다면 관리해야할 테스트 코드만 늘어날 뿐이다. 그리고 우아한 기술 블로그를 보니, 팀내에서 너무 불필요한 테스트는 하지 않으려고 하며 branch 커버리지를 끌어올리기 위해 노력한다고 한다.

 

마지막으로 SonarCloud를 도입함으로써 얻은 장점을 이야기해보겠다. 나는 IDE에 SonarLint 플러그인을 설치해서 개인적으로 코드의 품질을 관리하고 있었다. 하지만 SonarCloud를 도입함으로써 코드 품질 향상을 개인에서 팀으로 끌어올릴 수 있었다. 그리고 팀 내 코드 리뷰가 어느정도 활성화되어 있는데, 잠재적으로 문제가 될만한 코드, 안티패턴인 코드를 찾아주고 수정안을 제시해줘 코드리뷰의 수고를 덜어주었다.

 

참고