Handling Nulls in Java

겔로그·2022년 9월 28일
0

Java

목록 보기
7/10
post-thumbnail

개요

자바 개발자라면 NullPointerException 상황을 많이들 겪어보셨을 겁니다. NullPointerException이란, 참조하는 값이 null일 경우 발생하는데, 일반적으로 null이란 값은 값의 없음이란 용도로 흔히 사용되고 있습니다.

개발을 하면서 null로 인해 보편적으로 발생하는 문제들은 다음과 같습니다.

null로 인해 발생하는 문제

  1. null은 무의미한 값입니다. NPE가 발생했을 경우 이것이 의도적인 건지,알고리즘 로직상 문제가 있는 것인지 확인하기 어렵습니다.
  2. 불필요한 null 체크 로직은 코드 로직을 더욱 복잡하게 만듭니다.
  3. Java는 null 체크를 컴파일간 실행하지 않기 때문에 런타임간 갑자기 발생하는 NPE로 인해 짜증이 몰려옵니다.
  4. null은 누락된 값이 허용되는지 여부 등 사용 의도에 대한 정보를 제공하지 않습니다.

자바 8 버전 이후, 이런 NPE를 방지하는 개념들이 하나 둘씩 나오기 시작했습니다. 이번 시간에는 NPE를 방지하는 방법에 대해 알아보고 어떠한 것들이 있는지, 어떻게 사용하는지 확인해 보겠습니다.

예시

다음 코드를 함께 보시죠

public Double getLoanAmountOfStudent(Student student){
  return student.getAccount().getLoan().getAmount();
}

함수를 계속해서 호출하는 로직입니다. 이 때 호출하는 메소드 중 하나라도 결과값이 null일 경우, 이는 곧 연속적인 NPE를 발생시킬 것입니다.

따라서 null을 방지하기 위해 다음과 같은 로직으로 개선하였습니다.

public Double getLoanAmountOfStudent(Student student) {
   if (student != null) {
     if (student.getAccount() != null) {
       Account account = student.getAccount();
       if (account.getLoan() != null) {
         Loan loan = account.getLoan();
         if (loan.getAmount() != null) {
           return loan.getAmount();
         }
       }
     }
   }
   return 0d;
 }

NPE는 발생하지 않겠지만 발생하는 문제는 다음과 같습니다.

  • 코드 로직이 더러워집니다.
  • 조건문을 하나라도 까먹으면 똑같이 NPE가 발생합니다.

이러한 부분들을 해결하기 위해 어떤 것들을 이용할 수 있는지 해결 방안을 통해 알아보겠습니다.

해결 방안

1. apache.commons 이용

apache.commons 라이브러리에는 다음과 같은 null 관련 조건문을 다루고 있습니다. 저 또한 해당 객체들에 대해 null 및 빈 값을 체크하는 로직으로 깔끔하게 사용할 수 있어 자주 애용하는 로직입니다.

  • Collections.isEmpty()
  • MapUtils.isEmpty()
  • StringUtils.isEmpty()

2. Objects::nonNull in Streams

stream 내에서 null이 발생할 경우, 어디서 발생한 에러인지 정확하게 알 수 없다는 문제가 발생합니다. 해당 상황을 방지하고자 Stream 호출 후 바로 nonNull을 이용해 null을 필터링 합니다.

final var list = Arrays.asList(1, 2, null, 3, null, 4);
list.stream()
        .filter(Objects::nonNull)
        .forEach(System.out::print);
// => 1234 출력

4. requireNonNull methods of java.util.Objects

Objects Null 관련 메소드

  • requireNonNull(): 명시된 객체에 대해 null 여부를 판단하고 null일 경우 custom한 NPE 발생
  • requireNonNullElse(): null이 아닌경우 두 번째 인 반환;
  • requireNonNullElseGet(): null이 아닐 경우 인자 반환, 그렇지 않을 경우

해당 메소드들을 통해 객체 null 여부에 따른 값 설정을 할 수 있습니다. 이는 주로 생성자 클래스에서 이용합니다.

사용 예시

@ToString
@Getter
@Setter
@Builder
@NoArgsConstructor
public class Student {

