자바 에서 null 을 안전히 다루는 방법

null 체크

Java

public boolean startsWithA1(String str) {
    if (str == null) {
        throw new IllegalArgumentException("null이 들어왔습니다.");
    }
    return str.startsWith("A");
}

public Boolean startsWithA2(String str) {
    if (str == null) {
        return null;
    }
    return str.startsWith("A");
}

public boolean startsWithA3(String str) {
    if (str == null) {
        return false;
    }
    return str.startsWith("A");
}

Kotlin

fun startsWithA1(str: String?): Boolean {
    if (str == null) {
        throw IllegalArgumentException("null이 들어왔습니다.")
    }
    return str.startsWith("A")
}

fun startsWithA2(str: String?): Boolean? {
    if (str == null) {
        return null
    }
    return str.startsWith("A")
}

fun startsWithA3(str: String?): Boolean {
    if (str == null) {
        return false
    }
    return str.startsWith("A")
}

fun startsWithA4(str: String): Boolean {
    return str.startsWith("A")
}
  • 코틀린에서 null이 들어갈 수 있는 타입은 완전히 다르게 간주된다.
  • 한번 null 검사를 하면 non-null임을 컴파일러가 알 수 있다.

SafeCall과 Elvis 연산자

Kotlin

fun startsWithA1(str: String?): Boolean {
    return str?.startsWith("A")
            ?: throw IllegalArgumentException("null이 들어왔습니다.")
}

fun startsWithA2(str: String?): Boolean? {
    return str?.startsWith("A")
}

fun startsWithA3(str: String?): Boolean {
    return str?.startsWith("A") ?: false
}
  • Safe Call: str?.length 형태로 사용한다.
    • null이 아니면 실행하고, null이면 실행하지 않고 null을 반환한다.
  • Elvis 연산자: str?.length ?: 0 형태로 사용한다.
    • 앞의 연산 결과가 null이면 뒤의 값을 사용한다.
    • Elvis연산은 early return에도 사용할 수 있다.

null 아님 단언

Kotlin

fun startsWith(str: String?): Boolean {
    return str!!.startsWith("A")
}
  • str!!.length 형태로 사용한다.
  • nullable type이지만, 아무리 생각해도 null이 될 수 없는 경우에 사용한다.
  • 혹시나 null이 들엉오면 NullPointException이 발생하기 때문에 정말 null이 아닌게 확실한 경우에만 널 아님 단언을 사용해야 한다.

널 아님 단언을 사용했지만 null이 사용된 경우

fun main() {
    println(startsWith(null))
}

자바 에서 null 을 안전히 다루는 방법

플랫폼 타입

Java

public class Person {

    private final String name;

    public Person(String name) {
        this.name = name;
    }

    @NotNull
    public String getName() {
        return name;
    }
}

Kotlin

fun main() {

    var person = Person("heekng")

    // @Nullable 일 때에는 가능
    // @NotNull 일 때에는 불가능
    startsWithA(person.name)

}

fun startsWithA(str: String): Boolean {
    return str.startsWith("A")
}
  • 플랫폼 타입: 코틀린이 null 관련 정보를 알 수 없는 타입
  • 코틀린에서 자바 코드를 가져와 사용할 때 어떻게 처리되는가?
  • 만약 Java 코드에 @Nullable 어노테이션이 붙어있을 경우에는 불가능하다.

자바 에서 null 을 안전히 다루는 방법

  • 하지만 @NotNull 어노테이션이 붙어있을 경우에는 가능하다.

자바 에서 null 을 안전히 다루는 방법

  • Java코드를 통해 null 가능성을 확인하지 못할 때에는?
    • 사용은 가능하지만 null이 입력되었을 때 에러가 발생한다.

자바 에서 null 을 안전히 다루는 방법

자바 에서 null 을 안전히 다루는 방법

  • 결국 Kotlin에서 Java코드를 가져다 사용할 때에는 Java코드를 읽으며 null 가능성을 확인하거나, Kotlin으로 wrapping 해야 한다.

