Lint는 무엇인가?

static

개발을 하다 보면 문법적으로는 성공하는데, 실제로는 정상적으로 동작하지 않는 코드들이 분명히 생깁니다. 이런 문제들은 보통 테스트를 통해 잡게 되는데, 테스트는 결국 코드를 직접 실행시키는 과정이다 보니 비용이 들고, 자주 돌리기에는 부담이 되는 것도 사실입니다.

그래서 모든 문제를 항상 테스트까지 가져가기보다는, 코드만 보고도 "이건 위험하다"라고 판단할 수 있는 것들은 최대한 앞단에서 걸러내는 게 더 효율적입니다. 이 역할을 해주는 게 Lint 같은 정적 분석 도구입니다.

Lint는 코드를 실행하지 않고, 코드 자체를 분석해서 잠재적인 버그 가능성이나 실수하기 쉬운 코드 패턴들을 찾아내고, 개발자에게 미리 수정하라는 신호를 줍니다. 다만 정적 분석이다 보니, 실제로 값이 어떻게 들어오는지에 따라 달라지는 동작까지 검증해주지는 못합니다.

그래서 Lint는 테스트를 대체하는 도구는 아니고, 테스트 전에 "굳이 실행까지 해볼 필요도 없는 코드"를 먼저 정리해 주는 용도에 가깝습니다. 테스트는 동작을 검증하는 단계이고, Lint는 그전에 코드 품질과 잠재적인 버그 가능성을 관리하는 단계라고 보면 될 것 같습니다.

정적 분석 vs 동적 분석

Lint는 정적 분석을 하는 도구라고 알고 있습니다. 그렇다면, 동적 분석을 하는 도구도 있지 않을까요?
그전에, 용어부터 먼저 정리하고 가는 게 좋을 것 같습니다.

정적 분석이란, 코드를 실행하지 않고 소스 코드나 바이트 코드를 분석하는 것을 말합니다. 이 과정에서 잠재적인 버그 가능성, 위험한 패턴, 코드 스멜, 스타일 문제 같은 것들을 찾아냅니다.
다만, 실행 경로를 알 수 없고 런타임에 어떤 값이 들어오는지도 모르기 때문에, 실제 동작을 기준으로 한 검증은 할 수 없다는 치명적인 한계를 가지고 있습니다.

이 한계를 보완하기 위해 등장하는 것이 동적 분석입니다.

동적 분석은 코드를 실제로 실행하면서, 그 동작, 상태, 성능, 오류를 관찰하고 분석하는 방식입니다. 우리가 흔히 사용하는 테스트 역시 동적 분석의 한 종류라고 볼 수 있습니다.

동적 분석은 정적 분석이 찾아낸 "잠재적인 버그"가 아니라, 실제로 발생하는 버그를 찾아내는 데 초점이 맞춰져 있습니다. 또한 실행 중인 프로그램을 관찰하기 때문에, 성능 병목이 어디에서 발생하는지, 메모리 누수가 어디에서 생기는지 같은 것도 확인할 수 있습니다. 경우에 따라서는 동시성 문제나, 특정 타이밍에서만 발생하는 이슈 같은 것들도 재현하고 분석할 수 있습니다.

결국 정적 분석은 코드 자체의 위험 요소를 사전에 걸러내는 역할을 하고, 동적 분석은 실제로 실행되는 프로그램의 현실을 검증하는 역할을 한다고 보는 게 가장 정확한 구분인 것 같습니다.

용어는 대충 알았으니 본격적으로 정적 분석에 대해 알아보고 이것을 왜 사용하는지까지 생각해 봅시다.
더 나아가 배포는 어떻게 진행이 되는지 고민하면 좋을 거 같습니다.

어떻게 잠재적인 버그를 알 수 있을까?

정적 도구들은 "룰"을 통해 잠재적인 버그를 확인한다고 합니다.  
대표적인 룰 몇 개를 살펴보겠습니다.

1. Null 역참조 

Null일 수 있는 변수를 참조하면 위험하다. 다음과 같은 코드가 있다고 가정해 봅시다.

String s = null; 
if (condition) { 
	s = "hello"; 
} 
System.out.println(s.length()); // cond=false면 NPE

 

이렇게 되면 s는 null일 수도 있고 null이 아닐 수도 있습니다. 왜냐하면 condition이라는 값이 false인 경우 NPE가 발생하게 되죠.
정적 도구는 이러한 사실을 미리 경고를 하게 됩니다.

2. equals 대신 == 사용

객체 비교에 ==를 사용하면 대부분 버그라고 합니다. 다음과 같은 코드가 있다고 가정해 보면

String a = new String("hello"); 
String b = new String("hello"); 
if (a == b) 
  { ... }

