코딩 컨벤션 강제하기 (feat. Ktlint, Detekt)

이제일·2023년 8월 11일
0

Android

목록 보기
15/15

Android Lint

Android Studio 는 코드의 구조적 품질 문제를 식별하고 수정하는 데 도움이 되는
코드 스캔 도구를 제공한다.

위 사진처럼 Toast 사용 시 show()를 실행하지 않으면 기본 알림창이 뜨는 것을 볼 수 있다.

검사 대상은 Java, Kotlin 및 XML 파일, 아이콘 및 ProGuard 구성 파일을 포함해서
Android 프로젝트를 구성하는 파일이다.

실행하기

코드 상에 Lint가 나타나면 바로 수정할 수도 있지만, 나중에 하겠다고 쌓아놓으면 찾기 힘들다
그래서 수동으로 실행하여 한번에 볼 수 있다.

1. Android Studio로 실행
Android Studio 메뉴 중 code -> Inpect Code 를 실행하면 아래처럼 결과를 볼 수 있다

2. 명령어로 실행
터미널에서 아래 명령어를 실행하면 된다

# Windows
./gradlew lint

#Linux 또는 macOS
./gradlew lint

buildType 중 특정 build variant 의 lint 를 실행하려면 lint 접두사 뒤로 카멜 케이스의 해당 type 을 넣어준다.
./gradlew lintRelease

그러면 html의 보고서가 아래 위치에 저장된다.
root/app/build/reports/lint-results-debug.html

Custom Lint

안드로이드 자체에서 제공하고 있는 기본 lint가 강력하므로 Custom Lint의 필요성은 크게 없을 수 있다.
하지만 다수의 인원이 작업할 때 공통으로 지켜야 하는 룰 중에서 강제화할 필요가 있는 것들에 대해서는 Custom Lint를 사용하면 좋은 경우도 있다.

1. Lint 라이브러리 모듈 생성
build.gradle에는 아래의 lint 종속성을 추가한다.

dependencies {
	compileOnly "com.android.tools.lint:lint-api:$lint_version"
	compileOnly "com.android.tools.lint:lint-checks:$lint_version"
}

Lint Version은 Gradle Plugin Version에 23.0.0을 더해준 것으로 사용하면 된다.

2. IssueRegistry 구현

class LintIssueRegistry : IssueRegistry() {

    // lint api 버전 설정
    override val api = CURRENT_API

    // 검사를 수행 할 이슈 등록
    override val issues = listOf()

}

3. META-INF 등록
service registration file을 등록해줘야 한다.
IssueRegistry 클래스에 대한 이름으로 생성해주고, 프로젝트에서 이 class 파일을 생성할 위치를 명시해주면 된다.
resources/META-INF/services 아래에 com.android.tools.lint.client.api.IssueRegistry 파일 생성 후 앞서 구현한 클래스의 위치 기재

4. Detector 구현
수행 할 검사의 이슈를 만드는 클래스로 Detector 를 상속받아 구현한다

  • 추가로 다음의 인터페이스를 필요에 따라 가져온다
    XmlScanner : xml 파일을 검사
    SourceCodeScanner : 소스코드 파일을 검사
    override 할 메서드로는 다음과 같다.
  • getApplicableMethodNames : 반환된 리스트의 메소드명을 소스코드에서 검출
    visitMethodCall : getApplicableMethodNames 에서 반환된 메서드의 이름이 검출되면 호출됨

5. 이슈 등록
이전에 만들어 놨던 IssueRegistry 구현체에 Detector에서 생성하는 이슈를 등록한다.

6. lint 모듈 적용

dependencies {
    lintChecks project(':lint')
}

// settings.gradle
include ':lint'

Ktlint

Ktlint는 Kotlin 코딩 규칙 및 Android Kotlin 스타일 가이드를 체크해주는,
코틀린 프로젝트에서 가장 흔히 쓰이는 린터이다.

해당 포스팅에서는 Gradle plugin 방식으로 진행을 하고,
ktlint를 래핑한 Gradle plugin 중 가장 대중적으로 쓰이는 jlleitschuh/ktlint-gradle를 사용한다.

설정

먼저 프로젝트 및 모듈 단위의 build.gradle에 종속성을 추가한다

