[Android/Java] Android Library 모듈에 Uber NullAway 적용하기

구근우·2021년 10월 23일
4

Android

목록 보기
3/3
post-thumbnail

NullAway 도입 배경

Android SDK는 고객사의 앱이 사용하게 된다. SDK의 NullPointerException(NPE)에 의해 고객사의 앱이 크래시로 종료되는 상황은 최대한 일어나지 않아야 한다. Kotlin은 Null-Safety 한 언어이기 때문에 Java 보다는 NPE로 고생할 일이 적다. 하지만 현재 회사에서는 Java로 Android SDK 를 개발하고 있다. 물론 Java에서도 @Nullable / @NonNull annotation을 이용해 null 핸들링을 할 수 있다. 하지만 개발 초기부터 annotation을 도입하지 않았고, 지금은 시간이 꽤 많이 흘러버렸다... 하나하나 annotation을 추가하는 것은 거의 불가능에 가까워졌기 때문에 자동으로 NPE가 발생할 수 있는 부분을 찾아주는 툴을 찾아보았다. 그러다 Uber에서 개발한 NullAway를 알게 되었다.
NullAway는 코드를 빌드할 때마다 자동으로 실행되기 때문에 즉시 NPE를 방지할 수 있을 것 같았다. 그리고 NullAway 실행의 build-time overhead는 일반적으로 빌드 시간 전체의 10% 미만이라고 하는 것도 장점으로 느껴졌다.

설치

NullAway는 Error Prone의 플러그인으로 만들어졌기 때문에 Error Prone 설치가 선행되어야 한다.
먼저 gradle에 Error Prone 을 설치하기 위해 gradle-errorprone-plugin을 설치한다.
rootProject의 build.gradle에 아래와 같이 plugin을 추가한다.

//rootProject/build.gradle

plugins {
    id("net.ltgt.errorprone") version "2.0.2"
}

이 plugin은 errorprone 이라는 configuration을 생성한다.
그래서 errorprone "com.google.errorprone:error_prone_core:2.4.0"을 이용해 Error Prone 을 dependency로 추가할 수 있다.
그런데 com.google.errorprone:error_prone_core:2.4.0 이 mavenCentral에 배포되어 있기 때문에, 먼저 rootProject 수준 build.gradle의 repositories에 mavenCentral을 추가해준다.

//rootProject/build.gradle

buildscript {
  repositories {
      mavenCentral()
  }
}

그리고 library 모듈 수준 build.gradle로 가서, errorprone configuration을 이용해 com.google.errorprone:error_prone_core:2.4.0 을 추가한다. 그리고 NullAway까지 같이 추가하면 아래와 같아진다.

//libraryModule/build.gradle

dependencies {
    errorprone "com.google.errorprone:error_prone_core:2.4.0"
    annotationProcessor "com.uber.nullaway:nullaway:0.9.1"
}

import net.ltgt.gradle.errorprone.CheckSeverity

tasks.withType(JavaCompile) {
  // remove the if condition if you want to run NullAway on test code
  if (!name.toLowerCase().contains("test")) {
    options.errorprone {
      check("NullAway", CheckSeverity.ERROR)
      option("NullAway:AnnotatedPackages", "your.package.name")
    }
  }
}

uber/NullAway 의 Android dependencies 예시를 보면 errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") 까지 추가되어 있다.
Error Prone은 최소 JDK 9 compiler 이상을 요구하지만, 만약 JDK 8을 사용하는 경우에 추가하는 옵션이 errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") 이다. 따라서 본인의 JDK 버전이 JDK 9 이상이라면 굳이 errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") 을 추가할 필요는 없다.