    private Long id;
    private String name;
    private List<SchoolClass> classes;
    private Map<Integer, Teacher> teacherMap;

    public Student(Long id, String name, List<SchoolClass> classes, Map<Integer, Teacher> teacherMap) {
        this.id = Objects.requireNonNull(id, "id is required");
        this.name = Objects.requireNonNullElse(name, "hayley");
        this.classes = Objects.requireNonNullElseGet(classes, ArrayList::new);
        this.teacherMap = Objects.requireNonNullElseGet(teacherMap, HashMap::new);
    }
}

5. Optional 이용

Optional이란?

  • 객체를 감싸고 null 객체 참조 핸들링을 제공하는데 사용

null check code

Coffee coffee = new Coffee();
Integer quantity = 0;
if (coffee.getSugar() != null) {
  quantity = coffee.getSugar().getQuantity();
}

=> 이것도 괜찮은 코드지만 Optional을 이용할 경우 null을 체크하는 코드가 불필요해집니다.

Optional 생성 방법

  • of(T value) : null이 아닌 객체의 Optional 을 인스턴스화 합니다. null 객체를 of()를 사용해 인스턴스화하면 NullPointerException이 발생한다는 점에 유의하시길 바랍니다.
  • ofNullable(T value) : null일 수 있는 객체에 대한 Optional을 인스턴스화합니다
  • empty() : null 객체를 나타내는 Optional을 인스턴스화합니다

초기화 예시

// example using Optional.of(T Value)
String name = "foo";
Optional<String> stringExample = Optional.of(name)
// example using Optional.ofNullable(T Value)
Integer age = null;
Optional<Integer> integerExample= Optional.ofNullable(age)
// example using Optional.empty()
Optional<Object> emptyExample = Optional.empty();

Optional 대표 기능

  • isPresent(): Optional 객체의 null 여부 확인
  • get(): Optional의 값을 가져옵니다. null로 인스턴스화한 Optional 객체는 get()호출시 NPE가 발생합니다.

-> 보통 Optional을 사용하는 개발자들은 isPresent()get()의 조합을 자주 사용합니다. 하지만 map에는 다음과 같은 기능이 존재합니다.

public class Student {

    private Long id;
    private String name;
    private List<SchoolClass> classes;
    private Map<Integer, Teacher> teacherMap;

    public Student(Long id, String name, List<SchoolClass> classes, Map<Integer, Teacher> teacherMap) {
        this.id = Objects.requireNonNull(id, "id is required");
        this.name = Objects.requireNonNullElse(name, "hayley");
        this.classes = Objects.requireNonNullElseGet(classes, ArrayList::new);
        this.teacherMap = Objects.requireNonNullElseGet(teacherMap, HashMap::new);
    }
}

Optional 추가 기능

  • map(Function<? super T,? extends U> mapper): Optional에 포함된 값을 제공되는 함수에 알맞게 변환합니다. Optional 객체가 비어있을 경우 Optional.empty()를 이용합니다.

  • orElse(T other): get()메소드와 동일한 기능으로 Optional 객체에 포함된 값을 가져오는 기능을 하지만, Optional이 비어있을 경우 other로 정의한 값을 가져옵니다.(default value를 정의할 수 있음)

  • orElseThrow(Supplier<? extends X> exceptionSupplier): orElse와 비슷한 기능이지만 Optional이 비어있을 경우, 에러를 던집니다.

Optional 장점

  • null 체크의 추상화
  • null 객체 를 처리하는 API 제공
  • 어떻게 달성할 것이 아닌 무엇을 이룰 것인지 표현하는 선언적 접근을 가능하게 함

결론

오늘은 null을 핸들링할 수 있는 몇 가지 방법에 대해 알아보았습니다. 읽는 것도 좋지만 하나 둘씩 직접 사용해보면서 감을 익혀보시는게 가장 좋을 것 같습니다.

저 또한 Optional에 대해 알고는 있지만 많이 사용해보지 않았었는데 이번 기회에 한 번 사용해보도록 노력해보겠습니다.

감사합니다.

Reference

Handling Nulls in Java with Optionals

Checking for Nulls in Java? Minimize Using “If Else”

Effective Use of Optionals in Java

profile
Gelog 나쁜 것만 드려요~

0개의 댓글