티스토리 뷰

728x90

자바 개발을 하다보면 피할 수 없는 NPE (NullPointerException)

값이 없는 상황을 어떻게 처리할까?

보수적인 자세로 NullPointerException 줄이기

대부분의 프로그래머는 필요한 곳에 다양한 null 확인 코드를 추가해서 null 예외 문제를 해결하려 할 것이다.

모든 변수가 null임을 체크하는 중첩된 if문을 추가하면 코드 들여쓰기 수준이 증가한다. 이와 같은 반복 패턴 코드를 깊은 의심이라고 부른다. 즉, 변수가 null인지 의심되어 중첩 if 블록을 추가하면 코드 들여쓰기 수준이 증가한다.

null 때문에 발생하는 문제

  • 에러의 근원
    • NullPointerException은 자바에서 가장 흔히 발생하는 에러이다.
  • 코드를 어지럽힌다.
    • 때로는 중첩된 null 확인 코드를 추가해야 하므로 null 때문에 코드 가독성이 떨어진다.
  • 아무 의미가 없다.
    • null은 아무 의미도 표현하지 않는다.
  • 자바 철학에 위배된다.
    • 자바는 개발자로부터 모두 포인터를 숨겼다. 하지만 예외가 있는데 그것이 바로 null 포인터다.
  • 형식 시스템에 구멍을 만든다.
    • null은 무형식이며 정보를 포함하고 있지 않으므로 모든 참조 형식에 null을 할당할 수 있다. 이런 식으로 null이 할당되기 시작하면서 시스템의 다른 부분으로 null이 퍼졌을 때 애초에 null이 어떤 의미로 사용되었는지 알 수 없다.

다른 언어는 null 대신 무엇을 사용하나?

그루비 같은 언어에서는 안전 네비게이션 연산자 (?.)를 도입해서 null 문제를 해결했다. 안전 네비게이션 연산자를 이용하면 null 참조 예외 걱정 없이 객체에 접근할 수 있다.

def carInsuranceName = person?.car?.insurance?.name

Optional 클래스 소개

자바 8은 하스켈과 스칼라의 영향을 받아서 java.util.Optional라는 새로운 클래스를 제공한다. Optional 은 선택형 값을 캡슐화하는 클래스다. 값이 있으면 Optional 클래스는 값을 감싼다. 반면 값이 없으면 Optional.empty 메서드로 Optional을 반환한다. Optional.empty는 Optional의 특별한 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드다.

null 참조는 null을 참조하려고하면 NullPointerException이 발생하지만 Optional.empty()는 Optional 객체이므로 이를 다양한 방식으로 활용할 수 있다.

Optional을 이용하면 값이 없는 상황이 우리 데이터에 문제가 있는 것인지 아니면 알고리즘의 버그인지 명확하게 구분할 수 있다. 모든 null 참조를 Optional로 대치하는 것은 바람직하지 않고 Optional의 역할은 더 이해하기 쉬운 API를 설계하도록 돕는 것이다.

Optional 적용 패턴

Optional 객체 만들기

  • 빈 Optional
  • Optional<Car> optCar = Optional.empty();
  • null이 아닌 값으로 Optional 만들기
    • Optional.of로 null이 아닌 값을 포함하는 Optional을 만들 수 있다.
    • Optional<Car> optCar = Optional.of(car); //car가 null이면 NullPointerException이 발생
  • null값으로 Optional 만들기
    • Optional.ofNullalbe로 null값을 저장할 수 있는 Optional을 만들 수 있다.
    • Optional<Car> optCar = Optional.ofNullable(car); //car가 null이면 빈 Optional 객체가 반환

맵으로 Optional의 값을 추출하고 변환하기

Optional은 map 메서드를 지원한다.

Optional<Insurance> optInsurance = Optional.ofNullalbe(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);

스트림의 각 요소에 제공된 함수를 적용하는 map 메서드와 개념적으로 비슷하다. 여기서 Optional 객체를 최대 요소의 개수가 한 개 이하인 데이터 컬렉션으로 생각할 수 있다.

Optional이 값을 포함하면 map의 인수로 제공된 함수가 값을 바꾼다. Optional이 비어있으면 아무 일도 일어나지 않는다.

flatMap으로 Optional 객체 연결

Optional<Person> optPerson = Optional.of(person);
Optional<String> name = optPerson.map(Person::getCar)
    .map(Car::getInsurance)
    .map(Insurance::getName);