We’ve updated our privacy policy so that we are compliant with changing global privacy regulations and to provide you with insight into the limited ways in which we use your data.

You can read the details below. By accepting, you agree to the updated privacy policy.

Thank you!

View updated privacy policy

We've encountered a problem, please try again.

null 과 null 로 인해 발생하는 문제

null pointer 와 null reference

널 포인터(null pointer)는 유효한 객체를 참조하지 않는 포인터를 나타내기 위해 예약된 값을 갖는다. 

널 포인터는 널 값과는 다른 의미를 갖는다. 대부분의 프로그래밍 언어에서 널 포인터는 "값 없음"을 의미하지만, 관계형 데이터베이스에서 널 값은 "알려지지 않은 값"을 의미한다. 이것은 실질적으로 중요한 차이로 이끌어 진다: 대부분의 프로그래밍 언어들은 두 널 포인터를 같다고 여기지만, 관계형 데이터베이스 엔진은 두 널 값을 같다고 여기지 않는다

https://en.wikipedia.org/wiki/Null_pointer

자바의 null 참조

  • 의미가 모호하다.
  • 초기화 되지 않은 상태
  • 정의되지 않은 상태
  • 값이 없는 상태
  • 모든 상태의 기본 값이다
  • 모든 참조는 null 일 수 있다

소프트웨어 결함 통계

자바 에서 null 을 안전히 다루는 방법
Sapienz: Multi-objective Automated Testingfor Android Applications

facebook 에서 인수한 Sapienz 라는 분석도구를 이용해 구글 플레이에 올라간 1000개의 App 을 결함 분석 후 통계를 냄

Native Crash (Java 가 아닌 부분에서 발생한 에러, 분석되지 않음) 와 NullPointerException 이 압도적인 비율을 차지하고 있다

Native Crash 에서도 NullPointer 가 존재할 것이기 때문에 NullPointer 는 실제 분석된 통계보다 더 많이 발생한다

null 로 인해 발생하는 문제

null 참조를 허용하는 프로그램에서 자주 발생하는 버그는 NullPointerExcetpion 이다

이는 아무것도 가리키지 않는 식별자를 역 참조 (dereference) 할 때 발생한다

1965 년 알골 (ALGOL) 이라는 명령형 언어를 설계 할 때 토니 호어 (Tony Hoare) 널 참조를 발명 했다

다음은 널 참조를 발명한 것을 후회하면 토니 호어가 말한 내용의 일부이다

널 참조를 십억 불짜리 실수 라고 부른다. 컴파일러가 자동으로 검사하는 방식으로 모든 참조 사용이 절대적으로 안전함을 확신하는 것이 목표 였다. 하지만 구현하기 너무 쉬웠기 때문에 널을 참조에 넣고 싶은 유혹에 저항할 수 없었다. 이 때문에 수많은 오류, 취약점 등이 생겨났고 지난 40년간 고통과 손해는 십억 불 정도일 것이다.

지금은 널 참조를 사용하지 않아야 하는것이 상식이 되고도 남아야 이상적인 것이 현실이다

하지만 널이 반드시 나쁜것 만은 아니며 비즈니스 널 도 존재한다

`java.net.Socket 의 생성자`

public Socket(String address, int port, InetAddres localAddr, int localPort) throws IOException
localAddr을 null 로 지정하면 address 로 주어진 주소를 로컬 주소로 설정하는 것과 동일하다.
localPort 를 0 으로 지정하면 소켓 주소를 바인딩할 때 시스템에서 비어 있는 포트를 소켓 로컬 포트로 할당한다.
https://docs.oracle.com/javase/7/docs/api/java/net/Socket.html

위에서 소개한 "localAddr을 null로 지정" 하는 경우 Null 참조가 올 바른 파라미터 이며, 이런 경우 비즈니스 널 (Business Null) 이라고 한다

