본문 바로가기

프로젝트/고민

코드의 품질 관리를 도움 받기: SonarCloud, CodeMetrics

SonarCloud

코드를 작성하다 보면 통용되는 컨벤션, 중복되는 코드, 보안 취약점 등을 인지하지 못한채 개발하게 된다. 심지어 Java를 C언어 처럼 사용한다거나 Go 언어를 Javascript처럼 사용하는 경우가 있을 것이다.(본인이 익숙한 언어에 맞게 다른 언어를 사용함) 이렇게 되면 새로운 언어를 배운다 하더라도 자신에게 익숙한 언어의 언어스타일로 개발을 하게 되는데 새로운 언어를 사용하는 의미 또한 많이 떨어질 것이다.
이러한 문제를 해결하기 위해 SonarCloud라는 코드 분석 오픈소스가 탄생하게 됐다.

품질관리 요소

이제 어떻게 품질을 관리하는지 알아보자

소나큐브가 코드를 분석하는 지표는 여기에 있다. Java를 기준으로 사용해본 결과 이펙티브 자바에서 강조하는 내용과 일치하는 부분이 많다.

Reliability(신뢰성)

문제: Null pointer exception이 발생할 수 있는 코드

public void unreliableMethod(String input) {
    System.out.println(input.length());
}

개선된 코드:

public void reliableMethod(String input) {
    if (input != null) {
        System.out.println(input.length());
    }
}

Maintainability(유지보수성)

문제: 너무 긴 메서드

public void longMethod() {
    // 여러 줄의 코드...
}

개선된 코드:

public void maintainableMethod() {
    subMethod1();
    subMethod2();
}

private void subMethod1() {
    // ...
}

private void subMethod2() {
    // ...
}

Security(보안 취약점)

문제: sql인젝션이 가능한 코드

String query = "SELECT * FROM users WHERE name = " + inputName;

개선된 코드:

PreparedStatement ps = connection.prepareStatement("SELECT * FROM users WHERE name = ?");
ps.setString(1, inputName);

Security Review(코드의 보안)

Security는 sql 인젝션, xss 등으로 부터의 보안을 의미하고 Security Review는 암호화 되지 않은 비밀번호, 민감한 정보의 로깅 등에 해당
문제: 암호화되지 않은 패스워드 저장

String password = "plainText";

개선된 코드:

String hashedPassword = hashFunction("plainText");

Coverage

테스트 커버리지 (jacoco report가 필요, 예시 생략)

Duplications

코드 중복도(예시 생략)

사용 방법 익히기(Github)

SonarCloud는 Github, Gitlab, BitBucket 등등 다양한 소소코드 관리 도구와 통합이 된다. 이 중 가장 대중적인 Github와 함께 사용하는 방법을 익혀보자

회원가입

먼저 링크에 들어가 회원가입을 하자

Organization 셋업

이 중 소나클라우드를 적용하고 싶은 조직을 선택

 

해당 조직 중 원하는 레포 선택(또는 전체 선택)

적절한 sonarCloud 조직 생성

유료 요금제와의 차이는 private 레포까지 접근할 수 있느냐 없느냐의 차이 정도로 보임

 

해당 조직의 프로젝트 선택

선택 화면 

레포지토리 선택 후 Clean as You Code 선택

Clean as You Code: 소나 클라우드에서 인식하는 새로운 코드(날짜를 기준으로 새로운 코드를 인식하게 할지, 버전을 기준으로 할지 설정 가능)

 

이런 기능이 있는 이유는 프로젝트 도중 소나클라우드를 사용했을 경우 이전의 코드에도 적용되기 때문에 새로운 코드에 집중하지 못할 수 있기 때문이다.(자세한 내용은 공식문서)

 

이후 소나클라우드는 코드를 분석하는데 아래의 화면이 나올 것이다.

 

이제 pr을 올릴때마다 아래의 화면이 나올 것이다.

 

Jacoco 연동

이 과정은 좀 복잡한데 이를 이용해 CI 파이프라인에 sonar를 추가해서 pr 메시지에 작성하게할 수 있다. 나의 경우 빌드 도구를 gradle로 사용했으므로 gradle을 기준으로 설명하겠다. (maven, ant 등 다양한 빌드 툴도 지원하고 방법도 문서에도 나와있음)

