상황

다음과 같이 상품(Product)을 나타내는 클래스가 있다.

public class Product {
  private Long price;
  public Long getPrice() {
    return price;
  }
}

만약 아래 코드처럼 상품의 가격이 0인지 확인하는 코드를 짜면 어떻게 될까?

public void someMethod() {
  Product product = new Product();
  if (product.getPrice() == 0L) {
    System.out.println("price is zero.");
  }
}

코드를 실행하면 Null Pointer Exception이 발생한다. 왜일까? 아마 product객체의 getPrice() 메서드가 null을 반환하기 때문인 것 같다. 그런데 정말로 단지 getPrice()의 반환값이 null이기 때문에 나는 것일까?
아래 코드를 한번 보자.

public void someMethod() {
  if (null == null) {
    System.out.println("null is null.");
  }
}

극단적으로 null끼리 비교하는 코드이지만 Null Pointer Exception은 발생하지 않는다. 그럼 첫 번 째 코드에서는 왜 Null Pointer Exception이 발생했던 걸까? 이유는 Auto Unboxing에 있다.

Auto Unboxing

첫 번 째 코드를 보면 Product클래스의 getPrice() 타입은 Long인데 이와 비교하려는 0L의 타입은 Primitive 타입인 long이다. 자바에서는 이렇게 Wrapper 타입과 Primitive 타입을 비교하는경우, Wrapper 타입을 Primitive 타입으로 바꾸는 Unboxing이 자동으로 수행되는데 이를 Auto Unboxing이라고 한다.

위의 코드에서 Unboxing 과정은 Number 클래스의 longValue() 메서드를 호출함으로써 수행되는데, 메서드가 호출되는 대상 객체가 null이기 때문에 메서드를 호출하지 못하고 Null Pointer Exception이 발생하게 된 것이다.

해결책?

그럼 위와 같은 상황에서 Null Pointer Exception이 발생하지 않게 하려면 어떻게 해야할까? 여러가지 방법이 있을 것 같다.

1. null 체크 로직

값을 비교하기 전에 비교 대상 객체가 null인지 체크하고 적절한 처리를 해준다. 아마 가장 쉽게 떠올릴 수 있는 방법일 것이다. 매번 null 체크를 하는 것은 조금 번거로울 수 있다.

2. Auto Unboxing이 일어나지 않도록 한다.

다음 처럼 객체끼리 비교하면 Auto Unboxing이 발생하지 않는다. 그러므로 NPE도 발생하지 않는다.

public void someMethod() {
  if (product.getPrice() == Long.valueOf(0)) {
    System.out.println("price is zero.");
  }
}

그런데 Long.valueOf() 메서드를 저렇게 사용하는건 불필요하고 번거로워 보인다.

3. 그냥 Primitive 타입을 사용한다.

Product 클래스의 price 속성이 꼭 null을 허용할 수 있는 Wrapper타입 Long이어야 할까? 상품의 가격이 null이라는건 어떤 의미를 가져야 할까? 만약 null이 어떠한 의미도 가지지 않고 불필요 하다면 그냥 Primitive 타입을 사용하는게 가장 깔끔하고 명확한 방법이라고 생각한다. 이렇게 하면 Null Pointer Exception이 발생할 걱정은 하지 않아도 된다.