IT/Java

Java에서 NullPointerExceptions을 예방하는 방법 (feat. Optional)

yeTi 2021. 5. 10. 14:32

안녕하세요. yeTi입니다.
오늘은 java에서 볼 수 있는 NullPointerExceptions을 예방하는 방법에 대해 알아보고자 합니다.

개요

Java를 개발하다보면 NullPointerExceptions을 어렵지 않게 만날 수 있는데요.
어떤 스타일로 개발을 하면 NullPointerExceptions을 피해갈 수 있을지에 대해 고민해보고 내용을 공유하고자 합니다.

문제인식

Java에서 Null을 언제쓰는지 생각을 해보면 클래스가 초기화되지 않았거나, 인스턴스가 존재하지 않는다는 의미를 표현하고 싶을 때 사용합니다.

간단한 해결법은 클래스를 원하는 시점에 초기화하고 인스턴스가 없다는것을 인지하고 로직을 구현하면 되는데요. 여기서 사용하기 곤란한 코드가 발생합니다.

바로 다음과 같은 코드입니다.

String version = "UNKNOWN";
if(computer != null){
  Soundcard soundcard = computer.getSoundcard();
  if(soundcard != null){
    USB usb = soundcard.getUSB();
    if(usb != null){
      version = usb.getVersion();
    }
  }
}

인스턴스가 없다는것을 인지하고 로직을 구현하려고 하니, 코드의 가독성이 떨어지고 boilerplate code 또한 발생하게 됩니다. 그리고 이는 사용하는 변수의 수가 늘어날 수록 더욱 심해질겁니다.

그렇다면 어떻게하면 간결한 코드를 유지하면서 null 처리를 할 수 있을까요?

클래스 관점에서 해결 방안

아래에서 사용한 코드는 github에서 확인할 수 있습니다.

solve 1

클래스 선언시 인스턴스를 생성하는 방법입니다.

public  class  Computer {
  private Soundcard soundcard = new Soundcard();
  ...
}

인스턴스가 생성되는 시점에 사용하기 원하는 모든 인스턴스를 생성합니다.
이렇게 하면, 사용단에서는 Null이 발생하지 않기 때문에 간결한 코드를 유지할 수 있는데요.

Computer  computer = new  Computer();
String  version = computer.getSoundcard().getUsb().getVersion();
log.info("USB Version is " + version);

하지만 인스턴스가 없다는 의미를 전달하기 위해서 다른 방식을 고려해야한다는 문제점과, 인스턴스를 사용하지 않는 상황에서도 생성함으로써 불필요한 메모리를 할당하는 문제점도 존재합니다.

solve 2

Optional을 사용하여 클래스 선언시 빈 인스턴스를 생성하는 방법입니다.

public  class  Computer {
  private Optional<Soundcard> soundcard = Optional.empty();
  ...
}

사용단에서는 Stream을 사용하여 그래프탐색을 할 수 있습니다.

Computer  ct = new  Computer();
String  version = ct.getSoundcard()
                    .flatMap(Soundcard::getUsb)
                    .flatMap(USB::getVersion)
                    .orElse("UNKNOWN");
log.info("USB Version is " + version);

하지만 Optional을 클래스의 필드 타입으로 사용할 경우 Serializable interfaceimplement할 수 없기 때문에 serializable domain model을 사용할 수 없습니다.

Serialize에 대한 참고자료. 자바 직렬화, 그것이 알고싶다. 훑어보기편

solve 3

Optional로 사용하기 위해 Getter를 추가하는 방법입니다.

public  class  Computer {
  private Soundcard soundcard;
  ...
  public  Optional<Soundcard> getSoundcardAsOptional() {
    return  Optional.ofNullable(soundcard);
  }
  ...
}

사용단에서는 Stream을 사용하여 그래프탐색을 할 수 있습니다.

Computer  ct = new  Computer();
String  version = ct.getSoundcardAsOptional()
                    .flatMap(Soundcard::getUsbAsOptional)
                    .map(USB::getVersion)
                    .orElse("UNKNOWN");
log.info("USB Version is " + version);

본 방법은 Java 8 in Action에서 serializable domain model을 사용하는 경우 제안하는 방법입니다.

개인적으로 이 방법에 의구심이 있는데요.
사용단에서 특정 인스턴스가 Nullable한 특성을 가지는지를 인지해서 함수를 선택하여 사용하는것이 최선인가 라는 것입니다.

solve 4

try-catchexception을 처리하는 방법입니다.

Computer  computer = new  Computer();

String  version;
try {
  version = computer.getSoundcard().getUsb().getVersion();
} catch (NullPointerException  e) {
  version = "UNKNOWN";
}
log.info("USB Version is " + version);

해당 방법은 if/else문을 피한것으로 보이지만 결국 catch문에서 분기문을 통하여 적절한 조치를 해야할 수 있기 때문에 근본적으로 boilerplate code를 해결하지는 못합니다.

함수 관점에서 해결 방안

함수를 사용함에 있어서 반환값이 존재하지 않을 경우가 있는데요.

이 때, null, Optional, Exception중에 무엇을 사용하는 것이 좋은지에 대해 생각해 보겠습니다.

Null 반환

일단 기본적으로 null을 반환하는것은 지양해야한다고 생각합니다.

왜냐하면 반환값에 null이 있을 수 있다고 고려된다면 함수 사용 후 무조건 null을 처리하는 코드를 넣어줘야하기 때문에 비효율적이라고 생각되기 때문입니다.

이에 설계단에서 인터페이스 명확히 정의하여 함수의 반환값이 null이 제공되지 않도록 하는게 좋다고 생각합니다.

Optional

null 반환에 의미있는 대안으로 Optional객체를 반환해주는 것도 좋은 방안 중 하나라고 생각합니다.

하지만 객체지향적인 관점에서 본다면 함수의 인터페이스를 정의하는데 반환값이 있을수도 있고 없을 수도 있다는것은 모호한 설계라고 생각합니다. 설계나 인터페이스는 명확한것이 좋기 때문에 반환값은 값(인스턴스)로 정의하고, 필요에 따라 사용단에서 Optional 랩핑하여 활용하는것이 좋다고 생각합니다.

Exception

값이 존재하지 않는데, null도 지양하고 Optional도 지양한다면 어떻게 처리할 수 있을까요?
바로 NoSuchElementException을 활용하는 방안입니다.

Exception 또한 남용한다면 코드의 가독성을 떨어트리는 요소입니다.

그럼에도 불구하고 Exception이 좋다고 생각하는 이유는 함수의 인터페이스를 순수 인스턴스로 정의할 수 있고, NoSuchElementException를 반환값에 대해서만 발생하도록 제한한다면 가독성 또한 저하되지 않는다고 생각합니다.

결론

결론적으로 serializable domain model 수용하고, 인스턴스의 초기화를 필요시점에 할수 있는 solve 3의 방식이 가장 좋아 보입니다.

물론, 이 방법도 optional을 위한 함수를 만들어줘야하는 피로감이 있지만 더 나은 방법을 찾을 때까지는 가장 최선의 대안이라고 생각합니다.

그리고 함수의 인터페이스에서 NoSuchElementException을 활용하여 존재하지 않는 값에 대한 처리를 하는것이 좋다고 생각합니다. 물론 각 매개변수의 타입이 collection이라면 빈 인스턴스를 제공하는것이 좋습니다.

참고 문헌