개발자의 오르막

코드스멜을 피하는 방법 Sonarqube 본문

Architecture

코드스멜을 피하는 방법 Sonarqube

계단 2023. 7. 26. 19:03

 

Overview


https://www.cambiaresearch.com/articles/995518/six-reasons-to-ditch-the-term-code-smell

코드스멜, 소프트웨어 개발 시에 사용되는 용어로, 잠재적인 문제를 야기할 수 있는 부분들에서 냄새가 난다는 관용적 표현으로 많이 쓰입니다. 코드의 가독성, 유지보수성, 확장성, 성능 등 부정적인 영향을 끼칠 수 있는 부분들을 기준에서 해당이 됩니다.

 

만일, 여러분이 코드리뷰를 받을 때, 개발하였던 주요 로직에 대한 피드백이 아닌 통칭 코드스멜이 나는 코드에 대한 리뷰만을 왕창 받으면 참 기분이 별로 좋지 않을 것입니다. 내가 이럴려고 개발을 했나.. 왜 이런 부분을 놓쳤을까 등 자괴감에 빠지며, 정작 중요한 기능에 대해서는 코멘트를 못받을 수 있는 일이 허다합니다. 왜냐하면 코드스멜만큼 눈에 띄고 부정적인 영향을 나타내는 것밖에 없으니까요! 그리고 이런 부분은 냄새 자체가 나기 때문에 동료들에게 좋은 평가를 받기가 어렵습니다.

 

우리는 그런 이유로, 이런 문제들은 사람이 아닌 컴퓨터가 알려줬으면 합니다. 우리가 PR 을 날리기전에 이런 코드스멜을 풍기는 부분들을 지적해준다면!!. 다른 리뷰어가 봤을 때는 보다 주요 로직에 대해서 피드백을 받을 수 있겠죠.

그렇기에 우리가 활용할 수 있는 오픈소스 SonarQube 소개드리겠습니다.

 

 

SonarQube 란?


https://techblog.tabling.co.kr/%EA%B8%B0%EC%88%A0%EA%B3%B5%EC%9C%A0-%EC%A0%95%EC%A0%81-%EC%BD%94%EB%93%9C-%EB%B6%84%EC%84%9D-sonarqube-6b59fa9b6b85

 

SonarQube 는 버그, 코드 악취와 보안 취약점을 감지하기 위한 정적 분석 도구입니다. 20개 이상의 프로그래밍 언어를 지원하며, Github Action 과 결합하여 자동 리뷰를 수행할 수 있는 오픈 소스입니다. 소나큐브는 중복코드, 코딩 표준, 유닛테스트, 코드 커버리지, 코드 복잡도, 주석, 버그, 보안 취약점의 보고서를 제공해줍니다.

 

이러한 소나큐브의 대표적인 기능은 아래와 같습니다.

  • Linux, Window, Mac 등 다양한 환경에서 모두 구동이 가능합니다.
  • 20개가 넘는 프로그램 언어에 대한 코드 분석을 지원합니다.
  • 개발된 코드의 품질을 어드민을 통해 확인이 가능하며 지속적 관리가 가능합니다.
  • 품질 게이트를 통해 표준화된 코드 품질 요구사항을 설정할 수 있습니다.

 

 

SonarQube 동작 원리

소나큐브는 크게 Scanner 와 SonarQube Server 로 구분해서 생각하면 쉽습니다.

homebrew , wget 등 sonarqube scanner 오픈소스를 다운로드 받습니다. 그리고 이 Scanner 를 통해서 우리의 프로젝트를 분석하고 결과파일을 생성합니다.

해당 결과파일을 HTTP 프로토콜을 통해 SonarQube Server 로 전송합니다. SonarQube Server 는 대시보드 형태로 분석 결과를 도표화하여 시각적으로 우리에게 보여줍니다.

 

자. 그렇다면

백문이 불여일견이라고, 저의 사이드프로젝트를 분석한 대시보드를 확인해볼까요?

(저는 소나큐브 버전 6.7 기준으로 테스트를 진행하였습니다.)

 

 

SonarQube Project Dashboard


위의 대시보드를 보시면 크게 4가지 섹터로 구분됨을 알 수 있습니다.

 

 

