[Android][Kotlin] Custom Lint

D.O·2023년 10월 16일
0

Custom Lint 도입 결정

일관된 디자인을 유지하고 유지보수성을 높이기 위해서 자주 사용되는 기본 Compose Materail API를 내 앱 디자인 가이드에 맞게 일명 커스텀 Mineme Desing System으로 매핑시켰다.

이렇게 "Mineme Design System"을 도입함으로써 앱 전체에서 일관된 스타일과 구성 요소를 사용할 수 있겠다고 생각했다.

이런 매핑 과정에서는 내가 실수로 기본 Compose Material API를 직접 호출하는 경우가 종종 생겼다.

물론 현재 Mineme 프로젝트는 나 혼자 개발 중이라 이러한 실수가 적게 발생할 수 있지만 만약 다른 사람이 내 코드에 컨트리뷰터로 합류하여 같이 작업을 하거나 프로젝트 규모가 커지면 이러한 부분을 빈번히 놓칠 것이라고 생각을 했다.

이런 실수를 방지하고, 코드의 품질을 일관되게 유지하기 위해 Custom Lint를 도입하기로 했다!!

목표는 Custom Lint를 사용하여, 코드 작성 시점에서 바로 기본 Compose Material API의 잘못된 사용을 감지하고 개발자에게 알려주어, 문제가 발생하기 전에 즉각적으로 수정할 수 있게 하는 것!

Lint란

Lint는 코드에서 잠재적인 문제점을 식별하도록 설계된 정적 분석 도구입니다.

식별하는 문제점으로는 버그, 성능 문제, 스타일 위반, 안정성 문제, 접근성 문제 등이 포함한다고 합니다.

정적 분석 도구(static analysis tool)란

프로그램 코드를 실행하지 않고 코드 자체를 분석하여 문제점, 버그, 스타일 위반, 잠재적 오류 등을 식별하는 도구를 의미한다.

정적 분석 도구는 프로그램을 실행하거나 테스트하지 않고도 코드의 구조와 내용을 분석하여 문제점을 찾는다는 점과 실행 시점에 발견될 수 있는 오류나 예외 상황을 미리 식별하여 개발 초기 단계에서 수정할 수 있도록 도와준다는 점에서 비용과 시간을 절약하면서 코드 품질을 향상시킬 수 있다는 장점이 있다.

실행하기

코드 상에 Lint가 나타나면 바로 수정할 수도 있지만 놓친 부분에 대해서는 수동으로 실행하여 한번에 볼 수 있다.

1. Android Stuido로 실행

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

2. 명령어로 실행

프로젝트의 루트 디렉터리에서 다음 명령어 중 하나를 입력하여 프로젝트의 lint 작업을 호출하는 Gradle 래퍼를 사용할 수 있습니다.

  • Windows:
    gradlew lint
  • Linux 또는 Mac:
    ./gradlew lint

만약 특정 buildType 또는 특정 Product Flavor에서의 lint를 실행하려면 lint 접두사 뒤로 카멜 케이스의 해당 flavor,type을 넣어준다

ex) ./gradlew lintDemoDebug

그러면 html의 보고서가 아래 위치에 저장된다.

/root/app/build/reports/lint-results-debug.html

Custom Lint

안드로이드 자체에서 제공하는 있는 기본 Lint 또한 강력하므로 크게 Custom Lint의 필요성을 못 느낄 수 있다. 하지만 나의 경우처럼 기존 Lint에서 다루지 않는 프로젝트 특성상의 문제를 감지하고자 할 때 이럴 때 CustomLint를 사용하면 된다.

1. Lint 라이브러리 모듈 생성

먼저 실제 린트 검사를 위해 필요한 내용들을 구현할 Lint모듈을 생성한다

그리고 해당 모듈의 build.gradle에는 아래의 lint 종속성을 추가한다.

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

Lint와 관련된 의존성은 모두 compileOnly로 선언하는데, 그 주된 이유는 compileOnly가 의존성을 컴파일 시점에서만 필요로 하며, 런타임에는 포함되지 않기 때문이다.

Lint는 앱의 코드를 정적으로 분석하는 도구로, 런타임에 작동하지 않습니다. 이 말은, Lint와 관련된 의존성이 실제로 앱이 실행될 때 필요하지 않다는 것을 의미한다.

compileOnly를 사용하면 이러한 불필요한 의존성들이 최종 APK나 앱 번들에 포함되지 않게 됩니다. 결과적으로, 앱의 전체 크기를 줄일 수 있게 된다.