a와 b는 객체입니다. 그렇기 때문에 두 개를 ==으로 비교하면 false로 되어 {...}를 계산이 불가해집니다. 

이미 "룰"로 정의된 위험한 코드 패턴들은 굉장히 많습니다.
정적 분석 도구는 이처럼 코드를 실행하지 않고도, 과거에 반복적으로 문제가 되었던 패턴들과 코드 구조를 비교해서,
이 코드는 나중에 버그로 터질 가능성이 높다. 는 신호를 개발자에게 미리 알려주는 역할을 합니다.

 그러면 정적 도구는 어디까지 신뢰할 수 있을까?

정적 분석 도구는 잠재적인 버그를 미리 알려주고, 개발자가 사전에 대비할 수 있도록 도와줍니다. 하지만 모든 경고가 실제 버그를 의미하는 것은 아닙니다. 경우에 따라서는 개발자가 의도적으로 그런 구조를 선택한 것일 수도 있고, 실제 실행 환경에서는 문제가 발생하지 않도록 이미 다른 장치가 마련되어 있을 수도 있습니다.

정적 분석 도구는 코드를 실행하지 않고, 가능한 경로를 보수적으로 추론해서 "버그가 발생할 수 있다"는 가능성을 경고하는 방식이기 때문에, 이런 경우 오탐(false positive) 이 발생하는 것은 피할 수 없습니다.

그래서 정적 분석 도구는 참고 자료이지, 절대적인 판결문은 아닙니다.
경고가 떴다고 해서 무조건 코드를 고쳐야 하는 것도 아니고, 반대로 경고가 없다고 해서 코드가 안전하다고 단정할 수도 없습니다.

결국 정적 분석 도구는 개발자의 판단을 보조해 주는 도구이지, 개발자를 대신해서 설계 판단까지 내려주는 도구는 아닙니다. 따라서 정적 분석을 맹신하기보다는, 경고의 의미를 이해하고 맥락에 맞게 받아들이는 태도가 중요합니다.

그렇다면, 룰을 직접 설정할 수도 있지 않을까?

어떤 코드가 어떤 환경에서는 버그일 수 있지만, 다른 환경에서는 의도된 설계일 수도 있습니다. 실제로 실무에서는 성능, 구조, 레거시 호환 같은 이유로 일부러 "일반적인 룰"을 깨는 코드도 존재합니다.

그렇다면 Lint 같은 정적 분석 도구에서는 이런 룰을 어떻게 다루고 있을까요?
결론부터 말하면, 대부분의 정적 분석 도구는 룰을 직접 설정하고, 켜고 끄고, 강도를 조절할 수 있게 되어 있습니다.

예를 들어 어떤 룰은 에러로 처리해서 빌드를 실패시키고, 어떤 룰은 경고만 띄우고 넘어가게 할 수도 있습니다.
그래서 룰은 비활성화도 할 수도 있고, 커스텀 룰도 직접 추가하는 것도 가능합니다.

즉, 정적 분석 도구는 정해진 정답을 강요하는 도구가 아니라,
우리 팀이 중요하게 생각하는 코드 품질 기준을 코드로 강제하는 도구에 가깝습니다.

그래서 실무에서는 보통 버그 가능성이 매우 높은 룰(NPE, 리소스 누수등)은 강하게 막고 스타일이나 구조 관련 룰은 팀 합의에 따라 완화하거나 끄고 레거시 코드가 많은 프로젝트에서는 점진적으로 룰을 적용하는 식으로 운영이 됩니다.

결국 중요한 건, 룰을 많이 켜는 게 아니라, 우리 팀과 프로젝트에 맞는 룰을 고르는 것입니다.
정적 분석 도구는 그 선택을 기술적으로 가능하게 해주는 수단일 뿐이지, 스스로 정답을 정해주는 존재는 아닙니다.

Java 진영에서는 하나의 Lint로 모든 걸 해결하기보다는, 실제 버그 패턴을 잡는 SpotBugs, 스타일을 통제하는 Checkstyle, 그리고 IDE에서 즉시 피드백을 주는 SonarLint를 역할별로 나눠서 사용합니다. 프로젝트 초기에는 버그 가능성이 높은 룰부터 적용하고, 점진적으로 범위를 넓히는 방식이 현실적입니다.

각각 어떻게 설치할 수 있을까?

java진영에서는 주로 다음과 같은 도구가 사용이 된다고 합니다.

도구 설치  위치목적
SonarLint IDE 플러그인 개발 중 실시간 피드백
SpotBugs Gradle 플러그인 빌드/CI에서 버그 차단
Checkstyle Gradle 플러그인 스타일 규칙 강제