Bugs (버그)

버그 지표는 코드에서 발견된 실제 또는 잠재적인 버그의 수를 의미합니다.

이러한 버그는 코드에 존재하는 오류로 예상하지 못한 동작, 예외상황, 보안 취약점 등으로 이어질 수 있는 부분입니다.

 

 

 

위의 이미지처럼 특정 버그를 야기할 수 있는 지점을 찾아주고, 그 위험도를 나타내줍니다.

public Optional<File> convertMultipartFileToFile(MultipartFile file) throws Exception {
    File convertFile = new File(file.getOriginalFilename());
    if (convertFile.createNewFile()) {
        try {
            FileOutputStream fos = new FileOutputStream(convertFile);
            fos.write(file.getBytes());
        } catch (IOException ioe) {
            throw new Exception(ioe);
        }
        return Optional.of(convertFile);
    }
    return Optional.empty();
}

 

위가 해당하는 소스 부분인데, 제가 FileOutputStream 을 사용하면서 fos.close(); 를 누락한 것이 보이네요 그리고 file.getOriginalFilename() 에 대해서는 null 처리를 하지 않았군요..

메모리 누수에 대한 위험도 야기될 수 있는 뿐만 아니라 null 정보가 들어왔을 때의 RunTimeException 도 야기될 수 있겠습니다.

이처럼 현재 동작에는 이상이 없을 수 있지만, 잠재적인 요인으로 인해 위험할 수 있는 부분을 검출해주는 역할을 합니다.

 

 

Code Smells (코드스멜)

코드스멜 지표는 좋은 소프트웨어 개발 관행을 위반하거나 설계원칙을 어기는 코드들을 의미합니다.

