우리 팀의 코드 품질은? 정적코드 분석도구, 소나큐브 적용기

SungSiHyung·2021년 9월 25일
4
post-thumbnail

소나큐브 소개

소나큐브는 대표적인 코드 정적분석 툴입니다. 정적 분석이란 말은, 프로그램을 실행하지 않고 코드를 살펴 문제점을 살펴준다는 뜻입니다.

소나큐브가 필요한 이유와 분석해주는 항목에 대해선 다음 슬라이드에서 잘 설명되고 있어 첨부합니다.
sonarsource, sonarqube 소개

제가 진행하는 프로젝트에선 github PR -> sonarqube에서 코드 정적분석 -> PR에 결과 리포트 링크를 하는 식으로 활용하고 있습니다~~!

개발환경

프로젝트
1. Spring Boot 2.5.2
2. java 11

소나큐브 서버
1. AWS EC2, ubuntu 18.04
2. sonarqube 9.1, commuity version

소나큐브 개요

소나큐브는 크게 3부분으로 이루어져 있습니다.

  1. Scanner
    스캐너는 정적분석할 코드를 소나큐브 서버로 전달해주는 역할을 합니다. gradle, maven, jenkins, .net, ant, Azure Devops에 대해선 구현되어있는 플러그인을 제공하고, 직접 스캐너를 커스텀할 수 도 있습니다.
    공식문서

  2. Sonarqube server
    정적 분석을 할 프로그램이 돌아가고 있는 서버입니다. 스캐너로부터 API 요청을 받으면, 해당 코드에 대한 정적분석을 시행하고 리포트를 생성합니다.

  3. Database server
    분석결과를 영속화할 데이터베이스입니다. Postgresql, oracle, MS SQL을 지원합니다. mysql은? 만약 데이터베이스 서버를 따로 연결하지 않는다면, 소나큐브 프로그램 내부 H2 DB를 사용합니다.

소나큐브 서버 구성

저는 EC2 환경에 도커를 설치하여 소나큐브 서버를 구성하도록 하겠습니다.

기본 환경설정

소나큐브가 요청하는 기본 사양 중 다음 내용이 있습니다.
리눅스 - 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

도커 설치

EC2를 생성 후, docker를 설치해줍니다.

# 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 이라는 이름으로 다음 파일을 만들어주세요.

# console
vim docker-compose.yml
# 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:

컨테이너 실행

docker compose를 통해 컨테이너를 올려줍니다.

# EC2 console
# docker-compose.yml문을 데몬 옵션으로 실행한다는 의미입니다.
docker-compose up -d

docker ps 명령어를 통해 컨테이너가 잘 올라왔는지 확인해줍니다.

확인

해당 EC2 public ip로 접속해보면!

초기 ID / 비밀번호는 admin / admin입니다.
초기 접속 후 비밀번호 설정을 해주면, 서버 설정이 완료됩니다.

스캐너 등록

젠킨스 플러그인과 그래들 플러그인 중 고민했고, CI/CD 도구로 깃헙액션도 활용하고 있었기 때문에 그래들 플러그인을 통해 스캐너를 활용하기로 결정했습니다. 젠킨스 플러그인을 활용하는 내용은 다음 블로그에 잘 정리되어 있습니다.
링크

토큰 생성

아무 스캐너나 우리 소나큐브 서버에 분석을 요청하면 안 되기에, 토큰을 이용한 인증이 필요합니다.
1. sonarqube 서버 우상단에 아이콘을 클릭합니다.

  1. Security 탭에서 generate tokens를 통해 토큰을 생성합니다.

  1. 토큰 시크릿 텍스트는 잘 모셔둡시다.

프로젝트 build.gradle에 스캐너 등록

소나큐브는 코드 커버리지도 검사하는데, 코드 커버리지는 jacoco 라이브러리에 의존합니다. 다음 기술 블로그에 jacoco적용 에 대한 자세한 내용이 있습니다.
링크

아래는 기존 build.gradle에서, sonarqube를 등록하는데 필요한 부분만 따로 적은 내용입니다.