plugins {
    id("org.jlleitschuh.gradle.ktlint") version "10.2.1"
}

ktlint는 이후 별다른 설정이 없어도 디폴트 규칙으로 실행될 수 있지만,
.editorconfig 파일을 설정해 명시적으로 선언하는 것을 다음의 이유로 권장한다.

  • 상위 디렉토리의 .editorconfig 가 참조되는 경우를 막음
    root = true 라는 선언이 된 .editorconfig 파일을 찾기 전까지는 모든 상위 디렉토리를 탐색한다.
  • ktlint가 버전이 올라가면서 규칙의 디폴트 값이 변경될 수 있음
  • 다양한 IDE와 Editor등의 도구에서 이 설정을 참조하기 때문
    IntelliJ, VSCode, GitHub등에서 뷰어,포멧터의 설정으로 자동 반영
  • Kotlin 공식 코딩 컨벤션에 명시되지 않아도 추가로 규칙 설정 가능

.editorconfig 파일 설정
.editorconfig IntelliJ IDEA 외에, VS Code 등의 다른 코드 편집기 및 IDE에서도 동작하는 설정 파일이다.

파일 경로 및 패턴 설정은 다음과 같다

설정 명설명
*경로 구분 기호 / 를 제외한 모든 문자열과 일치
**모든 문자열과 일치
?단일 문자와 일치
[name]이름 문자열과 일치
[!name]이름에 없는 문자열과 일치
{s1,s2,s3}주어진 문자열과 일치 (쉼표로 구분) (EditorConfig Core 0.11.0부터 사용 가능)
{num1..num2}num1 과 num2 사이의 모든 정수와 일치 (num1과 num2는 양수 또는 음수)

섹션 안에 들어갈 수 있는 규칙은 다음과 같다

설정 명설명
ktlint_code_style"ktlint_official" or "android" 로 코드 스타일 설정
insert_final_newline"true" or "false"로 줄바꿈으로 끝나도록 하는지 설정
indent_style"space" or "tab"으로 들여쓰기 스타일 설정
indent_size각 들여쓰기 수준에 사용되는 자리 수
end_of_line"lf" or "cr" or "crlf" 로 설정하여 줄바꿈 표시 방법 제어
charset"latin1" or "utf-8" or "utf-8-bom" or "utf-16be" or "utf-16le"로 설정하여 문자 집합 제어
trim_trailing_whitespace"true" or "false"로 줄 바꿈 문자 앞에 있는 공백 문자를 제거 여부
max_line_length"off" or 양수로 설정하여 최대 줄 길이 설정

구성 예시

root = true

[*]
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.{kt, kts}]
insert_final_newline = false

실행

  • 스타일 체크 ./gradlew ktlintCheck

해당 태스크는 스타일 검사를 수행한다.

이 태스크는 ./gradlew build 를 실행했을 때 연결되는 전체 프로젝트 빌드 싸이클에 포함되어서 정의한 코드 스타일을 지키지 않으면 빌드가 실패한다.

  • 스타일 변환 ./gradlew ktlintFormat

해당 태스크는 스타일에 맞지 않는 코드를 일괄적으로 바꿔준다
전체 프로젝트의 소스 코드를 한꺼번에 고칠 때 사용할 수 있습니다.

해당 기능을 사용하다가 의도하지 않게 파일이 삭제되는 경우도 있었다고 하니 조심해서 사용해야한다.



Git 연동

  • ktlintCheck 태스크를 pre-commit hook으로 등록
    ./gradlew addKtlintCheckGitPreCommitHook

  • ktlintFormat 태스크를 pre-commit hook으로 등록
    ./gradlew addKtlintFormatGitPreCommitHook


Detekt

detekt 는 코드 포멧팅 보다는 코드 복잡성, code smell 탐색과 같은 코드 분석에 초점을 둔 도구

설정

Ktlint와 마찬가지로 프로젝트 및 모듈 단위의 build.gradle에 종속성을 추가한다
버전 호환은 다음을 참고

plugins {
    id("io.gitlab.arturbosch.detekt") version 1.23.1
}

프로젝트 레벨의 gradle에 다음의 예시처럼 detekt 옵션 설정