추가적으로 나는 java 버전은 17을 사용했다.

 

jacoco를 이용하면 테스트 커버리지를 확인할 수 있고 테스트 커버리지가 특정 값에 미치지 못할 경우 빌드를 실패하게 할 수 있다. 얼핏보면 테스트를 강제하고 좋은 품질의 코드를 작성하게 해줄 수 있지만, 테스트 커버리지를 높이기 위한 테스트를 작성할 가능성이 매우 높아진다. 이러한 강제가 테스트의 의도를 완전 망쳐버릴 수 있는 것이다. 이러한 강제는 또한 생산성도 낮추게 되는데 이러한 이유로 나는 테스트 커버리지를 sonar에게 전달하는 용도로만 사용했다. 어차피 테스트 커버리지 결과는 html로 생성이 되고 소나에서도 볼 수 있으니깐 테스트를 강제할만한 마땅한 이유는 찾지 못했다.

build.gradle에 jacoco 파이프라인 추가

여기서 jacoco 테스트 결과를 다른 파일에 생성하게 설정할 수 있는데 기본은 루트프로젝트/build/jacoco/test 디렉토리 내에 생성된다. html에 관한 설정을 별도로 하지 않아도 기본적으로 생성해주니 아래와 같이 설정해주자. (문서)

plugins {
    id "java"
    id "jacoco"
    id "org.sonarqube" version "4.3.1.3277"
}

jacocoTestReport {
    reports {
        xml.required = true
    }
}

build.gradle에 sonar 파이프라인 추가

여기서 Automatic Analysis를 끈다. 코드 분석을 깃허브 액션으로 할거니깐. 깃허브 액션에서 돌아가는 소나 분석과 자동 으로 수행되는 소나 분석이 충돌이 나기 때문이다.

설정을 끈 후 위 주황색으로 체크된 actions에 들어간 후 아래서 시키는데로 github 레포지토리에 소나 토큰을 추가한다.

아래 써져있는데로 build.gradle에 sonar 파이프라인을 추가한다.

깃허브 워크 플로우에 추가할 yml 파일(이거는 sonar 분석만 하는 워크 플로우고 이거 참고해서 커스텀 해서 사용 가능함)

name: SonarCloud
on:
  push:
    branches:
      - master
  pull_request:
    types: [opened, synchronize, reopened]
jobs:
  build:
    name: Build and analyze
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # Shallow clones should be disabled for a better relevancy of analysis
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: 17
          distribution: 'zulu' # Alternative distribution options are available
      - 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: Build and analyze
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}  # Needed to get PR information, if any
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        run: ./gradlew build sonar --info

 

나의 경우 최종적으로 아래와 같이 build.gradle을 작성했다. 프레임워크 없이 개발했던거라 다른 태스크도 있는데 jacoco, sonar, test 쪽만 보면 된다.

plugins {
    id "java"
    id "jacoco"
    id "org.sonarqube" version "4.3.1.3277"
}

jacocoTestReport {
    reports {
        xml.required = true
    }
}


jar {
    //실행할 메인 클래스 명시
    manifest {
        attributes 'Main-Class': 'org.prgrms.Main'
    }
    //라이브러리 전부 가져오기
    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

group = 'org.prgrms'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    // SLF4J API
    implementation 'org.slf4j:slf4j-api:1.7.32'
    // Logback: SLF4J의 구현체
    implementation 'ch.qos.logback:logback-classic:1.2.6'
    implementation 'mysql:mysql-connector-java:8.0.28'
    implementation 'com.zaxxer:HikariCP:5.0.0'
    testImplementation platform('org.junit:junit-bom:5.9.1')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

test {
    useJUnitPlatform()
    finalizedBy jacocoTestReport
}

tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
}

sonar {
    properties {
        property "sonar.projectKey", "joyfulviper_citron"
        property "sonar.organization", "joyfulviper"
        property "sonar.host.url", "https://sonarcloud.io"
        property "sonar.coverage.jacoco.xmlReportPaths", "$buildDir/reports/jacoco/test/jacocoTestReport.xml"
    }
}

깃허브 액션으로 소나 클라우드로 정적 코드 분석

위 경로에 깃허브 액션 워크플로우를 생성한 후 아래와 같이 작성해준다.

