Technical Note/JAVA

NullPointerException을 예방하는 방법

댓글을 달아주세요

NullPointerException은 null 때문에 발생하는 Runtime Exception입니다.

null인 객체에 접근하여 의도치 않게 NPE가 발생할 수 있지만 더 중요한 문제는 null 자체의 의미가 모호해 다양한 버그를 만들어냅니다. 예를 들어 메소드의 호출 결과로 null이 반환 되었을 경우 데이터가 없음을 의미할 수도 있지만 실패를 의미할 수도 있습니다. 심지어 성공을 의미하는 경우도 있을 수 있죠.

1
2
3
4
Map<String, String> map = new HashMap<String, String>();
map.put("hello", null);
map.get("hello");   // "hello" key의 value인 null을 return
map.get("nice");    // "nice" key가 없으므로 null을 return

위의 경우도 key에 대한 value가 null인지, key가 없어서 null을 return 한건지 return 받은 null 값을 가지고는 그 의미가 모호합니다. 누가 null을 만들었는지 참 얄밉지만 어쨋든 null 체크를 제대로 안했던지, 설계상의 결함이던지 null로 인한 버그는 결국 개발자 책임입니다.

똥이 무서워서 피하기보단 더러워서 피한다고… NPE를 예방하는 방법에 대해 정리해봤습니다.
들어가기에 앞서 null에 대한 프로그래머 대가들의 한마디…

“Null Sucks!”
- Doug Lea : Concurrent Programming In Java 저자 & JDK의 concurrency utilities 개발자

“I call it my billion-dollar mistake.”
- C.A.R. Hoare 2009년 어느 컨퍼런스에서.
1965년 Algol W 언어에서 처음으로 null reference를 만든 장본인 (Quick Sort도 발명)

코딩 습관 들이기

객체가 언제든 null일 수 있다는 판단하에 접근하면 도움이 될 것 같습니다.

1. equals 메소드 사용시
문자열 비교시 non-null String 기준으로 비교합니다. equals 메소드는 symmetric하므로 a.equals(b)와 b.equals(a)가 동일합니다. 그렇다면 null 일 수 있는 객체에서 equals 메소드 호출은 피하는게 낫겠죠?

1
2
3
4
5
6
public void doSomething() {
   // name이 null일 경우, NPE 발생!
   if (name.equals("BAD")) {
      // do something
   }
}
1
2
3
4
5
6
public void doSomething() {
   // name이 null이어도 NPE 발생 안함
   if ("BAD".equals(name)) {
      // do something
   }
}

다만 equals를 통과한 null 객체가 if문 안에서 메소드 호출 등의 이유로 다시 사용된다면 동일한 NPE가 발생할 수 있습니다. 이럴 경우엔 미리 null 체크를 하는게 좋겠죠?

2. toString()보다는 valueOf()를 사용할 것
1번 처럼 null 일 수 있는 객체에서 메소드 호출은 NPE 발생 위험이 있겠죠? static으로 제공되는 valueOf()를 사용하면 null을 Parameter로 넘겨도 null을 return할 뿐 NPE는 발생하지 않습니다. (valueOf()는 String, Boxed Primitives 클래스(Integer, Double 등)에서 static 메소드로 제공 됨)

1
2
3
BigDecimal bd = getPrice();
System.out.println(String.valueOf(bd)); // NPE 발생안함
System.out.println(bd.toString()); // NPE 발생

3. 메소드에서 null return하지 않기
가장 간단한 방법은 메소드 구현시 의미없는 null을 return하지 않도록 처리합니다. null이 아닌 빈 문자열 또는 빈 콜렉션을 return 하도록 합니다.

1
2
3
4
5
6
7
8
9
10
11
public List<User> getUsers() {
   // ...
 
   Result result = executeNamedQuery(GET_ALL_USERS);
 
   if (!result.isEmpty()) {
      return result;
   }
 
   return Collections.EMPTY_LIST; // or EMPTY_SET or EMPTY_MAP, etc. Depending on your return type
}

4. null을 Parameter로 넘기지 말 것
null을 Parameter로 넘길 경우 받는 쪽에서도 어찌보면 불필요한 null 체크가 필요하겠죠? null 자체에 어떤 특별한 의미가 없다면 주지도 받지도 맙시다.

