Null을 안전하게 사용하기, Optional 올바르게 사용하기
Null을 왜 사용하지 말아야 할까?
Null의 개념을 최초로 도입한 토니 호어(Tony Hoare)는
null 참조를 만든 것은 Billion-dallor mistake(1조 짜리 실수)라고 했습니다.
Null을 사용함으로 인해, 무수한 에러들이 발생했기 때문입니다.
위 그래프는 안드로이드 앱의 결함 원인 통계입니다. NullPointer 에러로 인한 결함이 2위이고, Native crash 원인 내부에도 NullPointer로 인한 결함이 있을 수 있다고 합니다.
Null을 반환하는 API가 있다고 하면, 그것을 사용하는 사람이 꼼꼼하게 Null 가능성에 대해 점검을 해야 합니다. 하지만 모든 객체에 대해 Null 가능성을 점검하는 일은 쉽지 않습니다.
두 가지 방법으로 이를 해결할 수 있는데요, (더 많은 방법이 존재하긴 합니다.)
- Null을 안전하게 다루기
- Optional을 사용하기
하나씩 살펴보겠습니다.
Null을 안전하게 다루기
Null을 반환하지 말자.
- null 반환 대신 예외 발생
- Collection의 경우 emptyList, emptySet 등 사용
- Null 객체를 구현 (Null object pattern 사용 / 참고 링크(클릭))
- Optional 반환 → 아래에서 알아볼 예정
Null의 범위를 지역에 제한하라.
- 부득이하게 null을 사용해야 한다면, 지역 변수(메서드, 생성자 내부)로만 사용한다.
- 불가피하다면 클래스 영역에서만 사용한다.
초기화를 명확히 하라.
- 실행 시점에 초기화되지 않은 필드가 없게 하라.
- 실행 시점에 null인 필드는, 초기화되지 않았다는 의미가 아니라, 값이 없다는 의미여야 한다.
그 외에도 많은 내용들이 있지만, 개인적으로 와닿고 이해되는 부분을 정리해보았습니다.
Optional 올바르게 사용하기
Optional은 null일 수도 있는 값을 wrapping한 클래스입니다.
Optional 사용의 기본 원칙
1. Optional은 클래스의 필드로 사용하면 안 됩니다.
정확히는 사용할 수는 있으나, 설계 목적에 반하는 사용입니다.
Optional은 반환 값이 null일 수도 있음을 알려주기 위해 만들어졌습니다.
그래서 데이터를 직렬화(Serialize)할 수가 없습니다.
2. Optional은 메서드, 생성자의 인자로 사용하면 안 됩니다.
메서드나 생성자에서는 Optional을 받든 아니든 null 체크를 반드시 하는 것이 안전합니다.
굳이 비용이 비싼 Optional을 사용할 필요가 없습니다.
정리하면, Optional은 반환 타입으로만 사용하는 것이 옳습니다.
Optional 메서드
Optional에는 다양한 메서드가 존재합니다.
그에 대해 간단히 용법을 설명해보겠습니다.
- empty() : null을 담고 있는 Optional 인스턴스를 생성합니다. (Optional 내부적으로 미리 생성해놓은 싱글톤 인스턴스입니다.)
- of(T value) : Optional 인스턴스를 생성해, 인자로 받은 값을 저장합니다. null이 입력되면 NPE(NullPointerException)이 발생합니다.
- ofNullabe(T value) : of와 동일하나, null이 입력될 수 있습니다.
- get() : 값이 있다면 반환하고, 없다면 NoSuchElementException이 발생합니다. 그냥 안 쓰는 것이 좋습니다.
- orElse(T value) : 값이 있다면 반환하고, 없다면 orElse() 메서드 내부의 값을 반환합니다.
- orElseGet(Supplier<? extends T> other) : 값이 있다면 반환하고, 없다면 인자로 받은 Supplier를 실행합니다. 함수를 실행한다는 뜻입니다. (orElse의 lazy한 버전이라고 생각해주시면 됩니다.)
- orElseThrow(Supplier<? extends X> exceptionSupplier) : 값이 있다면 반환하고, 없다면 Supplier로 넘어온 예외를 던집니다.
- isPresent() : 값이 있다면 true, 없다면 false를 반환합니다.
- isEmpty() : isPresent()와 반대입니다. 값이 없을 때 true를 반환, 있다면 false를 반환합니다.
- ifPresent(Consumer<? super T> consumer) : 값이 존재하는 경우 consumer 로직을 실행합니다.
Tip1. orElse(new Object()) 보다, orElseGet(() → new Object())를 사용한다.
orElse(new Object()) 를 사용하는 경우, orElse에 도달하기 전에 new Object() 명령이 실행됩니다.
하지만, orElseGet(() → new Object())의 경우 orElse에 도달하는 경우 즉, Optional 내부에 값이 없는 경우에만 Supplier가 실행되므로 객체 생성 비용을 아낄 수 있습니다.
Tip2. 단지 값을 얻을 목적이라면 Optional 대신 null을 비교한다.
// X
return Optional.ofNullable(status).orElse(READY);
// O
return status != null ? status : READY;
Tip3. primative 자료형의 경우 OptionalInt, OptionalLong, OptionalDouble을 사용한다.
자료형을 다시 Boxing, Unboxing하지 말고, 이미 준비된 클래스를 사용하는 것이 좋습니다.
Optional의 활용
Optional은 0~1개의 원소를 가지고 있는 Stream으로 생각할 수 있습니다.
직접 구현, 상속 관계가 있는 것은 아니지만, 사용 방법이나 사상이 유사하기 때문입니다. (함수형 인터페이스 구조)
따라서 map(), flatmap(), filter() 등의 메서드를 사용할 수 있습니다.
public Optional<Name> getNameIfLengthOverStandard(Player player, int standard) {
return Optional.ofNullable(player)
.filter(player -> player.getName().length() > standard)
.map(Order::getName);
}
위처럼, 특정 조건을 만족하는 경우에는 값을, 아닌 경우 null을 담고 있는 Optional을 반환할 수도 있습니다.
과정에서 map()을 통해 Player의 필드인 Name으로 자료형을 치환했습니다.
Null의 위험성과 올바르게 사용하는 방법,
그리고 Optional을 통해 Null-safe한 코드를 작성하는 방법을 알아보았습니다.
Optional은 생성 비용이 비싸므로, 최후의 수단으로 사용하는 것이 좋다고 하네요.
이 점 공유드리며, 글을 마치겠습니다.
긴 글 읽어주셔서 감사합니다.
참고 자료
- null 안전하게 다루기 : https://www.slideshare.net/gyumee/null-142590829
- Optional 바르게 사용하기 : https://homoefficio.github.io/2019/10/03/Java-Optional-바르게-쓰기/
- Optional을 왜 사용하는가 : https://velog.io/@hope1213/Optional은-왜-사용하는지-사용시-주의사항
- Optional을 Optional답게 사용하기 : https://www.daleseo.com/java8-optional-effective/