코드스멜은 가독성 저하, 유지보수 어려움, 성능 저하 등을 유발할 수 있으므로 개선이 필요한 부분으로 간주됩니다.

 

  • Rename this package name to match the regular expression '^[a-z]+(\.[a-z][a-z0-9])$'.
    • Querydsl 을 사용하다보니, QClass file 이 생겼네요. 패키지 이름에 대해서도 네이밍을 변경하길 권장하고 있습니다.
  • Rename this method name to match the regular expression '^[a-z][a-zA-Z0-9]*$'
    • **public** **static** Attachment Of(UsageType usageType, FileType fileType, 이런 메서드와 같이 대문자 OF 같은 부분도 지적해줍니다.
  • Add a private constructor to hide the implicit public one.
    • 아래 코드 예시처럼 Dto 를 사용하기 위해 생성자를 명시하지 않은 경우도 CodeSmell 로 알려주고 있습니다. 하나의 클래스 안에는 private 생성자를 만들기를 권장하네요.
public class AttachmentCommand {

    @Getter
    @Builder
    public static class Upload {
        private MultipartFile multipartFile;
        private UsageType usageType;
        private String uuid;
        private FileType fileType;
    }
}

이처럼 동작하는데는 지장이 없을수도 있으나 개발 관행상 좋지 않은 부분, 지적하기 애매한 부분을 소나큐브가 알려줍니다. PR 을 올리기 전에 이러한 부분들을 미리 볼 수 있고, 이부분들을 왜 권장하는지 알아보면서 향기로운 코드를 짜볼 수 있는 경험을 할 수 있습니다.

 

 

 

 

Coverage (코드커버리지)

코드커버리지는 소프트웨어 코드에서 테스트가 얼마나 많은 부분을 실행했는지를 나타냅니다.

커버리지는 일반적으로 테스트 슈트를 실행하고, 코드가 실행되는 부분을 추적하는 도구를 사용하여 측정됩니다.

높은 코드 커버리지는 테스트를 통해 코드의 신뢰성과 안정성을 높일 수 있습니다.

 

 

Duplications (중복코드)

중복코드 지표는 코드에서 발견된 중복 코드의 비율을 나타냅니다.

중복코드는 동일한 논리를 반복해서 구현하는 코드로, 유지보수성을 저하시키고 버그 발생 가능성을 높일 수 있습니다.

 

 

 

위의 목록처럼 중복된 코드가 반복되는 부분들을 랭크화시켜줘서 우리에게 알려주고 있습니다.

public class ParentDto {
    @Getter
    @NoArgsConstructor
    public static class ChangeHighSchoolRequest {
        private String highSchool;
    }
    @Getter
    @NoArgsConstructor
    public static class ChangeYearRequest {
        private Integer year;
    }
    @Getter
    @Builder
    public static class Response {
        private final Boolean isOk;
    }
}

저의 프로젝트 같은 경우 Dto 를 도메인별로 만들어둔 경우가 있는데, 이러한 부분들이 중복된 코드로서 많이 발견이 되네요

그렇다면 이제는 실습의 시간입니다. 소나큐브를 설치 및 실행을 해볼까요?

 

 

 

소나큐브 Server 설치


본 글에서는 Sonarqube server 에 대한 설치를 중점으로 다루지 않고, 간략하게만 안내해드리겠습니다.

 

SonarQube Server 는 기본으로 요청하는 서버 기본사양이 있습니다.

  • 최소 2GB / 1 vcpu 용량의 서버
  • PostgreSQL 버전 9.3 이상
  • OpenJDK 11 또는 JRE 11
  • 모든 SonarQube프로세스는 sonar 사용자로 실행

Linux 설정 (최소 사양)

  • fm.max_map_count : 524288
  • fs.file-max : 131072
  • 파일 디스크립터 개수 : 131072
  • 스레드 개수 : 8192

 

서버의 최소 사양 확인 명령어

# vm max 값 확인
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

 

EC2 도커 설치

# EC2 console, docker 설치 및 권한설정
sudo apt-get update && \\
sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common && \\
curl -fsSL <https://download.docker.com/linux/ubuntu/gpg> | sudo apt-key add - && \\
sudo apt-key fingerprint 0EBFCD88 && \\
sudo add-apt-repository "deb [arch=amd64] <https://download.docker.com/linux/ubuntu> $(lsb_release -cs) stable" && \\
sudo apt-get update && \\
sudo apt-get install -y docker-ce && \\
sudo usermod -aG docker ubuntu && \\
sudo curl -L "<https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$>(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose && \\
sudo chmod +x /usr/local/bin/docker-compose && \\
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

 

도커 컴포즈문 생성

# docker-compose.yml
version: "3"

services:
  sonarqube:
    image: sonarqube:community
    depends_on:
      - db
    environment: # 소나큐브 DB 연결 설정
      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:
    	# 소나큐브의 defaulut 포트는 9000입니다. http 기본 포트로 접속하기위해 들어가면 화면을 보이게 하기 위해 호스트 포트는 80으로 설정
      - "80:9000" 
  db: #postgres 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:

 

 

 

Sonar Scanner 사용

소나큐브 설치는 공식문서(https://docs.sonarsource.com/sonarqube/latest/) 에서 버전별, 운영체제 별로 설치를 진행할 수 있습니다.

 

다른 오픈소스 라이브러리와 마찬가지로 소나큐브 스캐너도 아래와 같은 방법으로 설치할 수 있습니다.

 

 

Sonarqube 대시보드에서 sonar-token 발행

소나큐브 설치가 완료되면, 최초 로그인 계정으로 admin/admin 으로 계정이 제공됩니다.

다음으로 소나큐브에서 사용할 sonar-token 을 발급받아야 하는데요.

 

sonarqube 대시보드의 맨 오른쪽 상단에 계정을 클릭하고, MyAccoutn 라는 메뉴를 선택합니다.

위의 Security 탭에서 우리가 사용하고자 하는 Sonar-token 을 발행할 수 있습니다.

 

 

 

PHP, Golang 등 Language 로 Sonnar scanner 적용

위에서 Sonar Scanner 라이브러리를 설치완료하였다면 이제 우리가 분석하고자 하는 프로젝트를 스캔해볼 차례입니다.

sonar scanner 를 테스트하기 위해 간략한 golang 프로젝트를 만들어봤는데요.

sonar-scanner 는 프로젝트의 루트 위치에서 sonar-project.properties 의 정보를 기반으로 해당 프로젝트를 분석합니다.

 

sonar-project.properties

sonar.projectKey=sonar-golang
sonar.projectName=golang-002
sonar.sources=.
sonar.exclusions=**/*_test.go,**/vendor/**,**/testdata/*
sonar.tests=.
sonar.test.inclusions=**/*_test.go
sonar.test.exclusions=**/vendor/**
sonar.go.coverage.reportPaths=./coverage.out
  • sonar.projectKey : 프로젝트를 식별하기 위한 고유 키
  • sonar.projectName : 프로젝트 대시보드에서 명시되는 프로젝트 이름
  • sonar.sources : sonnar scanner 가 분석하고자 하는 소스 위치
  • sonar.exclusions : 분석에서 제외할 파일
  • sonar.tests : 테스트 소스 코드의 경로
  • sonar.test.inclusions : 분석에 포함할 테스트 파일 패턴
  • sonar.test.exclusions : 분석에서 제외할 테스트 파일 패턴
  • sonar.go.coverage.reportPaths : 코드 커버리지 보고서의 경로

위의 properties 에는 우리에게 중요한 sonar server 의 호스트 주소와 토큰에 대한 정보가 빠져있습니다. 이는 소스트리에 올라가지 않게 환경변수로 지정하여 사용할 수 있게 합니다.

예시로 Docker 명령어를 통해 sonnar-scanner 를 활용한 CMD 입니다.

docker run --rm \\
    --network host \\
    -e SONAR_HOST_URL="<http://sona-dev.kollus.com>" \\
    -e SONAR_LOGIN="ddddddd" \\
    -v "${PWD}:/usr/src" \\
    sonarsource/sonar-scanner-cli

sonnar scanner 가 성공적으로 적용되면 아래와 같은 로그가 찍히면서 sonar-server 로 요청을 전송합니다.

 

 

 

Gradle

그럼 다음으로 Gradle 프로젝트를 scan 하는 실습을 진행해보겠습니다.

현재 제가 테스트하는 sonarqube-server 는 java 8 을 지원하기 때문에 java 8 version 으로 구성하였습니다.

 

build.gradle

plugins {
    id 'org.springframework.boot' version '2.7.13'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    id "org.sonarqube" version "3.3"
    id 'java'
    id 'jacoco'
}

group 'net.catenoid'
version '1.0-SNAPSHOT'
sourceCompatibility = '8'

repositories {
    mavenCentral()
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

dependencies {

    /** spring **/
    implementation 'org.springframework.boot:spring-boot-starter-web'
    annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
    developmentOnly 'org.springframework.boot:spring-boot-devtools'

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

    /** DB **/
    runtimeOnly 'com.h2database:h2'

    /** TEST **/
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'

    /** sonar qube **/
    implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3'
}

test {
    useJUnitPlatform()
    finalizedBy 'jacocoTestReport'
}

jacoco {
    toolVersion = "0.8.7"
}

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

sonarqube {
    properties {
        property "sonar.sources", "src"
        property "sonar.language", "java"
        property "sonar.sourceEncoding", "UTF-8"
        property "sonar.java.binaries", "${buildDir}/classes"
        property "sonar.test.inclusions", "**/*Test.java"
        property "sonar.exclusions", "**/resources/static/**, **/Q*.class, **/test/**"
        property "sonar.coverage.jacoco.xmlReportPaths", "${buildDir}/reports/jacoco/test/jacocoTestReport.xml"
    }
}

build.gradle 을 보니, 위의 sonner-project.properties 에 명시되었던 정보가 들어는 것을 확인할 수 있습니다.

 

./gradlew sonar \\
  -Dsonar.projectKey=sonarqube-java \\
  -Dsonar.projectName=sonarqube-java-001 \\
  -Dsonar.host.url=http://sona-dev.com \\
  -Dsonar.token=ddddd

위와 같은 gradle 명령어를 실행하면 golang 프로젝트와 마찬가지로 데이터를 전송할 수 있습니다.

 

 

 

Sonarqube Github Action 적용

이러한 Sonarqube 를 Github Action 으로도 적용할 수 있습니다.

애초에 우리가 개발을 진행할 때마다 일일이 sonar-scanner 명령어를 사용할 수 없기 때문에 GithubAction 을 통해 자동화를 진행할 수 있습니다.

 

 

 

실제 깃허브에서도 Github Actions 을 만들 때 sonarqube 에 대한 템플릿을 제공하고 있습니다.

SonarCloud 는 sonarqube-server 를 우리가 아닌, sonar 회사에서 만든 클라우드 플랫폼과 연동하는 것을 말하며, SonarQube 는 자체적으로 sonarqube-server 를 만들었을 때 사용합니다.

 

Golang, PHP 등 일반적 Language

name: SonarQube analysis

on:
  pull_request:
    branches: [ "main", "develop" ]
    types: [opened, synchronize]

jobs:
  Analysis:
    runs-on: ubuntu-latest

    steps:
      - name: Analyze with SonarQube
        uses: SonarSource/sonarqube-scan-action@7295e71c9583053f5bf40e9d4068a0c974603ec8
        env:
          SONAR_PROJECT_KEY: sonar-golang
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}  # Needed to get PR information
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}   # Generate a token on SonarQube, add it to the secrets of this repo with the name SONAR_TOKEN (Settings > Secrets > Actions > add new repository secret)
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}   # add the URL of your instance to the secrets of this repo with the name SONAR_HOST_URL (Settings > Secrets > Actions > add new repository secret)
          PR_NUMBER: ${{ github.event.pull_request.number }}
        with:
          args:
            -Dsonar.projectKey=sonar-golang
            -Dsonar.projectName=${{ env.SONAR_PROJECT_KEY }}-${{ env.PR_NUMBER }}
  • 위의 깃헙 액션 yaml 파일을 보면, PR 이 opened , synchronize 될 때 동작하게 되어 있습니다.
  • 우리가 개발을 완료하고, 코드리뷰를 위해 PR을 생성하는 순간과, 코드 수정하고 Push 하는 순간에 위의 이벤트를 실행하도록 구성하였습니다.
  • sonar_project_key 는 고정으로 설정되어있고, projectName 은 PR_NUMBER 와의 조합을 통해 해당 분석이 어느 PR 에 대한 분석인지 알 수 있습니다.
  • 위의 GITHUB_TOKEN 은 우리가 따로 셋팅할 필요 없이 깃허브에서 자체적으로 환경변수 값을 넣어줍니다.
  • SONAR_TOKEN 과 SONAR_HOST_URL 은 Repository → Settings → Secrets and variables → Actions 메뉴에서 설정할 수 있습니다.

 

 

 

