[JAVA] 자바 null 박살내기
개발자들의 영원한 적, null의 탄생
개발자들에게 프로그래밍 하면서 자신을 가장 괴롭히는 것을 몇 가지 꼽아보라 한다면, 보통 그 리스트에 null
처리는 반드시 들어가게 될 것이다. 그만큼 null
은 개발자를 괴롭히는 적이며, 그들은 이 null
을 처리하기 위해 끝이 없어보이는 힘겨운 싸움을 하게 된다.
null
은 컴퓨터 과학자인 토니 호어(Tony Hoare)가 알골 W(ALGOL W)라는 프로그래밍 언어를 설계하면서 고안한 개념이다. 당시 토니 호어는 알골 W라는 객체 지향 프로그래밍 언어를 설계하면서, 컴파일 타임에 참조 오류를 잡아내 완벽하게 안전한 참조의 사용을 보장할 수 있는 프로그래밍 언어를 목표로 설계했다. 그러나 토니 호어는 컴파일러를 구현하기 쉽다는 이유로 null
참조자를 도입하려는 유혹을 버리지 못했다고 한다. 이는 10억불 짜리 실수라며, 토니 호어 본인이 2009년에 직접 회고한 적이 있다.
자바 개발자는 NullPointerException이 싫다
자바 개발자라면 누구나 NullPointerException
(이하 NPE)을 겪어봤을 것이다. 이 예외는 null
참조자가 할당된 객체의 필드나 메소드에 접근하려고 하면 발생한다. 자바는 Null Safety
하지 않은 언어이기 때문에, 언제 어디서 null
이 발생할지 예측하기 힘들다. 따라서 개발자가 자바로 프로그래밍 할 때, NPE
는 거의 숙명이나 다름없다.
Null Safety
언어란, null
참조자로 인해 발생할 수 있는 문제들을 원천 봉쇄하기 위한 타입 선언을 사용하는 언어들을 의미한다. 대표적으로 TypeScript
나 Kotlin
과 같은 언어들이 있다.하지만 숙명이라고 해서, 방치할 수 만도 없는 노릇이다. 따라서 자바 개발자는 항상 null
이 들어올 것을 대비해 방어적으로 프로그래밍 해야만 한다.
NPE를 줄이는 방법
Null Safety
하지 않은 언어들에서 null
참조자는 항상 골칫거리다. 그래서 이런 언어들은 null
참조자를 처리하는 방법이 존재하고 또 비슷한 편이다. 여기서 말하는 null
방어법은 대부분의 언어에서도 통용되는 얘기지만, 자바에서만 사용할 수 있는 얘기도 나올 것이다.
1. null을 매개변수로 넘기지 말 것
가장 기본적인 것이다. 대부분의 메소드들은 인수가 null
일 것을 고려하고 설계하지 않는다. 그럴 바에는 차라리 오버로딩으로 여러 개의 메소드를 만드는 것이 더 현명하기 때문이다. 설령 오버로딩을 지원하지 않는 언어라 하더라도, 대부분의 최신 고급 언어는 Default Argument
, Named Argument
를 지원하는 경우가 많다. 따라서 메소드를 호출할 때는 항상 NPE
의 가능성을 두고 매개변수를 전달해야 한다.
// BAD
public void methodA() {
methodB(null);
}
// GOOD
public void methodA() {
methodB("Hello!");
}
public void methodB(String message) {
// message가 null이면 NPE 발생
String newMessage = message.concat(", World!");
System.out.println(newMessage);
}
2. 반환 값으로 null을 반환하지 말 것
반환 값이 존재하는 메소드 중에는 간혹가다 마지막에 null
이 반환되는 것들이 있다. 이것은 반환할 변수에 마땅히 대입될 만한 값이 없어, 처음 초기화 시 대입된 null
참조자가 그대로 반환 되는 경우가 많다. 그런데 개발자 입장에서 메소드가 null
을 반환하는 경우는 그다지 달가운 상황이 아니다. 그 이유는 메인 로직에 null
값이 아님을 검증해야 하는 검증 작업을 추가로 작성해야하는 번거로움이 발생하기 때문이다. 또한 책임 측면에서 볼 때, 하나의 메소드에서 발생한 책임을 다른 곳으로 떠넘기는 것은 전형적인 안티 패턴이다. 이런 경우에는 null
대신 빈 객체를 반환하는 것이 좋다.
위와 같이 메소드를 작성하면, 호출부에서 다음과 같이 코드를 작성할 수 있다.
만약 일반적인 참조형 변수라서 빈 값을 생성할 수 없다면, 별도의 두 가지 선택지가 존재한다. 하나는 예외를 던지는 것이고, 다른 하나는 Optional.empty()
를 반환하는 것이다. Java8
이상을 쓰고 있다면 후자를 권장한다.
// BAD
public void methodA() {
// 호출자는 이 메소드가 null을 반환하는지 알기 어렵다...
String message = methodB();
}
public String methodB() {
return null;
}
// GOOD
public void methodA() {
Optional<String> wrappedMessage = methodB();
}
public Optional<String> methodB() {
return Optional.empty();
}
Optional
을 사용하는 것은 한가지 소소한 장점을 주는데, 그건 해당 메소드가 null
을 포함한 래퍼 클래스를 반환할 가능성이 있다는 점을 명시적으로 알려줄 수 있다는 것이다. 이는 실제로 Otional
래퍼 클래스 정의를 설명한 글에도 나와있는 내용이다. Optional
래퍼 클래스는 내부의 값이 null
인지 검증하고 후속조치를 취하는 다양한 방법을 제공하므로, 호출자가 편하게 null
을 처리할 수 있도록 도와준다.
3. 객체에 접근하기 전에는 null 체크부터
다른 개발자가 작성한 소스를 사용하거나 외부 라이브러리를 경우에는 1, 2번을 모두 준수해서 코드를 작성했을 것이라 확신할 수 없다. 따라서 이 경우에는, 메인 로직에서 null
검증 작업을 거쳐야 한다. 다음 코드를 보자.
// BAD
public void updateUser(User user) {
// user가 null이면 NPE 발생.
Phone phone = user.getPhone();
}
// GOOD
public void updateUser(User user) {
if (user != null) {
Phone phone = user.getPhone();
}
}
사실 이정도는 자바 개발자 초보라도 다들 해본적이 있을 것이다. 그런데 User
객체 내부의 Phone
객체의 접근하려고 한다면 어떻게 될까? 아마 코드가 다음과 같이 바뀌게 될 것이다.
public void updateUser(User user) {
if (user != null) {
if (phone != null) {
String phoneNumber = user.getPhone().getNumber();
}
}
}
if
문을 이용한 null
검증은 중첩 객체(객체 내부에 또 다른 객체가 존재하는 구조)인 경우 분기문의 깊이가 깊어지는 문제가 존재한다. Java8
이상을 사용중 이라면, 이 문제를 해결하기 위해 Optional
을 사용할 수 있다. Optional
을 사용하면 다음과 같이 코드를 변경할 수 있다.
만약 Optional
을 사용할 수 없다면... 그때는 안타까운 일이지만, 일일이 검증을 하거나 예외를 던지는 수 밖에는 없다.
번외. 자바 대신 코틀린을 사용하자
웃기려고 하는 소린가 싶겠지만, 이것은 꽤나 진지한 얘기다. 우선 코틀린이 Null Safety
한 언어라는 사실은 위에서 이미 언급한 사실이다. 코틀린 언어의 특징은 변수와 반환 값의 타입에 null
허용 여부를 명시적으로 지정할 수 있다는 점이다. 이는 자바에서 코틀린 코드로 작성된 메소드를 호출할 때도 어느정도 효과를 볼 수 있다. 코틀린으로 작성된 메소드의 인수가 null
이 허용되지 않는 타입일 경우, 해당 메소드를 호출한 자바 코드에서 null
을 매개변수로 넘기면 NPE
가 발생한다. 로직상 발생한 NPE
와 차이 없는 것이 아니냐고 생각할 수 있다. 하지만 로직 내부에서 발생한 NPE
와는 달리, 인수가 null
이어서 발생한 NPE
는 어디서 발생했는지 바로 알 수 있다. 따라서 코틀린으로 작성한 코드는 자바나 코틀린 어디서나 사용해도 null
로부터 어느정도 안전성을 보장받을 수 있다.
문제라면 한국은 안드로이드/개인/소규모 개발자들을 제외하면 대부분 자바를 사용할 수 밖에 없는 환경이라는 점이다. 이런 경우에는 어쩔 수 없지만 코틀린 도입을 깔끔하게 포기하거나, 일부 라이브러리만 코틀린으로 개발해 메이븐 의존성으로 추가하는 방법밖에 없을 것이다.
정리하며
이번 글은 자바 언어로 프로그래밍 하면서 발생할 수 있는 NPE
회피 방법에 대한 내용으로 작성해 보았다. 원래는 대부분의 언어에서 사용할 수 있는 방법론에 대해서만 다루려고 했으나, 자바의 Otional
래퍼 클래스가 NPE
를 해결하기 위한 유용한 도구이기 때문에 부득이하게 같이 다루게 되었다. 이번 글의 주제는 null
을 처리하는 방법에 대해 다루는 글이었기 때문에 Optional
에 대한 깊은 이야기는 다루지 않았다. 추후 시간이 나면 Optional
래퍼 클래스에 대해 심도있는 글을 추가로 작성해 보도록 하겠다.