정적 코드 분석이란?
정적 코드 분석은 프로그램 실행 없이 소스 코드를 분석하여 코드의 품질, 성능, 보안 문제 등을 찾아내는 기법이다. 이를 통해 코드 작성 단계에서 잠재적인 오류가 결함을 조기에 발견하고 수정할 수 있다.
주요 특징
- 잠재적인 버그가 발생할 수 있는 코드 발견
- 코드 컨벤션 위반 여부 판단
- 오타 검수
- 사용되지 않는 코드(미사용 import) 발견
- 잠재적 보안 취약점 발견
SonarCloud란?
이전에 SonarQube를 통해 프로젝트 정적 코드 분석을 해보았다. SonarQube는 코드의 구조, 문법, 스타일, 잠재적인 버그, 코드 복잡도, 보안 취약점 등을 분석하고 테스트 코드의 커버리지를 측정하는 기능을 제공한다. 이러한 분석을 통해 개발자는 코드 품질을 유지하고 개선할 수 있는 장점이 있지만 서버를 직접 구축해야 하는 단점이 있다.
반면 SonarCloud는 이미 구축되어 있는 서버를 이용하기 때문에 이러한 서버 구축의 부담을 덜 수 있다. SonarCloud는 클라우드 기반 서비스이기 때문에 자체 서버를 설치하거나 유지 보수할 필요가 없다.
따라서 보안 상의 문제로 클라우드 기반 서비스를 사용할 수 없거나, SonarQube에서 제공하는 다양한 플러그인을 사용하고 싶은 경우 SonarQube를 사용하고, 그 외에는 서버를 직접 관리할 필요가 없는 SonarCloud를 사용하는 것이 좋을 것 같다.
SonarCloud 설정
SonarCloud는 자바 프로젝트의 커버리지 리포트를 생성하지 않아 별도의 코드 커버리지 측정 도구를 사용하여야 한다.
나의 경우 이전에 JaCoCo를 이용해 코드 커버리지를 측정했기도 하고, SonarCloud에서도 JaCoCo 세팅을 추천하기 때문에 커버리지 측정 도구는 그대로 JaCoCo를 사용하기로 했다. 이전에 JaCoCo 적용 글을 작성해 해당 내용은 생략하려고 한다.
1️⃣ SonarCloud 기본 설정, GitHub에 토큰 등록
SonarCloud에서 회원가입을 하고 Organization과 분석할 프로젝트를 선택한다. 분석할 프로젝트가 Public 레포지토리인 경우 무료 플랜을 사용할 수 있다.
SonarCloud는 기본적으로 Automatic Analysis를 지원하지만 Github Actions CI를 사용할 경우 SonarCloud에서 제공하는 자동 분석을 꺼주어야 한다.(CI-based Analysis는 Automatic Analysis와 함께 실행 시 충돌 발생)
프로젝트의 Administration > Analysis Method > Automatic Analysis를 off 한다.
그 후 하단에 있는 With GitHub Actions를 눌러 'SONAR_TOKEN' 발급을 받아 Github 레포지토리의
Secrets and variables > Actions의 Repository secrets
에 저장한다.
Github Actions에서는 보안상의 이유로 소스코드에 노출하면 안되는 민감한 정보들(ex API 키, 인증 토큰, 비밀번호, SSH 키, 데이터베이스 자격 증명 및 기타 환경 변수)을 Github Secrets 변수에 등록해 정보를 안전하게 저장하고 GitHub Actions 워크플로우에서 필요할 때만 사용할 수 있도록 도와준다.
2️⃣ build.gradle에 sonar 의존성 추가 및 설정
그 후 SonarCloud에 나와있는 다음 yaml 파일 안내사항대로 루트 build.gradle에 sonar 의존성을 추가해준다.
plugins {
// ...
id "org.sonarqube" version "5.0.0.4638"
}
sonar {
properties {
property "sonar.projectKey", "프로젝트 키"
property "sonar.organization", "조직명"
property "sonar.host.url", "https://sonarcloud.io"
property 'sonar.sources', 'src'
property 'sonar.language', 'java'
property 'sonar.sourceEncoding', 'UTF-8'
property "sonar.profile", "Sonar way"
property 'sonar.test.inclusions', '**/*Test.java'
// 테스트 커버리지에서 제외할 클래스
property 'sonar.exclusions', '**/Q*.java, **/config/**'
property 'sonar.java.coveragePlugin', 'jacoco'
// JaCoCo Coverage Report 주소
property 'sonar.coverage.jacoco.xmlReportPaths', "${project.rootDir}/support/jacoco/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml"
}
}
여기서 projectKey와 organization은 본인 프로젝트에 맞게 설정해주어야 한다.
projectKey는 SonarCloud 주소창에 나와있는 https://sonarcloud.io/summary/overall?id={projectKey}' 값으로 projectKey'를 확인할 수 있다.
sonar.coverage.jacoco.xmlReportPaths 속성은 JaCoCo의 XML 파일 경로를 명시하는 역할을 한다. 프로젝트가 멀티 모듈인 경우, 각 모듈마다 별도의 XML 파일이 생성되게 된다. 이 경우 jacoco-aggregation을 통해 여러 개의 XML 보고서를 하나의 파일로 합칠 수 있는데, 만약 통합 보고서를 사용하지 않고 개별 보고서를 사용한다면 각 JaCoCo XML 파일의 경로를 각각 설정해주어야 한다.
🫧 멀티 모듈에서 jacoco-aggregation을 이용하지 않고 개별 보고서를 사용하는 경우
루트 build.gradle
subprojects {
apply plugin: 'org.sonarqube'
sonar {
properties {
property 'sonar.java.binaries', "${buildDir}/classes"
// jacoco-aggregation을 사용하지 않은 경우 각 프로젝트의 buildDir마다 따로 적용
property 'sonar.coverage.jacoco.xmlReportPaths', "${buildDir}/reports/jacoco.xml"
}
}
}
sonar {
properties {
property "sonar.projectKey", "프로젝트 키"
property "sonar.organization", "조직명"
property "sonar.host.url", "https://sonarcloud.io"
property 'sonar.sources', 'src'
property 'sonar.language', 'java'
property 'sonar.sourceEncoding', 'UTF-8'
property "sonar.profile", "Sonar way"
property 'sonar.test.inclusions', '**/*Test.java'
property 'sonar.exclusions', '**/Q*.java, **/config/**'
property 'sonar.java.coveragePlugin', 'jacoco'
}
}
3️⃣ GitHub workflow 설정
name: SonarCloud
on:
push:
branches: [ "develop" ]
pull_request:
branches: [ "develop" ]
jobs:
build_and_analyze:
name: Build and SonarCloud Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout branch
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: ${{ runner.os }}-gradle
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Build with Gradle
run: ./gradlew build
- name: Cache SonarCloud packages
uses: actions/cache@v4
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: SonarCloud Scan
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: ./gradlew sonar --info --stacktrace
위 workflow는 다음과 같은 기능을 수행한다.
- develop 브랜치로 체크아웃
- JDK 17 설치
- Gradle, SonarCloud 캐시 설정
- Gradle 빌드 수행
- SonarCloud 분석 실행
여기서 secrets.SONAR_TOKEN을 제외한 모든 변수들은 개발자가 따로 지정하지 않아도 자동으로 설정된다.(SONAR_TOKEN은 위 1번 과정에서 이미 저장한 값)
4️⃣ 결과 확인
모든 설정을 마친 후 PR을 올리면 SonarCloud bot이 요약된 정적 분석 결과를 comment로 남겨주는 것을 확인할 수 있다.
+ GitHub 브랜치 규칙 정하기
적용할 레포지토리의 [Settings > Rules > Rulesets]에서 New branch ruleset을 추가해 준다.
적용할 브랜치의 ruleset이 이미 있는 경우, 해당하는 브랜치의 ruleset을 수정해야 한다.
develop 브랜치의 ruleset이기 때문에 Ruleset Name을 develop으로 지정해 주고, Enforcement status를 Active로 활성화시킨다.
Target branches의 Add target에서 규칙을 적용할 브랜치를 추가한다.
나의 경우 현재 프로젝트의 Default 브랜치가 develop 브랜치로 설정되어 있어 Default를 추가해 주었다.
기준을 통과했을 때만 merge 할 수 있도록 [Require status checks to pass] 규칙을 선택하고, 세부 규칙으로 [Require branches to be up to date before merging]을 선택해 PR merge 전 해당 브랜치가 최신인지 확인한다.
[Add checks]에서 SonarCloud 분석 관련 checks를 추가해 준다.
만약 위처럼 Suggestion이 나오지 않을 경우 [Add checks]에서 워크플로우의 작업 이름을 검색해 추가 후 [Any source]를 클릭해 SonarCloud를 연결해 준다.
설정을 완료한 후 PR 생성 시, GitHub Actions가 자동으로 build와 analyze를 진행한다.
이처럼 SonarCloud의 통과 조건인 커버리지 80% 이상을 충족하지 않으면 ruleset에서 PR merge를 제한하게 된다.
'Back-end' 카테고리의 다른 글
[Java] GC(: Garbage Collection) 로그로 G1GC 동작과정 확인하기 (1) | 2024.09.11 |
---|---|
[JPA] N+1 문제 해결 방법 (0) | 2024.08.27 |
[Spring] SonarQube로 프로젝트 정적 코드 분석 (0) | 2024.08.19 |
[Spring Boot] 로컬 환경에서 Github Actions 테스트하기 (0) | 2024.08.16 |
[JUnit] 테스트 코드에 Test Fixture 사용하기: 테스트 데이터 재사용 (0) | 2024.08.05 |