"localPort 를 0 으로 지정" 하는 경우 센티널 값 (Sentinel Value) 라고 한다. 이는 값 자체를 0이라고 표현하는 것이 아닌 포트 값이 존재하지 않음을 표현한다

이런 상황들 외에도 다른 비즈니스 널이 존재하며, 이런 상황에는 어떻게 정리해야 할지를 알아야 한다

참조가 반드시 널인지 확인을 반드시 하고 참조를 하는등 규칙을 정한다면 NullPointerException 과 같은 예외를 마주치는 일은 없을테지만 이런 규칙은 실수로 인해 놓칠 수 있다 (휴먼 에러 발생 위험)

null 을 안전하게 다루는 방법

Java : 단정문 (assertion)

Java 의 assert 문은 1.4 에서 소개되었다

코드를 보다 쉽게 읽을 수 있게 해주는 잘 알려지지 않은 키워드로 남아 있다

개발을 하다보면 아래와 같은 null 체크 코드를 자주 만나게 되는데, assert 문을 이용해 이를 간단하게 만들 수 있다

Connection conn = getConnection();
if(conn == null) {
    throw new RuntimeException("Connection is null");
}

java assertion 은 keyword 이기 때문에, 별도의 라이브러리나 import 를 할 필요가 없다

다만 주의할점은 JVM 의 기본 옵션으로 비활성화 되어 있다는 점이다

이를 활성화 하기 위해서는 CommandLine Argument 로 -enableassertions 옵션을 전달해야 한다. (이는 축약해서 -ea 로 사용할 수 있다)

특정 클래스나 패키지에 활성화 하고 싶다면 -ea:Class, -ea:Package/Class -ea:Package… 와 같은 방식으로 활성화 할 수 있으며

비활성화 하고 싶다면 -disableassertions(-da) 옵션을 전달하면 된다

단정문 사용하기

assert keyword 는 boolean 조건을 지정하기만 하면 쉽게 사용할 수 있다

public void setup() {
    Connection conn = getConnection();
    assert conn != null;
}

Java 에서는 assertion 에 대해 두 번째 구문을 제공하며, assertion Error 발생시 이를 활용해 에러가 출력된다

public void setup() {
    Connection conn = getConnection();
    assert conn != null : "Connection is null";
}

AssertionError Handling

AssertionError 클래스는 Error 클래스를 상속받고 있고, 이는 Unchecked 예외이다

이를 try-catch 구문을 통해 Handling 시도를 해서는 안된다

AssertionError 는, 복구할 수 없는 상태를 나타내기 위한 것이기 때문이다

Assertion Best Practice

assertion 은 비활성화 될 수 있다는 점에 유의해야한다

assertion 을 사용할 때는 다음에 4가지에 대해 고민해 보고 사용해야한다

1. 항상 null 또는 빈 값을 체크해야 하는 경우 선택사항이다

2. public method 에서 사용하는 것을 피하고 대신 IllegalArgumentException or NullPointerException 를 사용해야 한다

3. assertion 조건에서 메소드를 호출해서는 안되고, 로컬 변수에 메소드의 결과를 할당한 뒤 해당 변수를 assertion 과 함께 사용해야 한다

4. assertion 은 절대 실행될 수 없는 위치에 존재하는 것이 좋다. (switch 문의 default 혹은 절대 끝날 수 없는 loop)

Java : java.util.Objects

Java 의 Objects 는 1.7 에서 소개되었다

버전이 올라가면서 다양한 NullHandling 관련 메소드들이 추가되었다

Java8

  • isNull(Object obj)
  • nonNull(Object obj)
  • requireNonNull(T obj)
  • requireNonNull(T obj, String message)
  • requireNonNull(T obj, Supplier<String> messageSupplier)

requireNonNull 메소드는 obj 가 null 일 경우 NPE 를 발생시킨다.

Java 9

  • requireNonNullElse(T obj, T defaultObj)
  • requireNonNullElseGet(T obj, Supplier<? extends T> supplier)

defaultObj, supplier, supplier.get() 이 각 null 일 경우 NPE 를 발생시킨다.