detekt {
    config.setFrom("${projectDir}/detekt-config.yml")
    buildUponDefaultConfig = true
    debug = true
}

이 외 다양한 옵션 설정

detekt config로 사용할 파일 구성 예시
앞선 예시에서 설정한 파일이름 detekt-config.yml로 생성

build:
    maxIssues: 0

config:
    warningsAsErrors: true
    
complexity:
  	active: true
    TooManyFunctions:
        thresholdInFiles: 30
        thresholdInClasses: 20
        ignorePrivate: true
        
output-reports:
	exclude:
    	- 'TxtOutputReport'
        - 'XmlOutputReport'
        - 'MdOutputReport'

default로 정의된 설정 파일과 설명한 옵션 이외의 내용은 공식 페이지 참조

  • build/maxIssue : 설정된 개수 이상의 문제가 발생되면 더 이상 분석을 하지 않고 빌드를 캔슬한다.
  • config/warningsAsErrors : 경고도 전부 에러로 간주하여 빌드를 실패시킬지 여부
  • complexity/TooManyFunctions : 함수 개수에 대한 코드 복잡도 분석 룰 설정
    • thresholdInFiles : 한 파일에 몇 개까지의 함수를 허용할지 결정
    • thresholdInClasses : 클래스에 몇 개까지의 함수를 허용할지 결정
    • ignorePrivate : Priavte 접근 제어자가 설정된 함수를 카운트에서 제외할지 결정
    • output-reports/exclude : 생성하지 않을 리포트 파일의 포맷 설정

Report 파일 하나로 합치기
XML과 SARIF 포맷에 한해 Report 파일을 합치는 기능이 있다. 공식 문서

gradle 파일에 설정

val reportMerge by tasks.registering(io.gitlab.arturbosch.detekt.report.ReportMergeTask::class) { 
  output.set(rootProject.layout.buildDirectory.file("reports/detekt/merge.xml")) // or "reports/detekt/merge.sarif"
}

subprojects {
  detekt {
    // reports.xml.required.set(true)
    reports.sarif.required.set(true)
  }

  tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
    finalizedBy(reportMerge)
  }

  reportMerge {
    input.from(tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().map { it.xmlReportFile }) // or .sarifReportFile
  }
}


Git 연동

git hook 설정 스크립트

#!/usr/bin/env bash
echo "Running detekt check..."
OUTPUT="/tmp/detekt-$(date +%s)"
./gradlew detekt > $OUTPUT
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
  cat $OUTPUT
  rm $OUTPUT
  echo "***********************************************"
  echo "                 detekt failed                 "
  echo " Please fix the above issues before committing "
  echo "***********************************************"
  exit $EXIT_CODE
fi
rm $OUTPUT

git action 설정

jobs:
  without-type-resolution:
    runs-on: ubuntu-latest
    env:
      GRADLE_OPTS: -Dorg.gradle.daemon=false
    steps:
      - name: Checkout Repo
        uses: actions/checkout@v3

      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          java-version: 11

      - name: Run detekt
        run: ./gradlew detekt

      # Make sure we always run this upload task,
      # because the previous step may fail if there are findings.
      - name: Upload SARIF to GitHub using the upload-sarif action
        uses: github/codeql-action/upload-sarif@v2
        if: success() || failure()
        with:
          sarif_file: build/reports/detekt/detekt.sarif

각 Detekt Gradle task에서 Detekt.basePath를 설정해야 GitHub가 저장소가 주석을 올바르게 배치할 위치를 알 수 있다.
basePath = rootProject.projectDir.absolutePath

ktlint vs detekt vs spotless

  • Klint
    코드 스타일 검사에 초점
    공식 가이드에 기반하여 간단하게 구성

  • detekt
    코드 분석에 초점
    code small도 확인하고 많은 구성과 설정을 가지고 있다
    복잡할 수 있지만 사용자 지정 검사가 필요한 경우 좋은 옵션

  • spotless
    코드 스타일 검사에 초점
    ktlint와 통합하여 사용하기 좋고, 간단한 설정


참고 레퍼런스

Android Lint 공식 문서
Android Custom Lint
Ktlint configuration
Detekt 공식 문서

profile
세상 제일 이제일

0개의 댓글