목차
NullPointerException 이란?
NullPointerException 은 null을 참조하는 변수에 접근할 때 뜨는 에러이다. Java 뿐만 아니라 다른 많은 언어에서도 이와 비슷한 형태의 에러가 있으며, 전반적으로 비슷한 개념이라고 생각하면 된다.
NullPointerException 은 왜 뜨는가?
정의에서 언급됐듯이, 이 에러는 변수에 접근하는 특정 상황에서 발생한다. 먼저 변수에 대해 알아보자.
변수에는 크게 두 가지 타입이 있다.
두 가지 형태의 변수
Primitives(기본형/원시형)
데이터를 저장하는 변수이며 직접적으로 데이터를 변경할 수 있다. 흔히 소문자로 시작하는 데이터 타입을 가지고 있다(e.g., int, char etc).
아래 코드를 보자.
int num;
int num2 = num + num;
num 이 정의되지 않은 상태에서 num의 값을 통해 num2를 정의하려고 한다. 모든 변수는 사용되기 전에 정의되어야 하므로 이 코드는 깨질 것이다. 그러나 다음 형태의 변수에서는 좀 더 복잡하지만 흥미로운 일이 발생한다.
References(참조형)
어떠한 Object의 메모리 주소를 저장하는 변수이다. Object를 변경하려 한다면 직접적으로 할 수 없고 역참조(dereference) 해야 한다. 주로 . 혹은 [](indexing) 을 사용해서 역참조 할 수 있다. 일반적으로 대문자로 시작하는 데이터 타입을 가지고 있다(e.g., Object etc).
사용하기 전에는 항상 정의가 되어야 하는 기본형 변수와는 달리, 참조형 변수는 null로 정의될 수 있다. null로 정의를 하는 것은, 해당 변수가 아무것도 참조하지 않는다는 뜻이다. 개발자가 변수를 정의하지 않고 넘어가면 자동으로 null로 세팅되고, 직접적으로 세팅하는 것도 가능하다.
만약 참조형 변수를 정의를 하지 않았거나, 직접 null로 세팅을 해서 변수가 아무것도 참조하고 있지 않은 상태일 때, 해당 변수를 역참조 하려고 하면 NullPointerException 이 뜨게 된다. 아무것도 참조하지 않는 변수에 무엇을 참조하는지에 대한 정보를 요청할 때 뜨는 에러인 것이다.
다음 코드를 통해 이해를 돕고자 한다.
Integer num;
num = new Integer(10);
첫 째 줄에서 num 은 정의되지 않았다. 그 말인즉슨, 첫 째 줄에서의 num 은 null을 참조하는(아무것도 참조하지 않는) 상태이다.
둘째 줄에서는 num 이 새로운(new) Integer을 참조하도록 하고 있다. 둘째 줄부터는 num 은 어떤 것을 참고하고 있는 상태이다.
첫 째 줄의 상황에서 num을 역참조 하려 한다면, 아무것도 참조하지 않는 변수에 무엇을 참조하냐고 묻는 게 되어서, NullPointerException(NPE) 이 뜨게 되는 것이다.
하지만 이런 기본적인 상황에서 뜨는 NPE는 컴파일러가 우리에게 경고를 쉽게 줄 수 있기 때문에 큰 문제가 되지 않는다. 진짜 문제는 다음과 같은 상황에서 발생한다.
직접 생성하지 않는 변수
다음과 같은 메서드가 있다고 하자.
public void doRandomThing(RandomObject obj) {
obj.myMethod();
}
obj에 어떤 작업을 하며 obj 가 null 이 아니라는 가정을 한 상태에서 쓴 코드이다. 그런데 다음과 같이 메서드를 부르는 것도 가능하다.
doRamdomThing(null);
이때에는 obj 가 null 일 것이기에, NullPointerException 이 뜨게 된다. 변수를 직접적으로 생성하는 것도 아니기에 컴파일러에서 쉽게 걸러지지도 않는다.
NullPointerException 은 어떻게 예방할까?
지금까지 NPE 가 뭐고, 왜 뜨는지에 대해서 알아봤다. 그럼 이제 어떻게 예방하는지에 대해 알아보자.
에러 메시지로 문제 파악하기
만약 방금 본 예시처럼 메서드가 passed-in 된 변수에 대해 어떤 작업을 할 때, 그 변수가 null 이면 NPE 가 뜨는 것은 당연하다. 엄밀히 따지면, 우리가 예방해야 할 것은 NPE 가 뜨는 것이 아니라 null을 참조하는 변수를 역참조하는 행위이다. 따라서 NPE 가 떴을 때 최대한 그 문제의 원인을 잘 파악해서 null을 역참조 하지 않도록 코드를 짜는 게 중요하다.
이를 도와주는 방법 중 하나가 메서드가 시작될 때 passed-in 된 변수를 점검하는 것이다. 다음 코드를 메서드의 시작 부분에 추가한다면, 변수가 null 일 때 NPE를 보냄과 동시에 우리가 원하는 에러 메시지를 출력한다.
Objects.requireNonNull(obj, "obj는 null 이 되어선 안됩니다.");
지혜롭게 에러 메시지를 작성함을 통해 NPE 가 떴을 때 빠르게 대처할 수 있다. 팁은 에러 메시지에 정확히 어떤 변수가 null 이 되어선 안되는지를 명시하는 것이다. 이를 통해 명확하게 문제를 일으키는 변수를 구별할 수 있고, 한 번 점검이 된 변수는 다시 정의되기 전까지는 NPE를 띄우지 않을 것이라 확신할 수 있다.
case 구분과 도큐먼트 작성
만약 메서드가 passed-in 된 변수에 작업하는 것만이 목적이 아닐 수 있다. 어떨 때는 그 변수를 사용하고 어떨 때는 아니라면, 그 변수는 null 이 되어도 괜찮은 케이스들이 있는 것이다. 이럴 때는 변수가 null 일 때와 아닐 때를 구분해 주면 좋다.
/**
* @param obj 는 null 이 될수도 있습니다. 그 때는 ____ 와 같은 결과가 출력됩니다.
*/
public void doRandomthing(RandomObject obj) {
if(obj == null) {
// 작업 1
} else {
// 작업 2
}
}
변수가 null 일 때와 아닐 때 어떻게 메서드가 변하는지에 대해 도큐먼트를 작성하는 것도 좋은 방법이다.
NullPointerException 이 뜰 수 있는 상황들
- null을 참조하고 있는 객채(instance)에 접근
- null을 참조하고 있는 객체 메서드 부르기
- throw null;
- null array(배열)의 element에 접근
- null에 동기화(synchronized)
- boxed(기본형을 참조형으로 바꿈)된 변수가 null을 참조 중인 경우의 작업
- null을 참조하는 boxed 변수의 unboxing 전환
- 조상 클래스의 null을 참조(super) 하는 것
- foor loop을 사용해 null을 참조하는 collection과 array의 element에 접근하는 것
- switch 구문의 변수 파트에 null을 사용하는 것
- null 인 변수의 객체를 생성하는 것
- name1::name2 형태의 메서드 참조에서 name1 이 null 일 때
마무리
개발하는 환경에 놓여있다 보면 메모리와 포인터의 개념이 중요하게 작용할 때가 많다. 언어를 구성하는 데에 있어서 매우 중요한 개념들 중 하나이기에 기본을 탄탄히 잡고 가면 큰 도움이 될 것이라 생각한다. 기본을 다지고 개발할 때 꼼꼼히 개발하는 습관을 들여 나중에 곪아있던 문제가 터져 개발에 손을 놓고 싶어지는 불상사가 일어나지 않도록 하자! 이 글이 도움이 되었길 바란다.