널널(null null)하게 개발하지 마세요
언어마다 약간의 차이가 있지만, 자바에서는 존재하지 않는 객체 메모리 주소를 `null`이라 표현한다. 따라서 원시타입을 제외한 객체를 가리키는 모든 변수는, 초기에 null 상태라고 볼 수 있다.
null을 참조하려고 하면 자바는 NPE(Null Point Exception)라는 런타임 오류를 뱉어내는데 해결하기 쉬우면서도 어려운 오류다.
null safe하게 코드를 작성하려고 노력하지만, 항상 예상하지 못한 곳에서 NPE가 발생한다. 런타임 오류기 때문에 코드가 배포돼서 서비스되기 전까지는 모르기 때문에 치명적이다.
널 레퍼런스를 처음 만든 토니 호어가 '10억 불짜리 실수'(billion dollar mistake)였다고 회고한 적 있다. 널 포인터를 체크하지 않고 사용해서 발생하는 버그가 엄청나게 많기 때문. 때문에 최신 언어들은 null reference를 배제하려는 움직임을 보인다. - 나무위키
Null Safe 방법
null을 안전하게 다루는 여러 방법이 있지만, 내가 알고 있던 정보로는 `Optional`이 제일 트렌디하고 최선이라고 생각했다. 하지만 사용하다보니 생각보다 불편한 점이 있었고, 다른 곳에서는 어떻게 사용하는지 찾아보던 중 몰랐던 방법을 찾아 글로 정리한다.
class Box {
Message getMessage() {
if (somtimes()) {
return null;
}
return new Message();
}
}
class Message {
String message;
}
가끔씩 null을 반환하는 Box를 null safe 하게 만들 수 있을까?
if문 처리
가장 기본적인 방법으로, 직접 if문에서 null 예외처리 하는 것이다. 보통 라이브러리를 사용하거나 공통 함수를 사용해 처리한다.
void nullSafe(Box box) {
Message msg = box.getMessage();
if (msg == null) {
msg = new Message();
}
if (Objects.isNull(msg)) {
msg = new Message();
}
}
하지만 개발자들이 가끔씩(제법 자주) 예외처리하는 것을 까먹는다는 문제점이 있다.
Optional
Scala 언어의 영향을 받아 자바 8부터 도입된 Optional 객체를 활용하는 것이다. null일수도 있다는 것을 객체로 알려줄 뿐만 아니라, 값을 꺼내기 위해서 null 예외처리를 강제하도록 인터페이스가 설계됐다. or, orElse, orElseThrow, orElseGet,... 다양한 메서드를 지원한다.
class Box {
Optional<Message> getOptionalMessage() {
if (sometimes()) {
return Optional.empty();
}
return Optional.of(new Message());
}
}
void nullSafe(Box box) {
Message msg = box.getOptionalMessage().orElse(new Message());
}
자바에서 여러 방식으로 최적화를 하지만 Optional도 결국 객체기 때문에, 메모리를 차지한다는 성능 문제가 있다.
또한 특정 상황에서는 가독성이 떨어지고, Optional의 잘못된 사용으로 null 문제를 해결 못하고 있다는 주장이 있다.
물론 Optional은 잘못이 없다. 개발자들의 잘못된 사용이 위의 문제를 야기하는 것이다.
Optional의 올바른 사용법에 대해 여러 가지 논의가 있다.
@Nullable
`@Nullable` 어노테이션을 적극 활용해서 IDE 혹은 정적분석도구의 도움을 받는 것이다.
IDE 지원
Intellij의 경우, NPE가 발생할 수 있을만한 코드를 어노테이션 기반으로 분석하고 알려주는 옵션이 있다.
모든 곳에 어노테이션을 작성하기에는 번거롭고 가독성도 해치기 때문에, `package-info.java` 파일에 패키지레벨 어노테이션을 설정하는 것이 일반적이라고 한다. 만약 @Nonnull을 패키지레벨 어노테이션으로 설정했다면, @Nullable한 곳에서만 어노테이션을 override 하면 된다.
// package-info.java @Nonnull package com.example.nulltrend; import jakarta.annotation.Nonnull;
하지만 IDE에서 지원해 주는 것은 단순한 경고이기 때문에, 코드를 빌드하는 과정에서는 경고를 무시해도 전혀 문제가 없다. 따라서 만약 개발자가 IDE의 경고를 무시하고 mainline 소스에 통합한다면, 이는 여전히 null-safe 하지 못한 소스가 된다.
정적분석도구
정적분석도구를 활용하면 런타임오류를 빌드 오류로 변환시켜, 일종의 컴파일 오류로 쉽게 예방할 수 있다. nullable 정적 분석도구는 다양하지만 그중 uber에서 만든 `nullaway`를 적용시켜 봤다.
// build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.0'
id 'io.spring.dependency-management' version '1.1.0'
id "net.ltgt.errorprone" version "3.0.0"
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
errorprone "com.google.errorprone:error_prone_core:2.19.0"
annotationProcessor "com.uber.nullaway:nullaway:0.10.10"
}
tasks.named('test') {
useJUnitPlatform()
}
tasks.withType(JavaCompile).configureEach {
options.errorprone {
option("NullAway:AnnotatedPackages", "com.example")
}
}
tasks.compileJava {
options.errorprone.error("NullAway")
}
/Users/hyojip/Desktop/repo/NullTrend/src/main/java/com/example/nulltrend/NullTrendApplication.java:36: error: [NullAway] returning @Nullable expression from method with @NonNull return type
return null;
^
(see http://t.uber.com/nullaway )
위의 #getMessage 함수에서 null을 리턴하던 것을 정적분석도구가 발견해 컴파일에 실패했다.
@Nullable
Message getMessage() {
if (sometimes()) {
return null;
}
return new Message();
}
@Nullable 어노테이션을 추가한 뒤, 컴파일하면 아래와 같이 빌드에 성공하는 것을 볼 수 있다.
> Task :compileJava
> Task :processResources UP-TO-DATE
> Task :classes
> Task :resolveMainClassName
> Task :bootJar
> Task :jar
> Task :assemble
> Task :compileTestJava UP-TO-DATE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test
> Task :check
> Task :build
BUILD SUCCESSFUL in 2s
7 actionable tasks: 5 executed, 2 up-to-date
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
3:13:04: 실행이 완료되었습니다 'build'.
'Backend > Java' 카테고리의 다른 글
Spring Actuator로 Property 변경하기 (0) | 2023.08.29 |
---|---|
오라클 - 마이바티스 날짜형 데이터 맵핑 (0) | 2023.07.12 |
공통 lib -> spring-boot-starter로 바꾸기(3) (0) | 2023.04.23 |
Stream에 Decorator 패턴 써먹기 (2) | 2023.04.03 |
공통 lib -> spring-boot-starter로 바꾸기(2) (0) | 2023.03.20 |