SonarQube
SonarQube
코드 품질 및 보안 분석 플랫폼. 정적 분석, 기술 부채 관리.
SonarQube
코드 품질 및 보안 분석 플랫폼. 정적 분석, 기술 부채 관리.
SonarQube(소나큐브)는 소스 코드의 품질과 보안을 자동으로 분석하는 오픈소스 정적 분석(Static Analysis) 플랫폼입니다. SonarSource에서 개발하여 Java, JavaScript, TypeScript, Python, C/C++, C#, Go, PHP 등 30개 이상의 프로그래밍 언어를 지원합니다. 코드 냄새(Code Smell), 버그, 보안 취약점(OWASP Top 10, CWE)을 탐지하고, 기술 부채(Technical Debt)를 시간 단위로 추정합니다.
SonarQube의 핵심 개념은 Quality Gate입니다. 코드 커버리지, 중복률, 새 코드의 버그/취약점 수 등 품질 지표에 대한 임계값을 설정하고, 이를 통과해야만 배포가 가능하도록 CI/CD 파이프라인과 통합합니다. "Clean as You Code" 철학에 따라 새로 작성/수정된 코드에 엄격한 기준을 적용하여 점진적으로 코드베이스 품질을 개선합니다.
분석 결과는 웹 대시보드에서 프로젝트별, 이슈별로 확인할 수 있습니다. 각 이슈에 대해 문제 설명, 영향, 수정 방법을 제안하며, IDE 플러그인(SonarLint)을 통해 개발자가 코딩 중 실시간으로 피드백을 받을 수 있습니다. Pull Request 분석 기능으로 코드 리뷰 전 자동으로 문제를 감지하고, 보고서를 PR 댓글로 남깁니다.
SonarQube는 Community(무료), Developer, Enterprise, Data Center 에디션으로 제공됩니다. 상위 에디션에서는 OWASP/CWE 매핑, 브랜치 분석, PR decoration, 더 많은 언어 지원 등 추가 기능을 제공합니다. SonarCloud는 SaaS 버전으로 GitHub, GitLab, Azure DevOps와 쉽게 통합됩니다. DevSecOps 파이프라인에서 SAST(Static Application Security Testing) 도구로 널리 사용됩니다.
# SonarQube 서버 실행
docker run -d --name sonarqube \
-p 9000:9000 \
-e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true \
-v sonarqube_data:/opt/sonarqube/data \
-v sonarqube_extensions:/opt/sonarqube/extensions \
-v sonarqube_logs:/opt/sonarqube/logs \
sonarqube:lts-community
# 브라우저에서 http://localhost:9000 접속
# 초기 계정: admin / admin
# PostgreSQL과 함께 실행 (프로덕션)
# docker-compose.yml
# version: "3"
# services:
# sonarqube:
# image: sonarqube:lts-community
# depends_on:
# - db
# environment:
# SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar
# SONAR_JDBC_USERNAME: sonar
# SONAR_JDBC_PASSWORD: sonar
# ports:
# - "9000:9000"
# db:
# image: postgres:15
# environment:
# POSTGRES_USER: sonar
# POSTGRES_PASSWORD: sonar
# POSTGRES_DB: sonar
# 프로젝트 식별
sonar.projectKey=my-project
sonar.projectName=My Project
sonar.projectVersion=1.0
# 소스 코드 위치
sonar.sources=src
sonar.tests=tests
sonar.exclusions=**/node_modules/**,**/dist/**,**/*.test.ts
# 언어별 설정
sonar.language=java
sonar.java.binaries=target/classes
sonar.java.source=17
# 커버리지 리포트 연동
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.python.coverage.reportPaths=coverage.xml
# 정적 분석 리포트 연동 (ESLint, Pylint 등)
sonar.eslint.reportPaths=eslint-report.json
sonar.python.pylint.reportPaths=pylint-report.txt
# 인코딩
sonar.sourceEncoding=UTF-8
# 브랜치 분석 (Developer Edition 이상)
# sonar.branch.name=feature/my-feature
# Pull Request 분석
# sonar.pullrequest.key=123
# sonar.pullrequest.branch=feature/my-feature
# sonar.pullrequest.base=main
# .github/workflows/sonarqube.yml
name: SonarQube Analysis
on:
push:
branches: [main, develop]
pull_request:
types: [opened, synchronize, reopened]
jobs:
sonarqube:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 전체 히스토리 (blame 분석용)
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Cache SonarQube packages
uses: actions/cache@v4
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
- name: Build and Test with Coverage
run: |
./gradlew build jacocoTestReport
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
with:
args: >
-Dsonar.projectKey=my-project
-Dsonar.sources=src/main
-Dsonar.tests=src/test
-Dsonar.java.binaries=build/classes
-Dsonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml
# PR 분석 시 Quality Gate 결과 확인
- name: SonarQube Quality Gate
uses: SonarSource/sonarqube-quality-gate-action@master
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# 의존성 설치
pip install coverage pytest pytest-cov
# 테스트 및 커버리지 수집
pytest --cov=src --cov-report=xml --cov-report=html tests/
# SonarScanner 실행
sonar-scanner \
-Dsonar.projectKey=my-python-project \
-Dsonar.sources=src \
-Dsonar.tests=tests \
-Dsonar.python.coverage.reportPaths=coverage.xml \
-Dsonar.python.version=3.11 \
-Dsonar.host.url=http://localhost:9000 \
-Dsonar.token=$SONAR_TOKEN
import requests
from typing import Optional
class SonarQubeClient:
"""SonarQube REST API 클라이언트"""
def __init__(self, base_url: str, token: str):
self.base_url = base_url.rstrip('/')
self.session = requests.Session()
self.session.auth = (token, '') # token as username, empty password
def get_project_status(self, project_key: str) -> dict:
"""프로젝트 Quality Gate 상태 조회"""
response = self.session.get(
f"{self.base_url}/api/qualitygates/project_status",
params={"projectKey": project_key}
)
response.raise_for_status()
return response.json()
def get_issues(
self,
project_key: str,
severities: Optional[list] = None,
types: Optional[list] = None,
page_size: int = 100
) -> list:
"""프로젝트 이슈 목록 조회"""
params = {
"componentKeys": project_key,
"ps": page_size,
"resolved": "false"
}
if severities:
params["severities"] = ",".join(severities)
if types:
params["types"] = ",".join(types)
all_issues = []
page = 1
while True:
params["p"] = page
response = self.session.get(
f"{self.base_url}/api/issues/search",
params=params
)
response.raise_for_status()
data = response.json()
all_issues.extend(data["issues"])
if len(all_issues) >= data["total"]:
break
page += 1
return all_issues
def get_metrics(self, project_key: str, metrics: list) -> dict:
"""프로젝트 메트릭 조회"""
response = self.session.get(
f"{self.base_url}/api/measures/component",
params={
"component": project_key,
"metricKeys": ",".join(metrics)
}
)
response.raise_for_status()
return response.json()
def create_quality_gate_condition(
self,
gate_name: str,
metric: str,
operator: str, # "LT", "GT"
error: str
):
"""Quality Gate 조건 추가"""
# 먼저 gate ID 조회
gates = self.session.get(
f"{self.base_url}/api/qualitygates/list"
).json()
gate_id = None
for gate in gates["qualitygates"]:
if gate["name"] == gate_name:
gate_id = gate["id"]
break
if not gate_id:
raise ValueError(f"Quality Gate '{gate_name}' not found")
response = self.session.post(
f"{self.base_url}/api/qualitygates/create_condition",
data={
"gateId": gate_id,
"metric": metric,
"op": operator,
"error": error
}
)
response.raise_for_status()
return response.json()
# 사용 예제
client = SonarQubeClient("http://localhost:9000", "squ_xxxxx")
# Quality Gate 상태 확인
status = client.get_project_status("my-project")
print(f"Quality Gate: {status['projectStatus']['status']}")
# 심각한 이슈 조회
critical_issues = client.get_issues(
"my-project",
severities=["BLOCKER", "CRITICAL"],
types=["BUG", "VULNERABILITY"]
)
print(f"Critical issues: {len(critical_issues)}")
# 주요 메트릭 조회
metrics = client.get_metrics("my-project", [
"coverage",
"duplicated_lines_density",
"sqale_rating", # Maintainability
"reliability_rating",
"security_rating",
"ncloc" # Lines of code
])
for measure in metrics["component"]["measures"]:
print(f"{measure['metric']}: {measure['value']}")
SonarQube가 탐지한 이슈 중 일부는 실제 문제가 아닌 경우(False Positive)가 있습니다. Won't Fix나 False Positive로 표시하여 노이즈를 줄이고, 커스텀 규칙 프로파일을 만들어 팀에 맞게 조정하세요.
레거시 코드베이스에 SonarQube를 도입하면 수천 개의 이슈가 나올 수 있습니다. 모든 이슈를 한 번에 해결하려 하지 말고, "Clean as You Code" 원칙에 따라 새 코드에만 엄격한 기준을 적용하여 점진적으로 개선하세요.
코드 커버리지 80%가 품질을 보장하지 않습니다. 의미 있는 테스트인지, 엣지 케이스를 다루는지가 더 중요합니다. 커버리지를 높이기 위해 단순 실행만 하는 테스트는 가치가 없습니다.