# build.gradle
plugins {
	...
    id 'org.sonarqube' version '3.3' 
    id 'java'
    id 'jacoco'
}

group = 'com.myproject.'
version = '1.0.0-beta'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

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

test {
    useJUnitPlatform()
    finalizedBy 'jacocoTestReport'
}

jacoco { 
    toolVersion = "0.8.7"
}

jacocoTestReport { 
    reports {
        xml.enabled true // sonarqube 분석에 필요한 xml파일만 생성
        csv.enabled false
        html.enabled false
    }

}

sonarqube { 
    properties {
        property "sonar.host.url", "{서버 url}"
        property "sonar.login", "{access token}"
        property "sonar.projectKey", "{프로젝트 ID}"
        property "sonar.projectName" , "{이번 빌드의 이름 이름}"
        property "sonar.sources", "src" # 소스 경로
        property "sonar.language", "java" # 언어
        property "sonar.sourceEncoding", "UTF-8"
        property "sonar.profile", "Sonar way" # 소나큐브에서 적용할 프로필(분석할 수준 설정)
        property "sonar.java.binaries", "${buildDir}/classes" # 자바 클래스 파일위치
        property "sonar.test.inclusions", "**/*Test.java" # 코드 분석에 사용할 테스트 소스 
        property "sonar.exclusions", "**/resources/static/**, **/Q*.class, **/test/**" # 테스트커버리지에서 제외할 파일, 예제에선 정적 js 파일과 queryDSL Q파일 제외
        property "sonar.coverage.jacoco.xmlReportPaths", "${buildDir}/reports/jacoco/test/jacocoTestReport.xml" # jacoco 플러그인의 결과파일
    }
}

sonarqube.properties의 내용들은 gradle task를 실행할 때 -Dsonar.accessToken={토큰}과 같은 방식으로 줄 수 있습니다.

project ID는 각각의 프로젝트를 가르킵니다. project ID를 다르게 하면 새로운 프로젝트가 생성되고, project Name만 다르게 하면 같은 프로젝트에서 이름만 달라집니다. 제가 진행중인 프로젝트에선 name을 PR번호에 따라 동적으로 변경되도록 했습니다.

access token 자리엔 아까 복사해놓은 secret text를 붙여넣어주면 됩니다. 해당 토큰값은 노출되면 안 되기에, build.gradle에 명시하기보단 실행시 인자로 주시는 것을 추천합니다.

gradle task 실행

확인을 위해 콘솔에서 task를 실행시켜봅니다!

# console
./gradlew -Dsonar.login={access token}

서버 화면으로 이용해, 확인해보면 새로운 project가 등록된 것을 확인할 수 있습니다.

이제 태스크를 실행시켜서 코드를 정적분석 할 수 있습니다.
하지만 필요할 때마다 태스크를 실행시키는 것은 불편합니다.
PR을 보낼 때 마다 태스크를 실행하도록 해보겠습니다~~

깃헙액션과 통합

