Java

Java 에서 널을 안전하게 다루기

엔꾸꾸 2021. 7. 11. 00:40

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

 

null pointer 와 null reference

 

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

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

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

 

자바의 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/