5. 불필요한 autoboxing, unautoboxing 피하기 & Object 보다는 기본형 사용하기
Boxed Primitives 클래스와 기본형(primitive) 타입간에 자동으로 autoboxing, unboxing을 해주는데요. 자동으로 기본형 변환을 해주기 때문에 헷갈리기 싶습니다. (그냥 기본값이 들어가겠거니…) 가능하면 null reference를 가질 수 있는 객체가 아닌 자바 기본형(Primitive)을 이용하는 것도 방법입니다.

1
2
3
Person ram = new Person("ram");
// getPhone()에서 return된 결과가 null일 경우 NPE 발생
int phone = ram.getPhone();
1
2
3
4
5
6
private static Integer count;
 
// NPE 발생
if( count <= 0) {
  System.out.println("Count is not started yet");
}

6. Chaining 메소드 호출 자제하기

1
String city = getPerson(id).getAddress().getCity();

중간에 return 받은 값이 null일 경우 NPE가 발생하며 Stack Trace에서도 해당 line 위치만 출력되기 때문에 어디서 에러가 발생했는지 디버깅하기도 어렵습니다.

Library 이용하기

Apache Commons lang, Google Guava(예전 Goolge Collections) 등 null safe한 method를 이용하는 방법입니다. 자세한 사용은 Library의 JavaDoc을 읽어보길 권합니다.

1. Apache Commons의 StringUtils
String의 null 체크를 간단히 할 때 많이 사용하는 클래스입니다. StringUtils.isNotEmpty(), isBlank(), isNumeric(), isWhiteSpace() 등이 있죠.

1
2
3
4
5
6
7
8
9
10
11
// StringUtils methods are null safe, they don't throw NullPointerException
System.out.println(StringUtils.isEmpty(null));
System.out.println(StringUtils.isBlank(null));
System.out.println(StringUtils.isNumeric(null));
System.out.println(StringUtils.isAllUpperCase(null));
 
Output:
true
true
false
false

2. Guava의 Optional 클래스 이용하기
Optional는 nullable한 T를 non-null 값으로 대체시키기 위한 방법인데요. (말이 어렵죠?)
Optional 객체는 non-null인 T reference를 포함하거나 아무것도 포함하고 있지 않습니다.

한마디로 Optional 객체는 명시적으로 null 값을 갖지 않는다는거죠.
NullObject Pattern을 일반화시켰다고나 할까요? 바로 이 점을 이용해서 NPE를 예방합니다.

– absent : 아무 것도 포함하고 있지 않은 상태
– present : non-null 값을 갖은 상태

1) Optional 객체의 생성 (static 메소드)

Optional.of(T)
- T로 받은 non-null 값을 포함하는 Optional 객체 반환, T가 null 일 경우 NPE 발생
Optional.absent()
- 아무것도 포함하고 있지 않는 absent Optional 객체 반환
Optional.fromNullable(T)
- T로 받은 값이 non-null일 경우 present로, null일 경우 absent로 처리한 Optional 객체 반환

2) Optional 객체를 다루기 위한 메소드

boolean isPresent()
- Optional 객체가 non-null 인스턴스를 포함할 경우 true 반환
T get()
- Optional 객체가 present 일 경우 포함하고 있는 인스턴스를 반환, absent일 경우 IllegalStateException 발생
T or(T)
- Optional 객체의 present 값을 반환. 만일 값이 없을 경우 명시한 T를 반환 (기본값)
T orNull()
- Optional 객체의 present 값을 반환, 만일 값이 없을 경우 null을 반환. fromNullable의 역임
Set asSet()
- Optional 객체에서 포함하고 있는 인스턴스의 변경 불가능한 싱글톤 Set을 반환. 만일 인스턴스가 없다면 변경불가능한 Empty set을 반환

3) 간단 예제

1
2
3
4
5
6
7
Optional<Integer> possible = Optional.of(5);
System.out.println(possible.isPresent());
System.out.println(possible.get());
 
Output:
true
5

4) 응용 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Integer a = 10;
Integer b = null;
 
Optional present = Optional.fromNullable(a);
Optional absent = Optional.fromNullable(b);
 
System.out.println("a is present : " + present.isPresent());
System.out.println("b is present : " + absent.isPresent());
System.out.println("a value : " + present.or(0));
System.out.println("b value : " + absent.or(0));
 
Output:
a is present : true
b is present : false
a value : 10
b value : 0

http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/base/Optional.html

