자바 개발자라면 NullPointerException
상황을 많이들 겪어보셨을 겁니다. NullPointerException
이란, 참조하는 값이 null일 경우 발생하는데, 일반적으로 null
이란 값은 값의 없음이란 용도로 흔히 사용되고 있습니다.
개발을 하면서 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는 발생하지 않겠지만 발생하는 문제는 다음과 같습니다.
이러한 부분들을 해결하기 위해 어떤 것들을 이용할 수 있는지 해결 방안을 통해 알아보겠습니다.
apache.commons 라이브러리에는 다음과 같은 null 관련 조건문을 다루고 있습니다. 저 또한 해당 객체들에 대해 null 및 빈 값을 체크하는 로직으로 깔끔하게 사용할 수 있어 자주 애용하는 로직입니다.
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 출력
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);
}
}
Coffee coffee = new Coffee();
Integer quantity = 0;
if (coffee.getSugar() != null) {
quantity = coffee.getSugar().getQuantity();
}
=> 이것도 괜찮은 코드지만 Optional
을 이용할 경우 null을 체크하는 코드가 불필요해집니다.
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();
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);
}
}
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이 비어있을 경우, 에러를 던집니다.
오늘은 null을 핸들링할 수 있는 몇 가지 방법에 대해 알아보았습니다. 읽는 것도 좋지만 하나 둘씩 직접 사용해보면서 감을 익혀보시는게 가장 좋을 것 같습니다.
저 또한 Optional에 대해 알고는 있지만 많이 사용해보지 않았었는데 이번 기회에 한 번 사용해보도록 노력해보겠습니다.
감사합니다.
Handling Nulls in Java with Optionals