ktlint를 붙이면 임포트 순서나 들여쓰기 같은 포맷 위반은 걸러지지만, catch 블록을 비워두거나 매직 넘버를 그대로 박아넣는 것 같은 로직상의 문제까지는 잡아주지 않습니다. 포맷은 멀쩡한데 코드 리뷰에서 매번 같은 지적이 반복되는 경우가 있어서, 정적 분석 도구인 Detekt를 붙여봤습니다.

왜 필요한가

리뷰에서 자주 나왔던 패턴 중 하나가 이런 코드였습니다.

fun syncItems() {
    try {
        repository.sync()
    } catch (e: Exception) {
    }
}

네트워크 예외를 잡아만 두고 아무 처리도 하지 않아서, 동기화가 조용히 실패해도 아무 로그도 남지 않았습니다. 컴파일은 되고 ktlint도 통과하기 때문에 CI에서는 걸리지 않고, 리뷰어가 매번 눈으로 찾아야 했습니다. 이런 종류의 문제(빈 catch 블록, 사용하지 않는 변수, 지나치게 깊은 중첩, 매직 넘버)는 룰 기반으로 기계적으로 찾아낼 수 있는 영역이라 정적 분석 도구로 옮기는 게 맞다고 판단했습니다.

핵심 개념

Detekt는 Kotlin 전용 정적 분석 도구로, complexity, style, potential-bugs, coroutines, naming 같은 룰셋 단위로 코드를 검사합니다. 각 룰은 detekt.yml 설정 파일에서 개별적으로 켜고 끌 수 있고, 심각도나 임계값(예: 함수당 허용 줄 수)도 조정할 수 있습니다.

기존 코드베이스에 처음 도입하면 수백 건씩 위반이 쏟아지는 경우가 많은데, 이때 쓰는 게 baseline 기능입니다. 현재 시점의 위반 목록을 스냅샷으로 떠서 detekt-baseline.xml에 저장해두면, 그 목록에 있는 위반은 무시하고 baseline 이후에 새로 생긴 위반만 CI에서 실패로 처리합니다. 즉 기존 코드는 건드리지 않고 새로 작성하는 코드부터 규칙을 강제하는 식으로 점진적으로 도입할 수 있습니다.

실전 예시

Gradle 플러그인 설정은 이렇습니다.

// build.gradle.kts
plugins {
    id("io.gitlab.arturbosch.detekt") version "1.23.7"
}

detekt {
    buildUponDefaultConfig = true
    config.setFrom(files("$projectDir/config/detekt.yml"))
    baseline = file("$projectDir/config/detekt-baseline.xml")
}

설정 파일에서 필요한 룰만 조정합니다.

# config/detekt.yml
complexity:
  LongMethod:
    threshold: 40
  NestedBlockDepth:
    threshold: 4

style:
  MagicNumber:
    ignoreNumbers: ['-1', '0', '1', '2']
    ignoreAnnotated: ['Preview']

potential-bugs:
  SwallowedException:
    active: true
  EmptyCatchBlock:
    active: true

./gradlew detektBaseline으로 baseline을 먼저 생성해두고, 이후에는 ./gradlew detekt를 CI에 붙입니다. 앞서 나왔던 빈 catch 블록은 이렇게 잡힙니다.

> Task :composeApp:detekt FAILED
SyncManager.kt:7:5: The catch block is empty [EmptyCatchBlock]
	at SyncManager.kt:7:5

수정은 예외를 로깅하거나 실패 상태로 변환하는 식으로 처리합니다.

fun syncItems() {
    try {
        repository.sync()
    } catch (e: Exception) {
        logger.error("sync failed", e)
        _syncState.value = SyncState.Error(e)
    }
}

기존 코드 중에 당장 고치기 애매한 위반은 baseline에 남겨두거나, 의도한 동작이 맞다면 어노테이션으로 해당 지점만 예외 처리합니다.

@Suppress("EmptyCatchBlock")
fun ignoreCleanupError() {
    try {
        tempFile.delete()
    } catch (e: Exception) {
        // 임시 파일 삭제 실패는 무시해도 되는 경우
    }
}

장단점 정리

장점

  • 코드 리뷰에서 반복적으로 지적되던 패턴(빈 catch, 매직 넘버, 과도한 중첩)을 CI 단계에서 기계적으로 걸러낼 수 있음
  • baseline으로 기존 코드는 건드리지 않고 새 코드부터 점진적으로 규칙을 강제할 수 있어서, 레거시가 많은 코드베이스에도 도입 부담이 적음
  • 룰셋 단위로 세밀하게 켜고 끌 수 있어서, 팀 컨벤션에 맞지 않는 룰만 골라서 비활성화하면 됨

단점

  • 초기 도입 시 룰 임계값(LongMethod, MagicNumber 등)이 기존 코드 스타일과 안 맞으면 오탐이 많아서, 몇 차례 설정을 튜닝하는 과정이 필요함
  • ktlint와 검사 영역이 일부 겹치는 룰(style 카테고리 일부)이 있어서, 두 도구를 같이 쓰면 어느 쪽 설정을 기준으로 삼을지 정리해둬야 함
  • 빌드 파이프라인에 검사 단계가 하나 더 늘어나는 만큼 CI 실행 시간도 그만큼 늘어남

ktlint로 포맷은 이미 정리되고 있는데 리뷰에서 로직상의 문제가 반복적으로 지적된다면, baseline으로 기존 코드 소음을 걷어내고 새 코드부터 적용하는 방식으로 도입 부담을 낮출 수 있습니다.