개요
지난 학기에 수강했던 DevOps 수업에서 SonarCloud를 처음 접했다. 당시에는 이미 개발이 완료된 프로젝트를 배포·운영하는 것이 목적이었기 때문에, 개발 과정에서 코드 품질을 지속적으로 관리하는 용도로 SonarCloud를 제대로 활용하지 못한 점이 아쉬움으로 남았다.
이번에 진행한 여행기록 서비스 프로젝트에서는 이 아쉬움을 보완하고자 SonarCloud를 도입했다. 팀원들에게 SonarCloud의 목적과 장점을 공유하고, CI 파이프라인에 자연스럽게 녹여 코드 품질을 지속적으로 관리할 수 있도록 구성했다.
이번 연동의 핵심 목표는 단순하다.
“코드를 더 클린하게 유지하자.”
이를 위해 dev 브랜치 기준으로 다음과 같은 파이프라인을 구성했다.
- 테스트 실행
- SonarCloud 코드 품질 분석
본 글에서는 Spring Boot 백엔드 프로젝트를 기준으로,
Gradle + GitHub Actions 환경에서 SonarCloud를 연동한 과정을 정리한다.
배경
프로젝트 환경은 다음과 같다.
- Spring Boot 3.5.0 + Java 21
- Gradle 빌드 시스템
- GitHub Actions 기반 CI/CD
- Unit 테스트와 Integration 테스트 분리
주요 작업 내용
1. SonarCloud 플러그인 추가
먼저 build.gradle에 SonarQube 플러그인과 JaCoCo 플러그인을 추가했다.
JaCoCo는 테스트 커버리지 측정을 위해 사용한다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.0'
id 'io.spring.dependency-management' version '1.1.7'
id "org.sonarqube" version "7.2.2.6593"
id 'jacoco' // 커버리지 측정용
}
2. SonarCloud 설정
build.gradle의 sonar 블록에 SonarCloud 분석을 위한 설정을 추가했다.
sonar {
properties {
property "sonar.projectKey", "<project-key>"
property "sonar.organization", "<org>"
// source / test directories
property "sonar.sources", "src/main/java"
property "sonar.tests", "src/test/java"
// coverage report
property "sonar.coverage.jacoco.xmlReportPaths",
"build/reports/jacoco/unitTest/jacocoUnitTestReport.xml"
}
}
DTO, Domain, 설정 코드 등 품질 지표로 관리할 필요가 없는 영역은 제외 대상으로 설정했다.
3. Unit 테스트만 커버리지 측정하기
이번 프로젝트에서는 Integration 테스트를 커버리지 계산에서 제외하고,
Unit 테스트만 기준으로 커버리지를 측정하도록 구성했다.
JUnit 5의 @Tag 기능을 활용했다.
3.1. Gradle Task 설정
// Unit 테스트만 실행 (Integration 테스트 제외)
tasks.register('unitTest', Test) {
useJUnitPlatform {
excludeTags = ['integration']
}
testLogging {
events "passed", "skipped", "failed"
showStandardStreams = true
exceptionFormat = "full"
}
finalizedBy jacocoUnitTestReport
}
// Unit 테스트용 JaCoCo 리포트 생성
tasks.register('jacocoUnitTestReport', JacocoReport) {
dependsOn unitTest
reports {
xml.required = true
xml.outputLocation =
file("build/reports/jacoco/unitTest/jacocoUnitTestReport.xml")
html.required = true
html.outputLocation =
file("build/reports/jacoco/unitTest/html")
}
executionData unitTest
sourceDirectories.from(files("src/main/java"))
classDirectories.from(files("build/classes/java/main"))
}
주의사항
excludeTags는 반드시 useJUnitPlatform() 블록 안에서 설정해야 한다.
블록 밖에 선언하면 Gradle 에러가 발생한다.
3.2. 테스트 태그 지정
Integration 테스트 클래스에는 다음과 같이 태그를 지정했다.
@Tag("integration")
@SpringBootTest
public class IntegrationTestBase {
// ...
}
4. GitHub Actions 워크플로우 설정
.github/workflows/sonarcloud.yml 파일을 생성해 CI 파이프라인을 구성했다.
name: SonarCloud Analysis
on:
push:
branches: [default, development]
pull_request:
types: [opened, synchronize, reopened]
branches: [default, development]
jobs:
build:
name: Build and analyze
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build project (compile only)
run: ./gradlew clean compileJava compileTestJava
- name: Run unit tests with coverage
id: unit_test
continue-on-error: true
run: ./gradlew unitTest jacocoUnitTestReport
- name: SonarCloud Scan
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew sonar
- name: Fail if tests failed
if: steps.unit_test.outcome == 'failure'
run: exit 1
주요 포인트
- 테스트 실패해도 분석은 실행
continue-on-error: true를 사용해 SonarCloud 분석은 항상 수행된다. - PR 머지 차단
테스트 실패 시 워크플로우 자체는 실패로 처리된다. - PR 자동 코멘트
GITHUB_TOKEN을 통해 PR에 분석 결과가 자동으로 남는다.
5. SonarCloud 토큰 설정
- SonarCloud → My Account → Security
- Token 생성
- GitHub 저장소 → Settings → Secrets and variables → Actions
- SONAR_TOKEN으로 등록
GITHUB_TOKEN은 GitHub Actions에서 자동으로 제공된다.
실무 관점에서의 고려사항
테스트 실패해도 분석을 실행하는 이유
- 코드 품질 분석은 테스트 성공 여부와 독립적으로 의미가 있다.
- 실패한 테스트가 있어도 일부 커버리지는 측정 가능하다.
- PR 작성자가 테스트 결과 + 코드 품질 결과를 동시에 확인할 수 있다.
- 최종적으로 테스트 실패 시 머지는 차단되므로 품질은 유지된다.
분석 방식
- Push 분석 (dev/main): 전체 코드 분석
- PR 분석: 변경된 코드만 분석
- 최초 분석 결과가 baseline이 되며, 이후 PR은 이를 기준으로 비교된다.
테스트 결과 확인 방법
- SonarCloud 대시보드
- 코드 품질, 커버리지, 버그·스멜 지표 확인
- GitHub Actions 로그
- 테스트 및 분석 상세 로그 확인
- 로컬 HTML 리포트
- build/reports/tests/unitTest/index.html
트러블슈팅: SonarCloud 기본 브랜치 문제
프로젝트에서 main 브랜치를 사용하지 않고 dev 브랜치를 기본 브랜치로 운영하고 있었다.
하지만 SonarCloud 무료 플랜에서는 기본 브랜치 하나만 정상적으로 분석 결과를 제공한다.
문제는 이 과정이 단순하지 않았다는 점이다.
문제 상황 정리
처음 SonarCloud와 GitHub 저장소를 연동했을 당시에는 main 브랜치가 존재했고,
SonarCloud는 이를 자동으로 default branch로 인식하고 있었다.
이후 프로젝트 운영 과정에서 모종의 이유로 GitHub 저장소의 기본 브랜치를 변경했다.
그리고 다시 SonarCloud와의 연동을 재설정했는데,
이때 SonarCloud가 자동으로 다른 브랜치를 default branch로 인식한 것처럼 보였다.
결과적으로 다음과 같은 상태가 되었다.
- 분석은 정상적으로 실행됨
- GitHub Actions 로그에도 오류 없음
- 그러나 SonarCloud 대시보드에서 분석 결과를 확인할 수 없음
추론한 원인
명확한 공식 문서는 찾기 어려웠지만, 실제 동작을 기준으로 정리해보면 다음과 같다.
- SonarCloud는 최초 연동 시점의 GitHub default branch 상태를 기준으로 기본 브랜치를 결정
- 이후 GitHub에서 default branch를 변경하면
→ SonarCloud 설정과 불일치 상태가 발생할 수 있음 - 무료 플랜에서는 기본 브랜치 1개만 활성화되므로
이 불일치가 발생하면 분석은 되지만 결과가 보이지 않는 상황이 발생
즉,
GitHub의 default branch 변경 이력이 SonarCloud 기본 브랜치 인식에 영향을 준 것으로 보인다.
해결 과정과 추가로 알게 된 점
이론적으로는 다음 방법으로 해결이 가능하다.
- SonarCloud Web UI
Project Settings → General Settings → Branches → Main Branch 변경
하지만 당시에는 Organization owner 권한이 없어 설정 변경이 불가능했다.
결국 팀원들과 논의 후 SonarCloud Organization을 삭제하고 재생성하는 방식으로 문제를 해결했다.
이 과정에서 몇 가지 중요한 점을 알게 되었다.
- SonarCloud에서 멤버 권한과 owner 권한은 명확히 다르다
- 무료 플랜은 기본 브랜치 1개만 정상 지원한다
- GitHub default branch 변경 이력이 SonarCloud 동작에 영향을 줄 수 있다
- 대부분의 제약은 유료 플랜에서 해소된다 :)
마무리
SonarCloud를 연동하면서 PR 단위 코드 품질 분석과 자동 피드백 환경을 구축했다.
Unit 테스트만 커버리지에 포함하고, 테스트 실패 시에도 분석은 실행하되 머지는 차단하는 방식으로 CI 흐름을 구성할 수 있었다.
코드를 “나중에 정리하는 대상”이 아니라,
지속적으로 관리하는 자산으로 다루고 싶다면 SonarCloud는 충분히 도입할 가치가 있다.
'Backend > Trouble Shooting' 카테고리의 다른 글
| JPA 자동 스키마 관리에서 Flyway 기반 DB 마이그레이션 (0) | 2026.02.03 |
|---|---|
| [Spring Boot] Swagger(Springdoc)에서 제네릭 공통 응답 스키마가 꼬이는 문제 해결기 (0) | 2026.01.22 |
| [SpringBoot] UTC 시간 처리, 코드가 아니라 설정으로 정리하기 (Hibernate 6 & Jackson) (0) | 2026.01.21 |
| [SpringBoot] 검증 로직의 위치(@Valid vs Service) : 테스트 코드로 드러난 설계 문제 (0) | 2026.01.14 |
| [SpringBoot] 날짜와 시간 처리를 UTC로 표준화하기 (0) | 2026.01.10 |