그러면 sonarLint부터 하나씩 실습을 시작해 보겠습니다.

1) SonarLint

SonarLint는 IDE에서 개인이 관리하게 됩니다. 다만, SonarLint는 SonarQube와 연동하는 것도 가능합니다.
이렇게 연동하면, IDE에서도 팀이나 조직에서 정한 공통 룰셋을 그대로 받아서 분석할 수 있기 때문에, 로컬에서 작성할 때부터 CI에서 걸릴 문제를 미리 확인하는 구조로 만드는 것이 가능하다고 합니다.

플러그인에서 SonarQube For ide를 다운을 받았습니다.
그럼 다음과 같은 화면이 나오는데요. 시작 버튼을 누르면 코드 스멜을 확인하게 됩니다.

그렇다면 룰은 어떻게 설정할까요? 

설정에 들어가면 tools -> SonarLint or SonarQube For IDE -> Role을 선택하면 Role을 지정할 수 있다고 합니다.
아쉽게도 커스텀 룰을 만드는 기능은 제공하고 있지 않는다고 합니다. 그렇지만 sornarQube를 연동하게 되면 커스텀 룰을 지정하는 것이 가능하고 합니다.

2) SpotBugs

이거는 빌드 도중에 실행이 됩니다. 이 도구 같은 경우는 IDE에서 개발자가 무시하고 개발할지라도, 빌드 단계에서 다시 한번 코드를 검사해서, 문제가 있으면 아예 빌드를 실패시킬 수도 있는 도구입니다. 무엇보다 SpotBugs는 실제로 사고로 이어지는 버그 패턴을 잡는 데 훨씬 집중된 도구라는 점입니다. 예를 들어, NullPointerExcepion 가능성, 잘못된 equals/hashCode 구현, 리소스 누수, 동시성 관련 문제 등, 운영에서 터질 거 같은 것들을 위주로 검사합니다.
https://github.com/spotbugs/spotbugs?tab=readme-ov-file  

 

GitHub - spotbugs/spotbugs: SpotBugs is FindBugs' successor. A tool for static analysis to look for bugs in Java code.

SpotBugs is FindBugs' successor. A tool for static analysis to look for bugs in Java code. - spotbugs/spotbugs

github.com

그 레들 플러그인을 통해 등록이 됩니다.(메이븐도 있겠죠) 위 글은 spotBugs github글입니다.

id "com.github.spotbugs" version "6.4.8"

그러면 다음과 같이 실행할 수 있다고 합니다. 

 

./gradlew spotbugsMain

Main은 운영만 검사하는 경우에 사용하고
Test는 테스트코드를 검사하는 경우에 사용된다고 합니다.
check는 테스트 + SpotBugs(Main/Test) + 기타 품질 검사입니다.

 

 


그럼 실행해 보겠습니다.

문제가 있는 경우 실패하게 하기

spotbugs {
    ignoreFailures = false
}

검사 강도 조절

spotbugs { 
effort = "max" // 분석 강도 
reportLevel = "low" // 어느 수준부터 보고할지 
}

강도: "min" | "default" | "max"  약하게 -> 힘들게
어디까지 문제로 볼 것인가: "high" | "medium" | "low" 심각한 거부터 -> 사소한 거 까지

처음 도입할 때는 default / high로 필터링한다고 합니다.

필터링

spotbugs {
    excludeFilter = file("spotbugs-exclude.xml")
}

파일은 다음과 같이 만든다고 합니다.

<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>

    <!-- 특정 버그 타입 무시 -->
    <Match>
        <Bug pattern="NP_NULL_ON_SOME_PATH"/>
    </Match>

    <!-- 특정 클래스 전체 무시 -->
    <Match>
        <Class name="com.example.legacy.LegacyService"/>
    </Match>

    <!-- 특정 패키지 무시 -->
    <Match>
        <Package name="com.example.generated"/>
    </Match>

</FindBugsFilter>

커스텀 룰 같은 경우는 플러그인 프로젝트 하나 더 파서 JAR 만들어서 끼워 넣는 구조라고 합니다.
결과는 다음과 같이 보입니다.

확실히 high랑 low로 했을 때 버그개수가 다르군요.. 참고로 high로 했더니 깨끗합니다.

3) CheckStyle