Java : java.util.Optional

Java 의 Optional 은 1.8 에서 소개되었다

이는 나사가 하나쯤 빠져 있다는 얘기가 많기 때문에 사용시 유의해야 한다

`Oracle 자바 아키텍트의 말`

내용이 Null 이 아니라고 확신할 수 없다면 절대 Optional.get 을 호출하지 말라. orElse 혹은 ifPresent 와 같은 메서드를 사용해야 한다.

Java Optional 클래스에는 isPresent(), get() 메소드가 있어서는 안된다. (있지 말아야할 것이 존재함)

그 이유는 Optional 은 선택적 데이터를 안전하게 처리할 수 있는 계산 환경을 제공하기 때문이다

Optional 에서 isPresent() 메소드를 이용해 값의 유무를 체크한다면 이는 obj != null 과 같은 행동이다

getOrElse 혹은 getOrThrow 와 같은 메소드를 사용해야 한다

Optional 을 사용하는 가장 좋은 방법은 합성 이다

이는 함수형 프로그램에서 말하는 모나드 라는 근본적인 개념을 보여준다

Java 의 Stream 도 모나드의 특성을 가지고 있다

Optional 을 사용할 때의 규칙

  • 절대 Optional 변수와 반환 값에 null 을 사용해서는 안된다
  • Optional 에 값이 있다고 확신하지 않는 한 Optional.get() 을 호출해서는 안된다
  • Optional 에서 여러 메소드를 연쇄적으로 호출해서 값을 얻기 위해 Optional 을 생성하는 것은 권장하지 않는다
  • Optional 로 값을 처리하는 중 중간 값을 처리하기 위해 또 다른 Optional 이 남용되면 복잡해진다
  • Optional 을 필드, 메소드 매개변수, 집합 자료형에 사용해서는 안된다
  • 집합 자료형 (Collections) 는 Optional 을 사용하지 말고, 빈 집합을 사용해야 한다

Optional 을 사용할 때 초보자가 많이 하는 실수

@Test
void optional_test() {
  OrderItem orderItem = Optional.ofNullable(new User())
    .map(u -> u.order.orderItem)
    .orElse(null);
}

@Test
void optional_testV2() {
  OrderItem orderItem = Optional.ofNullable(new User().order.orderItem)
  	.orElse(null);
}

@Test
void optional_testvV3() {
  OrderItem orderItem = Optional.ofNullable(new User())
    .map(u -> u.order)
    .map(o -> o.orderItem)
    .orElse(null);
}

private class User {
    Order order;
}

private class Order {
    OrderItem orderItem;
}

private class OrderItem {

}

1 번의 케이스를 가장 많이 봤고, 간혹가다 2번 케이스에 해당되는 코드도 보이는데 두 코드 모두 NPE 가 발생하는 코드

// doSomethingCalculate 가 매번 호출된다.
@Test
void optional_or_else() {
  String result = Optional.ofNullable("hello i'm ncucu")
  .orElse(doSomethingCalculate());
}

// doSomethingCalculate 가 null 일 경우만 호출된다.
@Test
void optional_or_else_get() {
  Optional.ofNullable("hello i'm ncucu")
  .orElseGet(this::doSomethingCalculate);
}

private String doSomethingCalculate() {
  System.out.println("doSomethingCalculate is called");
  try {
  	Thread.sleep(5000);
  } catch (InterruptedException e) {
  	e.printStackTrace();
  }
  return "calculate";
}

가장 많이하는 실수의 2번째이다. 만약 수행하는데 시간이 오래걸리는 계산 로직이 있거나, 외부 API Call, 혹은 DB Access 와 같은 작업을 할때 특히 주의해야 한다

orElse 메소드는 null 여부를 떠나 반드시 호출되며, orElseGet 은 null 일 경우에만 호출된다

만약 특정 값이 null 일때 해당 로직을 수행하도록 의도하고 orElse 를 사용해서 코드를 작성한다면 의도한대로 동작하지 않는다