Gradle Project Github Action

name: sonarqube analytics

# 하기 내용에 해당하는 이벤트 발생 시 github action 동작
on:
  pull_request: # 모든 브랜치에서 PR이 일어났을 때 github action 동작
    branches: [ "main", "develop" ]
    types: [opened, synchronize]

jobs:
  build:
    runs-on: ubuntu-22.04

    steps:
      - name: Checkout source code
        uses: actions/checkout@v2
  analysis:
    runs-on: ubuntu-22.04
    env:
      SONARQUBE_PROJECT_KEY: jungin
      SONARQUBE_URL: ${{secrets.SONAR_HOST_URL}}
      SONARQUBE_TOKEN: ${{secrets.SONAR_TOKEN}}
      PR_NUMBER: ${{github.event.pull_request.number}}
    steps:
      # 소스코드 체크아웃 수행
      - name: Checkout source code
        uses: actions/checkout@v2

      # Gralde 의 Scanner 발동, 위의 env 에서 선언한 환경변수와 함께 발동
      - name: Sonaqube Analysis
        run: ./gradlew test sonarqube
          -Dsonar.host.url=${{ env.SONARQUBE_URL }}
          -Dsonar.projectKey=${{ env.SONARQUBE_PROJECT_KEY }}
          -Dsonar.projectName=${{ env.SONARQUBE_PROJECT_KEY }}-${{ env.PR_NUMBER }}
          -Dsonar.login=${{ env.SONARQUBE_TOKEN }}
  • Gradle 프로젝트 같은 경우 GithubAction 의 sonarqube-scan-action 을 활용하지 않고 gradlew 명령어를 통해 sonar-scan 을 진행합니다.

 

 

 

Reference


Comments