마지막으로 CheckStyle입니다.
CheckStyle은 SpotBugs처럼 버그를 찾아주는 도구라기보다는, 코드 스타일과 컨벤션을 검사하는 도구입니다.
예를 들어, 들여 쓰기 규칙, 중괄호 위치, 클래스/메서드 네이밍 규칙, import 순서 같은 것들이 여기에 해당합니다.
즉, "이 코드가 위험한가?"를 보는 도구가 아니라, "이 코드가 팀 규칙에 맞게 쓰였는가?"를 보는 도구라고 보면 됩니다.
SpotBugs가 잠재적인 버그를 찾는 데 초점이 맞춰져 있다면, CheckStyle은 코드의 일관성과 가독성을 강제하는 데 초점이 맞춰져 있습니다. CheckStyle 역시 빌드 과정에서 실행되며, 규칙을 위반한 코드가 있으면 경고 또는 빌드 실패로 이어지게 됩니다.

 

GitHub - checkstyle/checkstyle: Checkstyle is a development tool to help programmers write Java code that adheres to a coding st

Checkstyle is a development tool to help programmers write Java code that adheres to a coding standard. By default it supports the Google Java Style Guide and Sun Code Conventions, but is highly co...

github.com

그럼 이 친구는 어떻게 사용할까요?

이거는 이렇게 사용할 수 있습니다.

apply plugin: "checkstyle"


checkstyle {
    toolVersion = "13.0.0"
    ignoreFailures = false
}
  •  

하지만 이렇게만 하면, 규칙으로 지정한 파일이 존재하지 않습니다.
이 방식은 규칙을 지정한 파일이 있어야 제대로 사용할 수 있습니다.

configFile = rootProject.file("config/checkstyle/checkstyle.xml")

루트프로젝트에 파일을 지정해서 추가를 할 수 있습니다. 여기서 의문의 드는 건 과연 이것도 gitignore로 지정해야 할까요? 이 파일 같은 경우, 팀의 합의해서 만든 코드 스타일입니다. 그렇기 때문에 반드시 커밋이 되어야 하는 파일입니다.

가장 쉬운 방법으로 구글 스타일을 가져와보겠습니다. 

 

checkstyle/src/main/resources/google_checks.xml at master · checkstyle/checkstyle

Checkstyle is a development tool to help programmers write Java code that adheres to a coding standard. By default it supports the Google Java Style Guide and Sun Code Conventions, but is highly co...

github.com

요렇게 다운을 받고 저 경로로 바꿔보겠습니다.

./gradlew checkstyleMain

이렇게 실행하게 되면

이것을 확인하면 구글 스타일과 제 코드와 비교했을 때 어떤 점이 다른지 알 수 있습니다.

그러면 IDE에서도 스타일을 등록할 수 있는 걸로 알고 있는데 이것도 lint일까요? 

넓은 의미로 보면 Lint의 한 종류라고 볼 수 있습니다. 실제로 IntelliJ 같은 IDE는 코드 스타일 검사, 포맷팅, 경고 표시 같은 기능들을 통해 개발자가 규칙을 어기지 않도록 도와줍니다.

다만, 이런 IDE 기반의 스타일 검사는 어디까지나 "개발자를 도와주는 보조 도구"에 가깝고, 빌드 과정에서 강제되는 CheckStyle 같은 도구와는 성격이 다릅니다. IDE 설정은 개인 환경에 의존하지만, CheckStyle은 빌드 단계에서 모든 개발자에게 동일한 규칙을 강제하기 때문입니다.

즉, IDE 스타일 검사는 "편의성 중심의 Lint"에 가깝고, CheckStyle이나 SpotBugs 같은 도구들은 "규칙을 강제하는 Lint"에 가깝다고 볼 수 있습니다.

Lint

  • 코드를 실행하지 않고 "문제가 될 수 있는 부분"과 "규칙 위반"을 미리 찾아주는 정적 분석 도구들의 총칭
  • 컴파일 에러는 아니지만, 나중에 버그가 되거나, 유지보수를 망치거나, 팀 규칙을 깨뜨릴 수 있는 코드들을 미리 알려주는 도구

마무리

기존에도 Lint는 정적 분석 도구라는 정도의 인식은 가지고 있었습니다. 그리고 그중에서도 SonarLint 계열만을 Lint라고 생각하고 있었습니다.

하지만 이번에 정리하면서 보니, Lint라는 개념은 생각보다 훨씬 범위가 넓었습니다. SonarLint처럼 IDE에서 즉각적인 피드백을 주는 도구도 있고, SpotBugs처럼 잠재적인 버그를 찾아주는 도구도 있으며, CheckStyle처럼 코드 스타일과 규칙을 강제하는 도구도 모두 넓은 의미에서는 Lint의 범주에 들어간다고 볼 수 있었습니다.

결국 Lint는 단일한 도구를 의미하는 것이 아니라, 코드를 실행하지 않고 정적인 관점에서 문제를 찾고, 품질을 유지하기 위한 여러 도구들의 묶음에 가까운 개념이라는 것을 정리할 수 있었습니다. 

댓글

Designed by JB FACTORY