따라서, 위와 같은 이유로 Lint 관련 의존성을 compileOnly로 선언하는 것이 바람직하다고 한다.

2. Detector 구현

Detector는 Lint에서 실제로 코드의 특정 부분을 검사하는 로직을 포함하는 클래스입니다.

class DesignSystemDetector : Detector(), Detector.UastScanner {

    /**
     * 검사하고자 하는 요소
     * UCallExpression: UAST에서 메서드 호출이나 함수 호출을 나타냄
     * UQualifiedReferenceExpression: UAST에서 한 객체나 클래스의 멤버에 대한 참조를 나타냄
     */
    override fun getApplicableUastTypes(): List<Class<out UElement>> {
        return listOf(
            UCallExpression::class.java,
            UQualifiedReferenceExpression::class.java,
        )
    }

    /**
     * UCallExpression와 UQualifiedReferenceExpression)에 대해 감지된 요소의 처리 방법을 정의
     */

    override fun createUastHandler(context: JavaContext): UElementHandler {
        return object : UElementHandler() {
            // 권장 이름 제안

            // UCallExpression 요소를 방문할 때마다 호출
            override fun visitCallExpression(node: UCallExpression) {
                val name = node.methodName ?: return
                val preferredName = METHOD_NAMES[name] ?: return
                reportIssue(context, node, name, preferredName)
            }

            // UQualifiedReferenceExpression 요소를 방문할 때마다 호출
            override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) {
                val name = node.receiver.asRenderString()
                val preferredName = RECEIVER_NAMES[name] ?: return
                reportIssue(context, node, name, preferredName)
            }
        }
    }

    companion object {

        // Issue 생성
        @JvmField
        val ISSUE: Issue = Issue.create(
            id = "DesignSystem",
            briefDescription = "Design system",
            explanation = "This check highlights calls in code that use Compose Material " +
                    "composables instead of equivalents from the Mineme design system " +
                    "module.",
            category = Category.CUSTOM_LINT_CHECKS,
            priority = 7,
            severity = Severity.ERROR,
            implementation = Implementation(
                DesignSystemDetector::class.java,
                Scope.JAVA_FILE_SCOPE,
            ),
        )

        // 권장 이름 Map
        val METHOD_NAMES = mapOf(
            "MaterialTheme" to "DoTheme",
            "Button" to "DoButton",
            "OutlinedButton" to "DoOutlinedButton",
            "TextButton" to "DoTextButton",
            "FilterChip" to "DoFilterChip",
            "ElevatedFilterChip" to "DoFilterChip",
            "NavigationBar" to "DoNavigationBar",
            "NavigationBarItem" to "DoNavigationBarItem",
            "NavigationRail" to "DoNavigationRail",
            "NavigationRailItem" to "DoNavigationRailItem",
            "TabRow" to "DoTabRow",
            "Tab" to "DoTab",
            "IconToggleButton" to "DoIconToggleButton",
            "FilledIconToggleButton" to "DoIconToggleButton",
            "FilledTonalIconToggleButton" to "DoIconToggleButton",
            "OutlinedIconToggleButton" to "DoIconToggleButton",
            "CenterAlignedTopAppBar" to "DoTopAppBar",
            "SmallTopAppBar" to "DoTopAppBar",
            "MediumTopAppBar" to "DoTopAppBar",
            "LargeTopAppBar" to "DoTopAppBar",
        )
        val RECEIVER_NAMES = mapOf(
            "Icons" to "DoIcons",
        )

        //  코드 내에서 문제점을 발견할 경우 Lint에 해당 이슈를 보고
        fun reportIssue(
            context: JavaContext,
            node: UElement,
            name: String,
            preferredName: String,
        ) {
            context.report(
                ISSUE,
                node,
                context.getLocation(node),
                "Using $name instead of $preferredName",
            )
        }
    }
}

Detector 클래스는 Android Lint에서 사용자 정의 규칙을 생성하기 위해 Detector 클래스를 상속받습니다.

이를 통해 개발자는 애플리케이션 코드의 특정 요소를 검사할 수 있습니다. UastScanner 인터페이스의 구현을 통해, Detector는 어떤 유형의 코드 요소를 검사할지 및 그 검사 방법을 지정합니다.

Uast는 Universal Abstract Syntax Tree의 약자로, 여러 프로그래밍 언어에 걸쳐 일관된 방식으로 구문 트리를 표현하는 데 사용됩니다.