Optional에 map 메서드를 사용했을 때 반환 되는 객체가 또 Optional 이라면 Optional<Optional<객체>> 가 되므로 컴파일 되지 않는다. 이럴 때 flatMap 메서드를 사용하면 된다.

flatMap은 인수로 받은 함수를 적용해서 생성된 각각의 스트림에서 콘텐츠만 남긴다. 즉, 함수를 적용해서 생성된 모든 스트림이 하나의 스트림으로 병합되어 평준화된다.

Optional<Person> optPerson = Optional.of(person);
Optional<String> name = optPerson.flatMap(Person::getCar)
    .flatMap(Car::getInsurance)
    .map(Insurance::getName);

Optional 스트림 조작

자바 9에서는 Optional을 포함하는 스트림을 쉽게 처리할 수 있도록 Optional에 stream() 메서드를 추가했다. Optional 스트림을 값을 가진 스트림으로 변환할 때 이 기능을 유용하게 활용할 수 있다.

public Set<String> getCarInsuranceNames(List<Person> persons) {
    return persons.stream()
            .map(Person::getCar)
            .map(optCar -> optCar.flatMap(Car::getInsurance))
            .map(optInsurance -> optInsurance.map(Insurance::getName))
            .flatMap(Optional::stream)
            .collect(toSet());
}

Optional 덕분에 이런 종류의 연산을 널 걱정없이 안전하게 처리할 수 있지만 마지막 결과를 얻으려면 빈 Optional을 제거하고 값을 언랩해야 한다는 것이 문제다.

다음 코드처럼 filter, map을 순서적으로 이용해 결과를 얻을 수 있다.

Stream<Optional<String>> stream = ...
Set<String> result = stream.filter(Optional::isPresent)
    .map(Optional::get)
    .collect(toSet());

디폴트 액션과 Optional 언랩

  • get()
    • 값을 읽는 가장 간단한 메서드면서 동시에 가장 안전하지 않은 메서드. 값이 없을 때는 NoSuchElementException을 발생시킨다. 그냥 쓰지 않는 것이 바람직하다.
  • orElse(T other)
    • orElse 메서드를 이용하면 Optional 이 값을 포함하지 않을 때 기본값을 제공할 수 있다. 그런데 여기서 값이 있어도 orElse가 실행되므로 orElseGet을 사용하는것이 낫다.
  • orElseGet(Supplier<? extends T> other)
    • Optional에 값이 없을 때만 Supplier가 실행된다. Optional이 비어있을 때만 기본값을 생성하고 싶거나 기본값이 반드시 필요한 상황일 때 사용한다.
  • orElseThrow(Supplier<? extends X> exceptionSupplier)
    • Optional이 비어있을 때 발생 시킬 예외의 종류를 정의할 수 있다.
  • ifPresent(Consumer<? super T> consumer)
    • 값이 존재할 때 인수로 넘겨준 동작을 실행할 수 있다. 값이 없으면 아무 일도 일어나지 않는다.
  • ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)
    • Optional이 비었을 때 실행할 수 있는 Runnable을 인수로 받는다는 점만 ifPresent와 다르다.

Optional을 사용한 실용 예제

잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기

map 에서 get 메서드는 요청한 키에 대응하는 값을 찾지 못했을 때 nulll을 반환하는데 이때 반환하는 값을 Optional로 감싸서 이를 개선할 수 있다.

Optional<Object> value = Optional.ofNullable(map.get("key"));

이와 같은 코드를 이용해서 null일 수 있는 값을 Optional로 안전하게 변환할 수 있다.

예외와 Optional 클래스

자바 API는 값을 제공할 수 없을때 null을 반환하는 대신 예외를 발생시킬 때도 있는데 예로 문자열을 정수로 변환하는 정적 메서드 Integer.parseInt(String) 이다.

정수로 변환할 수 없는 문자열 문제를 빈 Optional로 해결할 수 있다. 즉, parseInt가 Optional을 반환하도록 모델링할 수 있다.

public static Optional<Integer> stringToInt(String s) {
    try {
        return Optional.of(Integer.parseInt(s));
    } catch (NumberFormatException e) {
        return Optional.empty();
    }
}

기본형 Optional을 사용하지 말아야 하는 이유

Stream처럼 Optional도 기본형 특화 클래스인 OptionalInt, OptionalLong, OptionalDouble이 존재한다. 하지만 Optional의 최대 요소 수는 한 개이므로 Optional에서는 기본형 특화 클래스로 성능이 개선되지 않는다. 또한 다른 일반 Optional과 혼용할 수 없으므로 기본형 Optional을 사용하지 않는것을 권장한다.

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함