자바의 null 참조는?
의미가 모호하다.
초기화 되지 않은 상태
정의되지 않은 상태
값이 없는 상태
모든 상태의 기본 값
모든 참조는 null 일수 있다.
소프트웨어 결함 통계
Native Crash 는 Java가 아닌 부분에서 발생한 에러를 뜻합니다.
실제로, Native Crash 내에도 NullPointer 가 존재할 것이기 때문에, 실제 분석된 통계보다 더 많은 것을 위 그림으로 알 수 있습니다.
Java 에서 null을 다루는 방법은 NullPointerException라는 에러가 발생하지 않도록 코드드를 작성하는겁니다.
우선 null 이라는 개념은 언제 누구에 의해 만들어졌을까요?
null 참조는 1965년 Tony Hoare 라는 영국의 컴퓨터 과학자에 의해서 처음 고안되었습니다.
당시 그는 "존재하지 않는 값"을 표현할 수 있는 가장 편리한 값이 null 참조라고 생각했다고 합니다.
하지만,
나중에 그는 당시 자신의 생각이 "10억 불짜리 큰 실수"였고, null 참조를 만든 것을 후회한다고 토로 하였습니다.
(널 포인터는) 내 10억 달러짜리 실수였다. 1965년 당시, 나는 ALGOL W라는 객체 지향 언어에 쓰기 위해 포괄적인 타입 시스템을 설계하고 있었다. 내 원래 목표는 어떤 데이터를 읽든 항상 안전하도록 컴파일러가 자동으로 확인해 주는 것이었다. 그러나 나는 널 포인터를 집어넣으려는 유혹을 이길 수가 없었다. 그렇게 하는 게 훨씬 쉬웠기 때문이다. 이 결정은 셀 수도 없는 오류와 보안 버그, 시스템 다운을 낳았다. 지난 40년 동안 이러한 문제들 때문에 입은 고통과 손해는 10억 달러는 될 것이다.
NPE (NullPointerException)
null 참조로 인해 자바 개발자들이 가장 골치아프게 겪는 문제는 우리 개발자들이 흔히 겪는 NullPointerException 일 것입니다.
Java 라는 언어를 처음 배운사람부터 고급개발자까지 객체를 사용하여 모든 것을 표현하는 우리 개발자에게 NPE 는 코드 베이스 곳곳에 깔려있는 지뢰같은 녀석입니다.
컴파일 타임에서는 조용히 잠복해 있다가, Runtime 때, 펑펑 터지는 NPE의 스택 트레이스에 개발자들은 속수무책으로 당할 수 밖에 없습니다.
null 처리에 관하여 설명할 때 Java8 기준으로 이전 이후로 나뉠 수 있습니다.
Java Version 8 이전
이전에는 NPE의 위험에 노출된 코드를 코딩 스타일로 회피를 하였습니다.
아래는 예시 코드입니다.
if (data != null) {
// 호출
if(data2 != null) {
//호출
}
}
또는,
Member data = register.getMemList();
if(data == null) {
return "initData";
}
위와 같이 두 가지 방법 모두가 기본적으로 객체의 필드나 메소드에 접근하기 전에 null 체크를 함으로 써 NPE를 방지합니다.
하지만, 코드가 상당히 걸어지고 지저분해졌음을 볼 수 있습니다.
이 밖에도 null Object 패턴, 단언문(assertion) 등 NPE 문제를 해결하기 위한 다양한 시도들이 있었지만,
만족할만한 건 없었습니다.
Java Version 8 이후
위에서 Versuon 8 이전에 겪었던 null 처리 문제들을 정리해보겠습니다.
- Runtime 에 NPE라는 예외를 발생 시킬 수 있습니다.
- NPE를 미리 막기 위하여, null 체크를 하는 로직 추가를 하게되면 가독성과 유지 보수성이 떨어집니다.
위와 같은 문제를 그냥 두자니, 클린코드와는 거리가 멀어집니다. 여러 단점들을 가지고 가기에는 또 null 체크를 하지 않는다면 나중에 개발 도중에 곳곳에 숨어 있던, null 이슈가 장애를 유발하게 될 것입니다.
옵셔널을 이용하면서, 위와 같은 코드가 난잡하고 가독성 문제를 어느정도 해결 했다고 할 수 있습니다.
그 외에 몇 가지 방법을 통해 null를 다루는 방법
1. equals() 사용 시,
null 을 갖는 객체를 참조하려고 한다면, NullPointerException 이 발생하기에 아래와 같이 사용하는 게, 안전합니다.
equals() 를 부르는 인스턴스가 null 인 경우에는 익셉션 에러가 발생합니다.
하지만, 아래와 코드에 보이는 것처럼 equals 를 사용할 경우, 비교할 문자열이 equals 메서드를 호출하게 되는 경우이며,
true / false 로 값이 반환됩니다.
Object unknownObject = null;
[X]
unknownObejct.equels("Literal")
[O]
if("Literal".equals(unknownObejct)) {
// TODO
}
2. toString() 보다, valueOf() 를 선호하자.
null을 갖는 객체에서 toString()을 호출하면 NullPointerException 경험이 많을 겁니다.
그렇기에, null을 반환하는 valueOf()를 사용하는 게 좋습니다.
toString()
public static void main(String[] args) {
Integer a = 1;
System.out.println(a.toString());
a = null;
System.out.println(a.toString());
}
/******* 결과 *******/
Exception in thread "main" java.lang.NullPointerException
valueOf()
public static void main(String[] args) {
Integer a = null;
System.out.println(String.valueOf(a));
}
/******* 결과 *******/
null
3. NULL safe Method와 libraries 를 사용하자
- java.util.Obejcts (jdk 7~)
- java.util.Optional (jdk 8~)
- org.springframework.util.StringUtils
- org.apache.commons.lang3.StringUtils
- assert
등등등...
예시
libararies
StringUtils.isEmpty(null);
StringUtils.isBlank(null);
StringUtils.isNumeric(null);
StringUtils.isAllUpperCase(null);
safe Method
public class MetricsCalculator
{
public double xProjection(Point p1, Point p2) {
return (p2.x – p1.x) * 1.5;
}
…
}
public class MetricsCalculator
{
public double xProjection(Point p1, Point p2) {
if (p1 == null || p2 == null) {
throw InvalidArgumentException(
"Invalid argument for MetricsCalculator.xProjection");
}
return (p2.x – p1.x) * 1.5;
}
}
4. null을 반환하는 Method 작성 피하자.
null이 아닌 EMPTY_LIST, EMPTY_MAP 같이 비어있는 것을 표시하는 것을 사용하여 null 반환하지 않도록 합니다.
Collections 클래스에는 아래와 같이 EMPTY_LIST가 정의되어있습니다. 리턴 값이 List일 경우 사용됩니다.
public List getOrders(Customer customer){
List result = Collections.EMPTY_LIST;
return result;
}
추가로, 아래와 같은 메서드 또한 있습니다.
List list = Collections.EMPTY_LIST;
Set set = Collections.EMPTY_SET;
Map map = Collections.EMPTY_MAP;
5. @NotNull 이나, @Nullable 애노테이션을 사용합니다.
method가 null safe인지 아닌 지를 annotation을 사용하여, 표시하는 것이 좋습니다.
그래야 Compiler 가 여러분이 미처 확인하지 못하거나, 또는 굳이 확인할 필요가 없는 부분에서 null Check를 하도록 도울 수 있습니다.
public class Employee {
public final int id;
public final String name;
public final @Nullable String phone;
private Employee(int id, String name, @Nullable String phone) { ... }
}
6. 불필요한 AutoBoxing이나 UnBoxing 코드를 피하자
public class Product {
private Long price;
public Long getPrice() {
return price;
}
}
public void someMethod() {
Product product = new Product();
if (product.getPrice() == 0L) {
System.out.println("price is zero.");
}
}
위와 같은 코드일 때, 상품 가격이 0인지 확인하는 상황 일 경우에, 널포인트 익셉션을 배출하게 됩니다.
getPrice() 메서드가 null 을 반환해서 그렇습니다.
자바에서 Wrapper 와 Primitive 타입 비교하는 경우, Unboxing 이 자동으로 수행되어, Number 클래스의 longValue() 메서드를 호출 함으로 써 수행되는데, 메서드 호출되는 대상 객체가 null 이기 때문에 메서드를 호출하지 못하고 널포인터 익셉션이 발생하게 된다.
아래와 같이 코드를 수정하면 Unboxing 이 일어나지 않고, null == null 처리가 됩니다.
그리고 그냥....Primitivie 타입을 사용하는 게 가장 깔끔하지 않을까요
public void someMethod() {
if (product.getPrice() == Long.valueOf(0)) {
System.out.println("price is zero.");
}
}
Wrapper 타입을 Primitive 타입으로 바꾸는 Unboxing 이 자동으로 수행되는 게 Unboxing 이라고한다.
반대가 Autoboxing 입니다. Primitive -> Wrapper
7. 객체 생성 시 규약을 따르고 합리적인 default 값을 정의하자.
NullPointerException은 대부분 불완전한 정보나 요구되는 의존성을 모두 충족시키지 않고 객체가 생성되었을 때 발생합니다.
그렇기에 이를 피하는 방법만으로도 null을 다룰 수 있습니다.
null을 리턴하지 않고, 0과 같은 default 값을 리턴하도록 해야 합니다.
8. DB에선 null 제약 조건을 유지하자
도메인 객체(Customers, Order와 같은)를 저장하기위해 DB를 사용하는 경우 DB 자체에서 null 제약 조건을 정의해야 합니다. (데이터의 무결성 보장 때문에)
DB에서 이런 제약 조건을 유지하는 것만으로 Java code에서 null check를 줄일 수 있습니다. 이미 DB에서 해당 필드가 null 값을 가질 수 있는지 없는지 확인하기에 Java code 내에서 불필요한 null 체크를 최소화할 수 있습니다.
9. Null Object Pattern을 사용하자
import java.util.Date;
public interface Employee {
public boolean isTimeToPay(Date payDate);
public void pay();
// Employee.NULL은 null 객체
public static final Employee NULL = new Employee() {
public boolean isTimeToPay(Date payDate) {
return false;
}
public void pay() {
// 아무 것도 하지 않는다
}
}
}