https://code.google.com/p/guava-libraries/wiki/UsingAndAvoidingNullExplained#Optional

요건 Wrapper 메소드 형식으로 Optional 객체를 반환하게끔 사용하면 좋을 것 같네요.

3. Guava의 Preconditions 또는 NullPointerTester 클래스 이용하기
http://google-collections.googlecode.com/svn/trunk/javadoc/com/google/common/base/Preconditions.html#checkNotNull(T)
http://guava-libraries.googlecode.com/svn-history/r144/trunk/javadoc/com/google/common/testing/NullPointerTester.html

Nullness Annotation 활용하기

메소드의 return값 또는 파라미터의 null 허용여부를 Annotation을 이용하여 지정합니다. 코딩 습관 들이기 3, 4번 항목을 제약하기 위해 사용하는데요.

IDE에서 지원하는 @Nullable, @NotNull 등의 Annotation을 사용하면 코드에서 NPE 발생 가능여부를 미리 경고해줍니다. 또 해당 Annotation 제약사항을 위반했을 경우 컴파일시 NPE가 아닌 IllegalArgumentException 또는 IllegalStateException이 발생됩니다. Argument로 NotNull이어야 한다고 정했는데 Null이 들어왔다면 의미상 NPE보다는 잘못된 Argument가 맞겠죠?

현재 ItelliJ, Eclipse IDE 및 Find Bugs에서는 서로 다른 Annotation 라이브러리를 사용하는데요. JCP에 Software Defect Detection하기 위한 Annotation이 JSR-305로 요청 중이나 현재 dormant(중단) 상태네요. 언제 JDK에 포함될런지는 모르겠네요 :’(

상세 사용법은 아래를 참고하세요.

1. Eclipse (Juno 이상)
http://help.eclipse.org/juno/index.jsp?topic=%2Forg.eclipse.jdt.doc.user%2Ftasks%2Ftask-using_null_annotations.htm

2. IntelliJ
http://www.jetbrains.com/idea/documentation/howto.html

3. FindBugs
http://findbugs.sourceforge.net/manual/annotations.html

4. IntelliJ 예제
– 프로젝트에 /REDIST/annotations.jar 추가
– Project Settings > Inspections > Probable bugs 체크 > Constant conditions & exception 체크 > Sueggest @Nullable annotation …. 체크

1
2
3
4
5
6
7
8
9
10
11
12
@Nullable
private String getDefinition() {
   // ...
}
 
public void doSomething() {
   if (getDefinition().equals("DEFINITION")) {
      // ...
   }
 
   // ...
}

@Nullable인 getDefinition()는 NPE가 발생할 수 있다.’고 IntelliJ에서 경고합니다.

1
2
3
4
5
6
7
8
9
10
private User findUser(@NotNull final String id) {
   // ...
}
 
public void doSomething() {
   // try invoking with null parameter
   User user = findUser(null);
 
   // ...
}

@NotNull 사용한 곳에서 null 값을 전달하고 있다고 IntelliJ에서 경고합니다.

기타

1. DB 테이블 컬럼 생성시 default값 설정하기
사용자로부터 입력 받은 값 중 null을 허용하더라도 default값을 설정했다면,
추후 해당 테이블에서 값을 불러와 사용할 경우 null을 예방할 수 있겠죠.

2. Null Object Pattern, Factory Pattern 사용하기

3. Spring MVC에서 지원되는 Bean Validation에서 @NotNull 사용하기 (JSR-303)

결론

1. 개발자가 코딩시 주의하여 NPE를 발생시키는 코딩 습관은 자제한다.
2. 간단한 Apache Commons나 Guava Library의 null safe 관련 클래스를 학습한 후 적용한다.
3. IDE에서 지원하는 Annotation 등을 이용해서 NPE 발생 부분은 미리 캐치하여 예방한다.

# 참고 사이트
http://javarevisited.blogspot.kr/2013/05/ava-tips-and-best-practices-to-avoid-nullpointerexception-program-application.html
http://howtodoinjava.com/2013/04/05/how-to-effectively-handle-nullpointerexception-in-java/
http://isagoksu.com/2009/development/java/how-to-avoid-nullpointerexceptions-npe/
https://code.google.com/p/guava-libraries/wiki/UsingAndAvoidingNullExplained
http://www.scottlogic.com/blog/2013/09/09/nullable-in-kepler.html