getter 메소드는 Optional 타입을 반환해야 할까 ?

위 질문에 대한 Oracle 자바 아키텍트 Brian Getz 의 답변이다

물론, 사람들이 원한다면 그렇게 해야한다.하지만 우리가 그 기능을 추가할때는 의도한 것은 Maybe 타입 이었다.우리의 의도는 "결과가 없음" 을 나타내는 명확한 방법이 필요한 라이브러리 메소드 반환 타입에 대한 제한된 메커니즘을 제공하는 것으며,결과가 없음을 나타내는데 null 을 사용하면 오류가 발생할 가능성이 압도적으로 높았다.이를 사용할때 결과로 배열이나 리스트를 반환하는데에는 절대 사용해선 안된다. 대신 빈 배열 또는 빈 리스트를 반환해야 한다.또한 어떤 필드나 메서드의 매개변수로 사용해서는 안된다.일반적인 getter 의 반환값으로 사용할 경우 남용될 것이라고 생각된다.Optional 을 사용하지 않을 이유는 없다, 하지만 과잉 사용에 대한 위험에 대한 우려가 된다.

Java : JSR-305

  • FindBugs에서 "David Hovemeyer, Jaime Spacco, and William Pugh. Evaluating and Tuning aStatic Analysis to Find Null Pointer Bugs, Proceedings of the 2005 ACM SIGPLAN-SIGSOFTWorkshop on Program Analysis for Software Tools and Engineering (PASTE 2005),Lisbon, Portugal, September, 2005." 라는 논문에서 소개한 애노테이션
  • @NonNull, @NullFeasible, @UnknownNullness 등이 존재한다
  • IDE 지원
  • 아직 자바 표준으로 채택되진 않음
  • com.google.code.findbugs:annotations 의존성 추가 필요
  • spring-core 5.0 부터 지원

Java : JSR-308

  • JSR 305 와 함께 사용 가능한 별개의 명세, JEP 104 로 Java 8에 포함됨
  • 제네릭 타입 파라미터 / 타입 캐스팅 등 타입 자체에 애노테이션을 사용할 수 있도록 허용
  • Checker Framework

Checker Framework

  • null 안전성 확인
  • @Nullable, @NonNull, @PolyNull ..
  • Map Key, 잠금, 순차자료형, 정규식 등 확인 기능
  • 특정 환경이나 IDE 에 독립적이다.

@DefaultQualifier

  • package, class 또는 method level 에서 @Nullable or @NonNull 에 대한  default rule 지정 가능
@DefaultQualifier(value = Nullable.class, locations = TypeUseLocation.FIELD)
class MyClass {
    Object nullableField = null;
    @NonNull Object nonNullField = new Object();
}

마치며 

이번에는 Java 에서 null 을 안전하게 다루는 다양한 방법에 대해 살펴보았습니다

Java 8 버전 이상을 사용한다면 아무래도 Optional 을 주로 사용할텐데 그동안 별 생각없이 사용해온건 아닌지 다시 한번 돌아보면 좋을것같고, 그저 Optional 을 if (obj != null ) 대용으로 사용해 오신건 아닌지 돌아보는 계기가 되셨으면 합니다

(Optional.ofNullable(obj).ifPresent(...) 과 같은 코드는 정말 지양해야합니다)

참고

- https://www.youtube.com/watch?v=vX3yY_36Sk4 

- https://docs.oracle.com/javase/7/docs/api/java/net/Socket.html

- https://jaxenter.com/assertions-java-150586.html 

- https://www.youtube.com/watch?v=jI4aMyqvpfQ

- https://stackoverflow.com/questions/26327957/should-java-8-getters-return-optional-type/26328555#26328555

- https://dzone.com/articles/java-8-optional-usage-and-best-practices

- https://www.baeldung.com/java-assert

- https://www.oracle.com/technical-resources/articles/java/ma14-architect-annotations.html

- https://d2.naver.com/helloworld/8725603

- https://checkerframework.org/