Java와 Kotlin 둘 다에 적용되는 이유

Lint의 규칙을 작성하거나 적용할 때, 한 가지 궁금증이 생길 수 있습니다. 바로 Java와 Kotlin, 두 언어에 동일한 Lint 규칙이 어떻게 적용되는지에 대한 것입니다. 이는 UAST의 도입 덕분입니다.

UAST

UAST는 각 프로그래밍 언어의 AST (Abstract Syntax Tree)를 언어 중립적인 형태의 UAST로 변환하는 역할을 합니다.

Java와 Kotlin은 서로 다른 언어이지만, UAST는 이 두 언어의 코드 구문을 일관된 방식으로 해석하게 만듭니다. 예를 들면, Java의 메서드와 Kotlin의 함수는 UAST 내에서 같은 UMethod로 표현될 수 있습니다.

이렇게 UAST를 사용하면 Lint는 Java와 Kotlin 코드를 동일한 방식으로 처리하여, 언어에 중립적인 코드 검사를 수행할 수 있게 됩니다.

결과적으로, UAST의 도입 덕분에 Java와 Kotlin 양쪽에 동일하게 적용되는 Custom Lint 규칙을 효과적으로 작성하고 적용할 수 있습니다.

3. IssueRegistry 구현

IssueRegistry는 Lint에게 어떤 Issue들을 검사해야 하는지 알려주는 역할, Detector가 검출하려는 모든 이슈를 등록합니다.

/**
 *  Lint에게 어떤 이슈들을 검사해야 하는지 알려준다.
 */
class DesignSystemIssueRegistry : IssueRegistry() {

    // 필요에 따라 여러 이슈 포함 가능
    override val issues: List<Issue> = listOf(DesignSystemDetector.ISSUE)

    override val api: Int = CURRENT_API

    override val minApi: Int = 12

    override val vendor: Vendor = Vendor(
        vendorName = "Mineme",
        feedbackUrl = "https://github.com/NaJuDoRyeong/mineme_AOS_new/issues",
        contact = "https://github.com/NaJuDoRyeong/mineme_AOS_new",
    )
}

override val issues: List = listOf(DesignSystemDetector.ISSUE)

이 부분 list에 검사할 Issue들을 추가하면 됩니다.

여기서 vendor는 주로 Lint 결과 리포트나 Lint 사용자 인터페이스(UI)에서 Custom Lint 규칙에 대한 추가 정보나 피드백을 제공하는 링크 정보들을 제공합니다.

이런 식으로 리포터에 해당 Issue가 검출되면 여기에 Vendor에 정의했던 정보들이 보여집니다.

따라서 Vendor는 Lint 사용자에게 규칙 제공 업체에 대한 추가 정보와 연락 수단을 제공하는 역할을 한다고 생각하면 됩니다.

불필요하다고 생각되면 생략하시면 됩니다.

4. META-INF 등록

Lint 규칙을 JAR 파일로 패키징 할 때, 해당 JAR에는 META-INF/services/com.android.tools.lint.client.api.IssueRegistry라는 파일이 포함되어야 하는데

파일은 단순히 사용자의 IssueRegistry 구현의 완전한 클래스 이름을 포함 ,이렇게 함으로써 Lint는 실행 시 해당 파일을 찾아 사용자 정의 규칙을 로드합니다.

즉 , Lint가 Custom Lint를 어떻게 찾아야하는지를 알려줍니다.

5. Lint 실행

저는 demo flavor에 debug buildType에서 진행했습니다.

실행을 하면 어떤 부분에서 Lint에 걸렸는지 표시됩니다.
전체 report를 보고 싶다면 app 모듈의 build/report에 저장되어 있습니다.

이렇게 제가 지정한 Issue에 걸린 부분이 표시되게 됩니다.

그리고 기존에 오류 표시가 뜨지 않던 부분이 아래처럼 제가 report에 지정한대로 표시되게 됩니다.

마무리

Lint의 기본 개념 및 Custom Lint 작성 방법에 대해 살펴보았습니다.

많은 공동작업자가 참여하는 프로젝트나 대규모의 코드베이스에서는 Lint의 중요성이 더욱 두드러집니다.

필요에 따라 Custom Lint를 추가적으로 작성하고 Issue Registry에 등록하여 프로젝트의 품질을 높일 수 있습니다.

감사합니다! 😀

profile
Android Developer

0개의 댓글