name: SonarCloud
on:
  push:
    branches:
      # 브랜치는 matser로 했는데 '[dev, master, release]'나 패턴 '**/*'으로 이벤트를 받을 브랜치를 설정 가능
      - master
    # - [dev, master]
  pull_request:
    types: [opened, synchronize, reopened]
jobs:
  build-gradle-and-report:
    runs-on: ubuntu-latest
    steps:
      - name: github 체크아웃
        uses: actions/checkout@v3

      - name: jdk 세팅
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
      - name: gradlew 실행 권한 부여
        run: chmod +x ./gradlew

      # https://github.com/gradle/gradle-build-action
      - name: gradle 테스트
        uses: gradle/gradle-build-action@v2
        with:
          arguments: clean test

      # https://github.com/actions/cache
      - name: SonarCloud 결과물 캐싱하기
        uses: actions/cache@v3
        if: always()
        with:
          path: ~/.sonar/cache
          key: ${{ runner.os }}-sonar
          restore-keys: ${{ runner.os }}-sonar

      - name: gradle SonarCloud 분석 + PR 코멘트 자동 작성
        uses: gradle/gradle-build-action@v2
        if: always()
        with:
          arguments: sonar
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

      # https://github.com/EnricoMi/publish-unit-test-result-action
      - name: 테스트 결과를 'PR 코멘트'로 자동 작성
        uses: EnricoMi/publish-unit-test-result-action@v2
        if: always()  # 실패 여부 상관없이 항상 실행~
        with:
          files: build/test-results/test/TEST-*.xml

      # https://github.com/mikepenz/action-junit-report
      - name: 테스트 코드 중 테스트 실패한 부분에 'PR check'로 자동 작성
        uses: mikepenz/action-junit-report@v3
        if: failure()
        with:
          report_paths: build/test-results/test/TEST-*.xml

이제 깃허브 액션에서 pr 메시지를 작성할 수 있도록 리포지토리의

Settings -> Actions(좌측에 있음) -> General -> Workflow Permission(스크롤을 제일 아래로 내리면 됨) -> Read and Write Permissions 체크

 

이제 pr을 올릴 때 마다 아래와 같은 결과를 확인할 수 있다.

인텔리제이에 CodeMetrics, SonarLint 플러그인 설치

앞서 jacoco와 sonar를 이용해 코드의 품질을 관리했다. 이제 코드의 품질을 직접 모니터링 할 수 있다.

또한, 동료간에 코드리뷰를 할 때 코드의 품질보다는 코드의 역할에 집중해서 리뷰를 주고받을 수 있을 것이다.(이전에 비해) 코드 전체의 구조를 확인해주는 기능은 부족하기 때문에 옳은 아키텍처인지는 소나로 판단하기 어려울듯

 

하지만 소나의 코드 분석을 pr을 올린 시점에 하는 것은 생산성이 조금 떨어질 것이다. 이런 상황에서 ide에 플러그인을 설치해서 소나의 코드 분석을 지원받을 수 있다.

SonarLint 설치

intellij의 플러그인 저장소에서 SonarLint를 설치하면 Sonar의 코드 분석을 도움받을 수 있따.

CodeMetics 설치

CondeMetrics는 이와 별개인데 개인적으로 꽤나 사용해볼만하다고 생각이 들어 소나플러그인 하는김에 소개를 한다.

마찬가지로 인텔리제이의 플러그인 스토어에서 설치하면 아래와 같은 화면을 볼 수 있다.

 

  • Complexity is 6 its time to do something 이라는 메시지는 CodeMetrics에서 문석한 코드인데 메소드의 분기문, return 기타 등등을 분석해서 점수를 부과한다. 점수는 5/10/15를 기준으로 메시지가 출력되는데 10점 아래면 나쁘지 않은듯?(이거 점수 낮추려고 구현을 못하는 아쉬운 행동은 하지 말자..!)
  • 이런식으로 RuntimeException을 던지면 노란색 밑줄과 함께 더 구체화된 타입의 예외를 던지라는 뉘앙스의 메시지를 확인할 수 있다.

 

이렇게 여러 플러그인, 오픈소스의 지원을 받아 좀 더 나은 품질의 코드를 작성할 수 있지만 어디까지나 도움을 주는 도구일 뿐 완벽하게 리뷰를 해주지는 않는다. 이를 인지하고 코드를 코딩하자~~