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
을 명시해주었다. 그리고 메서드 내부에서 string
이 null
인 경우를 check 해주었다.
다음으로, getJSONObjectFromString
메서드의 return value로 null을 반환하지 않도록 했다.
이렇게 코드를 수정한 후 다시 빌드하면, NullAway 이슈가 발생하지 않고 빌드가 정상적으로 완료된다.
현재 개발 중인 Android SDK 를 빌드했을 때 NullAway 이슈가 생각보다 많이 나왔다!! null
체크를 많이 해뒀다고 생각했는데 아니었구나... 며칠 동안은 NullAway 이슈를 없애는 데 시간을 많이 할애해야 할 것 같다. 역시 null
은 "billion-dollar mistake" 가 맞구나 싶다.
Kotlin 쓰자.
NPE를 사전에 잡아주는 툴이 있군요! 하나 배워갑니다~~