3 분 소요

null이란?

  • null은 자바의 키워드이다.
    • null은 접근 지정자인 private 처럼 대소문자를 구분하는 키워드이다. 때문에 Null, NULL이 아닌 null로만 사용한다.
  • null은 참조형 타입의 기본 값이다.
    • 기본형 타입이 기본 값을 갖는 것(int형: 0, boolean형:false) 처럼 참조형 타입(객체, 배열의 참조변수)은 기본 값으로 null을 갖는다. 또한 null은 참조형 타입에서만 사용할 수 있고, 기본형 타입의 변수 할당할 경우 컴파일 오류가 발생한다.

Auto-boxing

Integer, Double 과 같은 Wrapper 클래스 레퍼런스가 null을 참조하고 있는 상태에서 이를 기본형 타입으로 언박싱 하는 경우 NullPointerException이 발생한다. 이는 컴파일 시점에 확인이 불가능해서 주의해야한다.

Integer value = null;
int intValue = value; // NullPointerException 발생 >> value.intValue()

아래와 같은 상황 주의

Map<String, Integer> map = new HashMap<>();
map.put("일",1);
int n = map.get("삼");// NullPointerException 발생

static 멤버

null을 참조하는 객체의 메서드를 호출하면 NullPointerException 가 발생하지만 static으로 선언된 메서드인 경우 정상 실행된다.

static 멤버는 객체가 아닌 클래스에 속하기 때문에 컴파일 타임에 최적화가 된다.

public class MyClass {
	public static void sayHello() {
		// ...생략
    }
	
    public static void main(String[] args) {
		MyClass myClass = null;
		myClass.sayHello(); // 레퍼런스가 null이지만  NPE가 발생하지 않는다.
    }
}

NullPointerException

앞에서 본 것처럼 자바에서 null은 참조가 없다는 의미인데 null을 참조하는 레퍼런스로 인스턴스 메서드를 호출하는 등을 수행하는 경우 NullPointerException(이하 NPE)가 발생한다.

NPE는 런타임에 발생하기 때문에 프로그램 실행하기 전인 컴파일 시점에는 알아채기 어렵다.

NPE 예제

// 사람
public class Person {
	private Phone phone;
    
    public Person getPhone(){
        return phone;
    }
}

// 핸드폰
public class Phone {
	private Manufacturer manufacturer;
}

// 제조사
public class Manufacturer {
	private String name;
}

편의상 멤버 필드에 대한 getter, setter는 생략하였다. 있다고 가정하고 아래에는 핸드폰 제조사의 이름을 반환하는 메서드이다. 아래 메서드를 호출하는 경우 NPE 위험성이 있다.

만약 Person 객체에서 phone이 null인 경우 getPhone() 메서드는 null이 나온다. 이때 phone.getManufacturer() 를 호출하는 순간 phone이 null이기 때문에 NPE가 발생한다.

// 핸드폰 제조사의 이름을 반환한다.
public String getPhoneManufacturerName(Person person) {
    return person.getPhone().getManufacturer().getName();
}

인자로 넘겨진 person 객체가 null인 경우 getPhone() 메소드를 실행하는 시점에서 NPE가 발생한다.

또한 이 getPhoneManufacturerName 메서드를 호출하는 곳에서 이 메서드의 결과로 null을 리턴받는 경우 향후 이 값을 이용하여 NPE가 발생할지도 모른다.

String manufacturerName = getPhoneManufacturerName(person);

// manufacturer 가 null 을 참조하고 있는 경우 NPE가 발생할 수 있다.
String lowerCaseName = manufacturerName.toLowerCase();

NPE 예방

1) 고전적인 방법으로는 하나하나 null 값을 체크하여 NPE를 예방하는 방식이 있다. 이 방식의 경우 하나하나 null 여부를 체크해야 해서 깊이가 깊은 클래스 (클래스의 필드로 객체를 갖는 경우) 일 수록 실수할 확률이 높다.

   public String getPhoneManufacturerName(Person person) {
   	if (person != null) {
   	    Phone phone = person.getPhone();
           if (phone != null) {
           	Manufacturer manufacturer = phone.getManufacturer();
   	        if (manufacturer != null) {
   	            return manufacturer.getName();
   	        }
   	    }
   	}
   	return "Samsung";
   }

가독성 부분에서 개선하기위해 아래와 같이 수정할 수 있다. 하지만 이 경우 return문이 여러곳으로 유지보수가 어렵다.

   public String getPhoneManufacturerName(Person person) {
   	if (person == null) {
   		return "Samsung";
   	}
   
   	Phone phone = person.getPhone();
   
   	if (phone == null) {
   	    return "Samsung";
   	}
   
   	Manufacturer manufacturer = phone.getManufacturer();
   
   	if (manufacturer == null) {
   	    return "Samsung";
   	}
   
   	return manufacturer.getName();
   }
  1. 널 객체 패턴(Null Object Pattern) 이란 방법도 있다. 이 패턴은 null 키워드를 사용하지 않도록 이를 대체하는 객체를 정의하는 것이다.

    먼저 인터페이스를 만들고 이를 구현하는 클래스를 만든다. 그리고 null을 대체하기 위한 클래스를 만든다.

    interface Messenger{
        void send(String msg);
    }
       
    class Kakao implements Messenger {
    	@Override
    	public void send(String msg) {
    		// ... 코드 구현 생략
        } 
    }
       
    class Line implements Messenger {
    	@Override
    	public void send(String msg) {
    		// ... 코드 구현 생략
    	}
    }
       
    // null object 역할
    class NullMessenger implements Messenger {
    	@Override
    	public void send(String msg) {
    		// 아무것도 구현하지 않는다.
    	}
    }
    

    다음은 이를 사용하는 예제이다.

    class MessengerFactory {
       	
    	public static Messenger getMessenger(String name) {
    		if ( ... ) {
    			// 특정 조건에 맞는 Messenger 구현체 클래스의 객체를 반환한다. 
    		}
       		
    		// 결과가 없는 경우 `null`을 반환하지 않고 `NullMessenger`를 반환한다.
    		return new NullMessenger();
        }
    }
       
    // ... 코드 생략
       
    Messenger messenger = MessengerFactory.getMessenger("KakaoTalk");
    messenger.send("Hello");
    

    이 경우 고전적인 방법처럼 if문을 많이 사용해가며 null 여부를 체크할 필요가 없다.

    여기서 좀더 수정을 해보면 아래와 같이 작성할 경우 널 객체 패턴용 클래스를 만들지 않고 싱글톤 기반으로 널 객체를 이용할 수 있다.

    interface Messenger {
    	void send(String msg);
       
    	public static final Messenger NULL = new Messenger() {
    		@Override
    		public void send(String msg) {
                // 아무것도 하지 않는다.
    		}
    	};
    }
    

    하지만 이런 널 객체 패턴 방법의 경우 널 객체 클래스의 존재를 잊는다면 null을 검사했던것 보다 더 복잡한 검사를 할 수 도 있고 인터페이스에 새로운 메서드가 추가가 된다면 구현체 클래스, 널 객체 클래스에서 메서드를 구현해 줘야해서 유지보수 비용이 늘어난다.

  2. Optional 를 사용한다.

    Java 8에서 등장한 Optional을 사용하면 NPE를 방지할 수 있다. Optional은 제네릭 타입을 감싸는 wrapper class이다. Optional 관련해서는 나중에 포스팅

댓글남기기