Java에서 null을 다루는 방법

null을 다루는 방법

Java에서 null을 다루는 방법이란 흔히 NullPointerException을 피하도록 코드를 작성하는 것입니다.

먼저 null을 창안한 “토니 호어”는 null을 10억 달러짜리 실수라고 말하였습니다.

왜 그렇게 표현하였을까요?

관계가 없음을 나타내는 null은 모든 타입의 멤버가 될 수 있고 이 때문에 참조 변수 사용 시 널을 확인해야 합니다. 그렇지 않으면 NullPointerException을 보게 될 것이고 이는 실제로 SW 결함 통계에서 상당히 많은 부분을 차지하고 있습니다.

  • null 값을 갖는 변수는 JVM 메모리에서 참조하는 값이 없으면 해시 코드는 항상 0입니다. 즉, 힙 영역에 데이터를 생성하지 않았음.

    그럼 이제 몇 가지 방법을 통해 null을 다루는 방법을 알아보겠습니다.

    1. equals() 사용 시

    1
    2
    3
    4
    5
    6
    7
    Object unknownObject = null;

    // unknownObject.equals("StringLiteral") -> wrong way

    if("StringLiteral".equals(unknownObject)) {
    // TODO
    }

    null을 갖는 객체를 참조하려고 하면 NullPointerException이 발생하기에 위와 같이 사용하는게 안전합니다.

    2. valueOf( )를 더 선호하자. (toString( ) 보다)

    null을 갖는 객체에서 toString()을 호출하면 NullPointerException을 보신 경험이 있을 겁니다.(저만 그런가요?) 그렇기에 null을 반환하는 valueOf()를 사용하는게 좋습니다.

    3. null safe methods와 libraries를 사용하자.

    Null safe methods와 classes 관련 documentation을 보는게 가장 정확합니다. (당연한 소리?!)
    Null safe method의 가장 흔한 예가 StringUtils 메소드로 아래처럼 코드를 작성해도 문제가 없습니다.

    1
    2
    3
    4
    System.out.println(StringUtils.isEmpty(null));
    System.out.println(StringUtils.isBlank(null));
    System.out.println(StringUtils.isNumeric(null));
    System.out.println(StringUtils.isAllUpperCase(null));

    4. null을 반환하는 method 작성을 피하자.

    null이 아닌 EMPTY_LIST와 같이 비어있는 것을 표시하는 것을 사용하여 null 반환하지 않도록 합니다.

    1
    2
    3
    4
    public List getOrders(Customer customer){
    List result = Collections.EMPTY_LIST;
    return result;
    }

    5. @NotNull 이나 @Nullable 어노테이션을 사용한다.

    메소드가 null safe인지 아닌지를 annotation을 사용하여 표시하는 것이 좋습니다. 그래야 컴파일러가 여러분이 미처 확인하지 못 한 또는 굳이 확인할 필요가 없는 부분에서 null check를 하도록 도와줄 수 있습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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 코드를 피하자.

    만약 wrapper 클래스 객체가 null이라면, autoboxing은 NullPointerException에 취약할 수 밖에 없습니다.

    다음 코드에서 doyun이라는 객체가 phjone number가 없다면 null을 리턴하지 않도록 해야 합니다. 그렇지 않다면 NullPointerException을 보게 될 것입니다.

    1
    2
    Person doyun = new Person("Doyun");
    int phone = doyun.getPhone();

    7. 객체 생성 시 규약을 따르고 합리적인 default 값을 정의하자.

    NullPointerException은 대부분 불완전한 정보나 요구되는 의존성을 모두 충족시키지 않고 객체가 생성되었을 때 발생합니다.

    그렇기에 이를 피하는 방법만으로도 null을 다룰 수 있습니다. 예를 들어 Employee라는 객체는 id와 name 값이 없으면 생성될 수 없게 하고 옵션으로 phone number 값을 가질 수 있습니다.
    단 여기서 phone number 값이 없다면 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을 사용하자.

  1. 추상 클래스 Employee.java
    1
    2
    3
    4
    5
    6
    7
    public abstract class Employee {
    protected int id;
    protected String name;
    public abstract boolean isNull();
    public abstract int getID();
    public abstract String getName();
    }
  2. 추상 클래스를 확장하는 CurrentEmployee.java (현재 종사하고 있는 직원)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class CurrentEmployee extends Employee {
    public CurrentEmployee(int id, String name) {
    this.id = id;
    this.name = name;
    }

    @Override
    public int getID() {
    return id;
    }
    @Override
    public String getName() {
    return name;
    }
    @Override
    public boolean isNull() {
    return false;
    }
    }
    NullEmployee.java (퇴사하거나 이직한 직원)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class NullEmployee extends Employee {

    @Override
    public int getID() {
    return -1; // default value
    }
    @Override
    public String getName() {
    return "No id in Employee DB";
    }
    @Override
    public boolean isNull() {
    return true;
    }
    }
  3. EmployeeFactory.java (DB에 저장되어 있는 id 조회)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class EmployeeFactory {

    public static final int[] idList = {00001, 00002, 00004};

    public static Employee getEmployee(int id, String name){
    for (int i = 0; i < idList.length; i++) {
    if (idList[i].equalsIgnoreCase(id)){
    return new CurrentEmployee(id, name);
    }
    }
    return new NullEmployee();
    }
    }
  4. NullPatternTest.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class NullPatternTest {
    public static void main(String[] args) {
    Employee employee1 = EmployeeFactory.getEmployee(00001, "Doyun");
    Employee employee2 = EmployeeFactory.getEmployee(00002, "Jieun");
    Employee employee3 = EmployeeFactory.getEmployee(00005, "Doyun");

    // Test
    System.out.println(employee1.getName()); // Doyun
    System.out.println(employee2.getName()); // Jieun
    System.out.println(employee3.getName()); // No id in Employee DB
    }
    }

    이렇게 하면 null 체크를 최소화하여 NullPointerException을 피할 가능성이 높아지고 코드가 간단해집니다.

    다만, 새로운 메소드를 추가할 때마다 Null 객체 클래스에서도 이를 오버라이드해야 하기에 유지보수가 힘들 수도 있습니다.

    이상으로 Null 다루기 편을 마치겠습니다. - 추가할 부분이 있으면 추가하겠습니다.