웹 개발

NPE(Null Pointer Exception)으로부터 안전한 프로그래밍 하기

노루아부지 2020. 11. 8. 01:36

NullPointerException은 개발 과정에서 가장 많이 접하면서 간과하기 쉬운 예외 중 하나입니다. reference type을 다룰 때는 항상 null에 대비하여 프로그래밍을 해야 합니다. 이 과정에서 불필요한 null check code가 포함되며, nessted object 참조 과정에서 반복적인 null check로 코드의 가독성을 떨어뜨리곤 합니다.

 

 

NPE(NullPointerException)이란?

java 데이터 타입은 기본 타입(primitive type)과 참조 타입(reference type)이 있습니다. 참조 타입은 객체의 생성 이전에는 할당된 메모리 주소가 없는 null을 참조하는 변수이며 이를 가지고 아래 작업을 수행하면 NPE가 발생하게 됩니다.

null 참조는 1965년에 Tony Hoare라는 사람에 의해 처음으로 고안되었습니다. 당시 그는 "존재하지 않는 값"을 표현할 수 있는 가장 편리한 방법이 null 참조라고 생각했다고 합니다. 하지만 나중에 그는 그 당시 자신의 생각이 "10억 불짜리 큰 실수"였고, null 참조를 만든 것을 후회한다고 토로했습니다.

 

NPE가 발생하는 경우

  • null 객체의 instance 함수(static이 아닌 method)를 호출하는 경우
  • null 객체의 instance 변수에 접근하는 경우
  • null 배열 객체의 length를 구하려는 경우
  • null 배열 객체의 값을 index로 접근하는 경우
  • application에서 NPE를 던지는 경우

 

NPE를 예방하는 습관

1) equals(), equalsIgnoreCase() 비교 시 시, 문자열이나 null이 아닌 객체를 선행하여 비교

String a = null;

if(a.equals("java")) {
  // NPE 발생
  System.out.prnitln("true");
}
else {
  System.out.println("false");
}

a의 값이 null이 경우 NPE가 발생합니다. null은 객체가 아니기 때문에 당연히 equals라는 method도 없기 때문입니다.

이 경우 아래와 같이 하면 NPE 문제를 해결 할 수 있습니다.

String a = null;

if("java".equals(a)) {
  System.out.prnitln("true");
}
else {
  System.out.println("false");
}

비교의 주체가 문자열이라면 NPE가 발생하지 않습니다. 정리한다면, 문자열 비교는 non-null String을 주체로 비교하는 것이 좋습니다. 즉, 비교의 주체가 문자열이 오도록 하거나, Constant 등을 활용하여 코딩하는 것이 좋습니다.

 

2) null을 parameter로 넘기지 않는다.

null parameter로 인해 쓸데없는 null 체크도 해줘야 한다.

 

3) null check 구문 추가

public Integer parseInt(String v) {
  if(v == null) {
    return 0;
  }
  
  // something
}

 

 

4) toString() 보다는 String.valueOf()를 사용

변수의 값이 null일 경우 toString()을 사용하면 NPE가 발생하지만, String.valueOf()를 사용하면 "null"을 결과로 반환합니다.

 

5) 필요한 경우가 아니면 reference 타입보다는 primitive 타입을 사

6) 함수에서 null 대신 Collection 객체를 반환(null을 반환하면 호출자는 그에 대비하여 null check 구문이 필요

7) null에 안전한  java 내장 함수나 org.apache.commons.lang.StringUtils와 같은 helper class를 활용

System.out.println(StringUtils.isEmpty(null)); //true
System.out.println(StringUtils.equals("1", null)); //false
System.out.println(StringUtils.equals(null, "1")); //false
System.out.println(StringUtils.indexOf("noroo", null)); //-1
System.out.println(StringUtils.indexOf(null, "noroo")); //-1
System.out.println(StringUtils.upperCase(null)); //null

 

8) java assert, Unit Test를 활용하여 다양한 상황에서의 테스트를 수행

9) Spring을 사용하고 있다면 @NotNull annotation을 사용

10) public 함수를 사용할 때나, 협업할 때는 기능 및 제약사항을 명시하고 공유

NPE의 대부분은 개발자의 부주의에서 발생하고, 이는 정보의 부족에서 비롯되는 경우가 많습니다. 해결을 위해서는 input에 대해 validation 체크 후 적절한 처리를 하고, return 값과 발생 가능한 exception에 대해서 명시, 공유되어야 합니다.

 

input 값에 대한 validation 체크 후 Exception 발생

public getYear(String dt) {
  //if(dt == null || dt.length()  == 0) {
  if(StringUtils.isEmpty(dt) {
    throw new IllegalArgumentException("dt는 값이 있어야 합니다.");
  }
  
  // do something
}

 

return 값과  발생 가능한 Exception에 대한 명시는 반드시 명시되어야 합니다. 아래는 Map interface의 get() 함수 java doc의 일부입니다.

/**
 * Returns the value to which the specified key is mapped,
 * or {@code null} if this map contains no mapping for the key.
 * ……….
 * ……….
 * @return the value to which the specified key is mapped, or
 *         {@code null} if this map contains no mapping for the key
 * @throws ClassCastException if the key is of an inappropriate type for
 *         this map
 */

 

 

null 처리가 취약한 코드

null 처리가 취약한 코드에서는 NPE가 발생할 확률이 높습니다. 예를 들어 다음과 같은 구조의 class가 있다고 가정합니다.

/** 주소 */
@Getter
@Setter
public class Address {
    private String phone;
    private String city;
}

/** 회원 */
@Getter
@Setter
public class Member {
    private Long id;
    private String  name;
    private Address address;
}

/** 주문  */
@Getter
@Setter
public class Order {
    private Long id;
    private Date  date;
    private Member member;
}

 

Order 클래스는 Member 타입의 member field를 가지며, Member 클래스는 다시 Address 타입의 address 필드를 가집니다.

(위 코드에서 @Getter, @Setter는 getter, setter를 생성해주는 lombok annocation인데, 여기서는 설명을 생략합니다.)

그리고 "어떤 주문을 한 회원이 언느 도시에서 살고 있는지 알아내기"위해서 다음과 같은 메서드가 있다고 가정합니다.

 

/** 주문을 한 회원이 살고 있는 도시를 반환한다 */
public String getCityOfMemberFromOrder(Order order) {
	return order.getMember().getAddress().getCity();
}

 

위 코드는 아래와 같은 경우 NPE가 발생할 수 있습니다.

  • order parameter에 null 값이 넘어옴
  • order.getMember() 의 결과가 null
  • order.getMember().getAddress()의 결과가 null

또한 order.getMember().getAddress().getCity()의 결과가 null을 반환하면 호출부에서 NPE가 발생할 가능성이 있습니다.

 

 

java 8 이전의 NPE 방어

 

public String getCity(Order order) {
  if (order != null) {
    Member member = order.getMember();
    if (member != null) {
      Address address = member.getAddress();
      if (address != null) {
         String city = address.getCity();
         if (city != null) {
           return city;
         }
      }
    }
  }
  return "Seoul"; // default
}

 

위의 코드는 실무에서 심심치 않게 볼 수 있는 코드 입니다. 객체 탐색의 모든 단계마다 null check를 합니다.

 

그러나 java8이 등장하면서 null check 방식의 페러다임이 바뀌게 됩니다. 이 부분에 대해서는 다음 포스트에서 알아보도록 하겠습니다.

728x90
loading