깃헙 액션은 깃허브에서 제공하는 CI/CD 툴입니다. commit이나 push, cron, webhook 등의 상황에서 작업을 자동으로 특정 작업을 할 수 있게 해줍니다.
main 브랜치 프로젝트 루트에 /.github/workflows/*.yml 형식으로 작성하면 github에서 인식하여 동작합니다.


workflow문

workflow.yml은 크게 언제 수행할지 선언하는 on절, 순서대로 수행 될 플로우(job)들을 명시하는 jobs절, 각 job들에서 수행할 기능을 작성하는 steps절 등이 있습니다.

workflow에 대해선 공식문서를 참고하는 것이 가장 좋습니다. 예시도 상세하고, 기능도 잘 설명이 되어있습니다. 링크

다만 분량이 방대하기 때문에, workflow 예제들을 먼저 보고 필요한 부분만 추가하는 식으로 학습하는 것이 좋다고 생각합니다~~

실제로 활용하고 있는 workflow문을 가져왔습니다.

# workflow의 이름
name: backend-build-test

# PR 요청에 대해
on:
  pull_request:
    # 특정 브랜치만
    branches:
      - develop # develop 브랜치에서 pr 이벤트가 일어났을 때 실행
    # pr이 생성 되었을 때, 담당 유저가 등록되었을 때, PR에 코드가 머지되었을 때, 라벨이 달렸을 때 동작  
    types: [opened, assigned, synchronize, labeled]

# job에 공통된 옵션을 준다
defaults:
  # build steps의 Run
  run:
    # ./backend 경로에서 실행
    working-directory: ./backend

# 실행 해야할 job
jobs:
  # Job의 이름, 자유롭게 짓습니다.
  ###### 본문 내용과 관련 없는 Job, 빌드 테스트
  build:
    # label이 [backend] (id: 3141722951) 일때만 동작
    if: contains(github.event.pull_request.labels.*.id, 3141722951)
    # ubuntu 환경에서 실행 (환경은 github이 제공)
    runs-on: ubuntu-latest
    # 각 단계
    steps:
      # 소스코드 체크아웃
      - name: Checkout source code
        uses: actions/checkout@v2
        # 서브모듈 설정
        with:
          submodules: true
          # secrets 변수는 github 세팅에서 미리 세팅한 데이터
          token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }}
	  # 빌드
      - name: Build Test
        run: ./gradlew bootJar
  ###### 아래부터 sonarqube와 관련있는 부분 시작
  analysis:
    # label이 [backend] (id: 3141722951) 일때만 동작
    if: contains(github.event.pull_request.labels.*.id, 3141722951)
    # ubuntu 환경에서 실행 (환경은 github이 제공)
    runs-on: ubuntu-latest
    # 이 Job에서 사용할 환경변수
    env:
      # KEY - VALUE
      SONARQUBE_ID: jujeol-pr
      SONARQUBE_URL: ${{ secrets.SONARQUBE_URL }}
      PR_NUMBER: ${{ github.event.pull_request.number }}
    steps:
      - name: Checkout source code
        uses: actions/checkout@v2
        with:
          submodules: true
          token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }}

        # 위에서 설정했던 정적코드 전송 태스크 실행
      - name: Sonaqube Analysis
        run: ./gradlew test sonarqube
             -Dsonar.host.url=${{ env.SONARQUBE_URL }}
             -Dsonar.projectKey=${{ env.SONARQUBE_ID }}
             # project의 제목을 {프로젝트명}-{pr번호} 형식으로 
             -Dsonar.projectName=${{ env.SONARQUBE_ID }}-${{ env.PR_NUMBER }}
             -Dsonar.login=${{ secrets.SONARQUBE_ACCESS_TOKEN }}

      - name: Comment Sonarqube URL
        uses: actions/github-script@v4
        with:
          # 이슈에 코멘트를 달아주는 스크립트 실행
          script: |
            const { SONARQUBE_ID, SONARQUBE_URL, PR_NUMBER } = process.env
            github.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `📊 ${ SONARQUBE_ID }-${ PR_NUMBER } 분석 결과 확인하기 [링크](${SONARQUBE_URL})`
            })

위에서 사용한 secrets 환경변수는 레포지토리 설정에서 선언한 내용을 가져옵니다.

해당 workflow를 적용하고나면 pr을 보낼 때, 그리고 pr에 푸쉬하면 리포트 url을 달아주게 됩니다.

적용하고보니

sonarqube와 github action을 이용해 프로젝트의 코드가 pr이 일어날 때마다 리포트를 받아볼 수 있는 기능을 구현해보았습니다. 적용하고 활용해보니 단순히 테스트 코드의 커버리지를 넘어 code smell, security hotspot, bugs 가능성이 있는 코드를 리포팅해주어 편리했는데요~!

근거와 수정방법까지 가이드 해주어 되려 소나큐브로부터 배우는 경험도 상당했습니다! ㅎ_ㅎ

profile
블로그 이전하였습니다!!! https://sihyung92.oopy.io/

0개의 댓글