지금까지 우리는 스프링 부트의 온갖 심화 기술과 아키텍처를 총동원하여 서비스를 만들었고, 완벽한 테스트 코드(Testcontainers)까지 짜서 동작을 보증했습니다. 그런데 '동작하는 코드'가 곧 '좋은 코드'일까요? 후임 개발자가 들어와서 메서드 하나를 1,000줄짜리 스파게티로 짜놓고, 하드코딩된 비밀번호를 소스에 박아두고, 아무도 안 쓰는 쓰레기 변수(Dead Code)를 잔뜩 남겨둔 채 테스트만 턱걸이로 통과시켜서 배포를 요청했습니다. 바쁜 시니어 개발자가 눈으로 수천 줄의 PR(Pull Request)을 읽으며 이 악취(Code Smell) 나는 코드를 다 걸러낼 수 있을까요?
코드를 실행시키지 않고(Static), 그냥 텍스트 자체만 쓱 훑어보고 논리적 오류, 복잡도, 보안 취약점을 완벽하게 잡아내서 점수를 매겨주는 감독관이 필요합니다.
이 정적 코드 분석(Static Code Analysis) 시장의 압도적 1위 툴이 바로 SonarQube(소나큐브)입니다.
이번 글에서는 "스파게티 코드 원천 차단!" SonarQube의 원리와 젠킨스(Jenkins) CI/CD 파이프라인과의 악랄한 연동(Quality Gate) 비법에 대하여 다뤄보겠습니다.
1. 정적 분석의 3대 핵심 타겟
소나큐브는 코드를 돌려보지 않고 텍스트 구조 패턴만으로 문제를 귀신같이 잡아냅니다.
- Code Smells (코드 악취): 지금 당장 에러는 안 나지만, 나중에 유지보수 할 때 피를 토하게 만드는 코드들입니다. 예를 들어
if-else문이 7번이나 중첩되어 순환 복잡도(Cognitive Complexity)가 너무 높다거나, 사용하지 않는import문이나 변수(Dead Code)가 방치된 것을 잡아냅니다. - Bugs (논리적 버그):
String비교를==로 했다거나,NullPointerException이 터질 가능성이 명백한 위험한 코드를 잡아냅니다. - Vulnerabilities (보안 취약점): 코드에
password = "1234"를 하드코딩했거나, 사용자의 입력을 검증 없이 쿼리에 집어넣어 SQL Injection에 노출된 보안 구멍을 무자비하게 찾아냅니다.
2. Spring Boot Gradle 환경에 Sonar 플러그인 꽂기
로컬이나 도커로 SonarQube 서버(예: http://localhost:9000)를 하나 띄워두고, 우리 스프링 부트 프로젝트가 코드를 그쪽으로 밀어 넣도록 설정해 봅니다.
// build.gradle 최상단에 플러그인 추가
plugins {
id 'org.springframework.boot' version '3.x.x
id 'org.sonarqube' version '4.4.1.3373' // 소나큐브 스캐너 플러그인
id 'jacoco' // 테스트 커버리지(테스트 코드가 전체 소스의 몇 %를 덮고 있는지) 측정 도구
}
// 소나큐브 서버로 리포트를 쏠 때 필요한 설정값
sonar {
properties {
property "sonar.projectKey", "my-shopping-mall"
property "sonar.host.url", "http://localhost:9000" // 띄워둔 소나큐브 서버 주소
property "sonar.login", "sqp_어쩌구저쩌구토큰" // 인증 토큰
// Jacoco가 측정한 커버리지 결과 파일을 소나큐브가 읽을 수 있도록 경로 지정
property "sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacoco/test/jacocoTestReport.xml"
}
}
터미널에서 ./gradlew test jacocoTestReport sonar 명령어를 날리면, 단위 테스트가 쫙 돌고 커버리지가 측정된 뒤, 모든 소스코드와 결과가 소나큐브 서버로 업로드(스캔)됩니다.
3. 악독한 실무의 종착역: Quality Gate (품질 게이트)
단순히 "너 코드 좀 더럽다?" 하고 시각화만 해주는 거라면 개발자들은 절대 고치지 않습니다. 실무에서는 소나큐브를 젠킨스(Jenkins) 배포 파이프라인 한가운데에 물리적인 바리케이드(Quality Gate)로 박아버립니다.
팀장이 소나큐브 설정에서 "테스트 코드 커버리지가 80% 미만이거나, 심각(Critical) 버그가 1개라도 발견되면 무조건 FAILED(실패) 판정!"이라는 엄격한 룰(Quality Gate)을 세팅합니다. 이제 후임 개발자가 코드를 푸시하면 젠킨스가 빌드를 하다가 소나큐브에게 "야, 얘 코드 합격이냐?"라고 묻습니다. 만약 커버리지가 79%라서 FAILED가 떨어지면? 젠킨스는 배포 파이프라인을 즉시 중단시키고 빌드 실패 상태로 전환합니다. 개발자는 눈물을 머금고 테스트 코드를 마저 작성해서 80%를 넘겨야만 비로소 코드를 운영 서버로 내보낼 수 있습니다. 이 강제성이야말로 기술 부채를 막는 궁극의 방어막입니다.
4. 마무리 및 주요 발전 방향
지금까지 더러운 코드가 서버에 올라가는 것을 로봇처럼 감시하고, 테스트 코드 커버리지를 핑계로 배포를 차단해버리는 악명 높고도 위대한 코드 품질 수호자, SonarQube 정적 분석과 Quality Gate 아키텍처에 대해 상세히 다루어 보았습니다. 이제 여러분의 프로젝트 소스코드는 시간이 지날수록 부패하는 것이 아니라 맑고 영롱하게 유지될 것입니다.
자, 드디어 . 하지만 WebFlux의 리액티브 코드(Mono, Flux)는 너무나 기괴하고 학습 곡선이 가혹했습니다. "아니, 코드는 옛날처럼 직관적이고 편안한 동기식(Blocking)으로 짜면서, 성능만 WebFlux급으로 뽑아낼 수는 없을까?" 자바 진영이 10년의 연구 끝에 내놓은 '자바 생태계 최고의 혁명', 기존 OS 스레드의 한계를 획기적으로 극복하는 초경량 가상 쓰레드(Virtual Threads)가 드디어 Spring Boot 3.2에 공식 탑재되었습니다. "WebFlux를 관짝으로 보내버릴 차세대 고성능 동시성 처리의 결정판!" Java 21 Virtual Threads(가상 쓰레드)의 충격적인 원리와 실무 스프링 부트 적용기에 대해 아주 심도 있게 살펴보겠습니다.
우리는 앞서 에서 대규모 트래픽을 감당하기 위해 WebFlux(리액티브 프로그래밍)를 배웠습니다. 톰캣(Tomcat)의 쓰레드가 DB 응답을 기다리며 멍때리는(Blocking) 현상을 없애기 위해, 코드를 Mono와 Flux라는 상자로 감싸고 flatMap으로 떡칠하는 기괴한 비동기 코드를 짜야만 했습니다. 러닝 커브는 끔찍했고 레거시 JPA와의 궁합은 최악이었습니다. 백엔드 개발자들은 눈물을 흘리며 외쳤습니다. "아니, 코드는 옛날처럼 위에서 아래로 읽히는 평범한 동기식(Blocking)으로 짜면서, 멍때릴 때 쓰레드 낭비만 안 하게 해주는 마법은 없나요?!" 자바를 만든 오라클(Oracle)은 10년 넘게 진행된 프로젝트 룸(Project Loom)을 통해 마침내 해답을 내놓았습니다. "운영체제(OS)가 관리하는 무거운 쓰레드를 버리고, JVM이 깃털처럼 가볍게 수백만 개를 찍어내는 가상 쓰레드를 만들어줄게!"
Java 21 Virtual Threads(가상 쓰레드)의 미친 성능과 Spring Boot 3.2 적용 비법에 대하여 다뤄보겠습니다.
1. 플랫폼 쓰레드의 한계와 가상 쓰레드의 탄생
기존 자바의 쓰레드(Platform Thread)는 OS(윈도우/리눅스) 쓰레드와 1:1로 매핑되었습니다. OS 쓰레드는 하나 만들 때마다 1MB의 거대한 메모리를 퍼먹고, 쓰레드끼리 교대(Context Switching)할 때마다 OS 코어로 내려가서 허락을 맡아야 하는 극강의 무거운 작업이었습니다. 그래서 톰캣은 고작 200개밖에 쓰레드를 못 만들었습니다(Thread Pool).
Virtual Threads(가상 쓰레드)는 OS 몰래 자바 가상머신(JVM) 메모리(Heap) 위에서 그냥 일반 자바 객체(Object)처럼 수십 바이트(byte) 크기로 깃털처럼 가볍게 찍어내는 쓰레드입니다! 톰캣이 200개가 아니라 100만 개, 200만 개를 찍어내도 메모리가 전혀 터지지 않습니다.
놀라운 작동 원리: 1개의 진짜 OS 쓰레드(Carrier) 등에 가상 쓰레드 A가 업혀서 실행(Mount)됩니다. 그러다 A가 DB에서 데이터를 가져오느라 2초간 멈춥니다(Blocking). 옛날 같으면 Carrier 쓰레드도 같이 2초간 멍을 때렸지만, 이제는 JVM이 A를 등에서 뻥 차서 내려버리고(Unmount), 그 자리에 대기하던 가상 쓰레드 B를 잽싸게 업혀서 1초의 낭비도 없이 일을 계속합니다! 이것이 WebFlux의 난해한 코드 없이도 논블로킹(Non-blocking) 급의 미친 성능을 내는 비밀입니다.
2. Spring Boot 3.2에서 Virtual Threads 켜기
가장 충격적인 것은 도입 방법입니다. 코드를 WebFlux처럼 다 갈아엎을 필요가 1도 없습니다. 기존에 짜둔 @RestController, @Service 등 평범한 동기식 MVC 코드를 그대로 둔 채, 설정 파일 한 줄만 켜면 톰캣이 알아서 일반 쓰레드 대신 가상 쓰레드로 요청을 받아냅니다!
# application.yml (Spring Boot 3.2 이상 + Java 21 이상 필수)
spring:
threads:
virtual:
enabled: true # 단 한 줄! 마법이 시작됩니다.
이 한 줄을 켜고 JMeter()로 부하 테스트를 때려보면, 똑같이 200개의 쓰레드 풀을 쓰던 서버가 1만 명의 동시 접속자를 아무런 타임아웃 에러 없이, Mono/Flux 없이 기존 코드로 완벽하게 처리해 내는 기적을 눈으로 볼 수 있습니다!
3. 아직 넘어야 할 산: Pinning(피닝)의 저주
"완벽하네요! 당장 실무에 도입합시다!" 하지만 세상에 공짜는 없습니다. 아주 무서운 'Pinning(고정)' 현상이 도사리고 있습니다.
가상 쓰레드가 Carrier 쓰레드 등에 업혀서 작업하다가 DB 대기가 걸리면 내려와야(Unmount) 한다고 했습니다. 그런데 자바 코드 내에 synchronized 키워드가 발려진 블록을 통과할 때는, 가상 쓰레드가 Carrier 쓰레드 등에 딱 붙어서(Pinned) 떨어지질 못합니다! 이 상태에서 DB 대기가 걸리면 Carrier 쓰레드까지 꼼짝없이 같이 묶여버리며 서버가 순식간에 뻗어버립니다. 레거시 라이브러리 내부에는 아직 synchronized 코드가 많이 남아있기 때문에, Virtual Threads를 실무에 도입할 때는 내 프로젝트가 의존하는 수많은 라이브러리들이 synchronized를 ReentrantLock으로 잘 대체했는지 철저히 검증하고 모니터링해야 하는 숙제가 남아있습니다.
'Framework > Spring Boot' 카테고리의 다른 글
| [Spring Boot] Flyway 플라이웨이 데이터베이스 형상 관리 (0) | 2026.10.23 |
|---|---|
| [Spring Boot] WebRTC 화상 회의 P2P 시그널링 서버 (0) | 2026.10.22 |
| [GraphQL] 그래프QL REST API 오버패칭 (0) | 2026.10.22 |
| [Spring Boot] 낙관적 락Optimistic vs 비관적 락Pessimistic JPA 동시성 제어 (0) | 2026.10.21 |
| [Spring Boot] Spring Cloud Gateway (0) | 2026.10.21 |