그 다음으로, tasks.withType(JavaCompile) 부분을 보면 NullAway에 옵션을 설정할 수 있다.
먼저 check("NullAway", CheckSeverity.ERROR) 은 NullAway 이슈를 error 레벨로 설정한다. (기본적으로 NullAway 이슈는 warning 레벨로 설정되어 있다.)
그리고 option("NullAway:AnnotatedPackages", "your.package.name") 은 NullAway가your.package.name 패키지 아래 항목만 검사하도록 하는 옵션이다. 그래서 검사 대상 패키지 명을 your.package.name 에 대입하면 된다. 추가적인 옵션들은 여기에 정리되어 있다.

사용법

NullAway를 사용할 때는 null 일 수 있는 field나, parameter, 그리고 return value 에 @Nullable annotation을 붙여주어야 한다. NullAway는 기본적으로 모든 field나, parameter, return value 가 null 이 아니라고 판단하기 때문에, @Nullable 이 붙지 않은 곳에 null 이 할당되면 NullAway 이슈가 발생한다.
그리고 아까 위에서 check("NullAway", CheckSeverity.ERROR) 옵션을 설정했기 때문에 NullAway 이슈가 발생하면 빌드 에러로 처리된다. 따라서 빌드 시 NullAway 이슈가 발생하면 빌드가 자동으로 중단되버린다. 모든 NullAway 이슈를 없애면 빌드가 정상적으로 마무리된다. 그럼 이제 예시 코드를 통해 사용법을 익혀보자.

예시

아래 예시 코드를 빌드해보자.

public static JSONObject getJSONObjectFromString(String string) {
    try {
        return new JSONObject(string);
    } catch (JSONException e) {
        return null;
    }
}

public static void test() {
    JSONObject jsonObject = getJSONObjectFromString(null);
}

빌드 과정에서 NullAway가 실행되었고, 두 가지 에러가 발생했다.

error: [NullAway] returning @Nullable expression from method with @NonNull return type
            return null;
            ^
            
error: [NullAway] passing @Nullable parameter 'null' where @NonNull is required
        JSONObject jsonObject = getJSONObjectFromString(null);
                                                        ^

첫 번째 에러는 getJSONObjectFromString 메서드의 return value가 @Nullable 로 표기되지 않았는데 catch 문에서 null이 반환되기 때문에 발생한 것이다.
두 번째 에러는 test 메서드에서 getJSONObjectFromString 메서드를 사용할 때 @Nullable이 명시되지 않은 string 파라미터 자리에 null을 넣었기 때문에 발생했다.

위 에러는 아래와 같이 코드를 수정해주면 해결된다.

public static JSONObject getJSONObjectFromString(@Nullable String string) {
    if (string == null) {
        return new JSONObject();
    }
    try {
        return new JSONObject(string);
    } catch (JSONException e) {
        return new JSONObject();
    }
}

public static void test() {
    JSONObject jsonObject = getJSONObjectFromString(null);
}

getJSONObjectFromString 메서드의 string 파라미터에 null이 들어갈 수 있기 때문에 @Nullable 을 명시해주었다. 그리고 메서드 내부에서 stringnull인 경우를 check 해주었다.
다음으로, getJSONObjectFromString 메서드의 return value로 null을 반환하지 않도록 했다.
이렇게 코드를 수정한 후 다시 빌드하면, NullAway 이슈가 발생하지 않고 빌드가 정상적으로 완료된다.

사용 후기

실제 빌드 후 에러 메시지

현재 개발 중인 Android SDK 를 빌드했을 때 NullAway 이슈가 생각보다 많이 나왔다!! null 체크를 많이 해뒀다고 생각했는데 아니었구나... 며칠 동안은 NullAway 이슈를 없애는 데 시간을 많이 할애해야 할 것 같다. 역시 null은 "billion-dollar mistake" 가 맞구나 싶다.

결론

Kotlin 쓰자.

참고

profile
Android SDK Developer

2개의 댓글

comment-user-thumbnail
2021년 10월 24일

NPE를 사전에 잡아주는 툴이 있군요! 하나 배워갑니다~~

답글 달기
comment-user-thumbnail
2021년 10월 31일

좋은 자료 감사합니다!!

답글 달기