본문 바로가기
java

[JAVA8] null 대신 Optional

by moonsiri 2022. 4. 24.
728x90
반응형

그동안 NullPointerException을 피하기 위해 어떻게 해결해 왔을까?

다음은 null 확인 코드를 추가해서 NullPointerException을 줄이려는 코드입니다.

// null 안전시도 1: 깊은 의심
public String getCarInsuranceName(Person person) {
    if (person != null) {   // null 확인
        Car car = person.getCar();
        if (car != null) {   // null 확인
            Insurance insurance = car.getInsurance();
            if (insurance != null) {   // null 확인
                return insurance.getName();
            }
        }
    }
    return "Unknown";
}
더보기
public class Person {
    private Car car;
    public Car getCar() { return car; }
}
public class Car {
    private Insurance insurance;
    public Insurance getInsurance() { return insurance; }
}
public class Insurance {
    private String name;
    public String getName() { return name; }
}

 

null 확인 코드 때문에 나머지 호출 체인의 들여 쓰기 수준이 증가했습니다.

이와 같은 반복 패턴(recurring pattern) 코드를 깊은 의심(deep doubt)이라고 부릅니다. 이를 반복하다 보면 코드의 구조가 엉망이 되고 가독성이 떨어집니다.

 

// null 안전 시도 2: 너무 많은 출구
public String getCarInsuranceName(Person person) {
    if (person == null) {
        return "Unknown";  // return
    }
    Car car = person.getCar();
    if (Car == null) {
        return "Unknown";  // return
    }
    Insurance insurance = car.getInsurance();
    if (insurance == null) {
        return "Unknown";  // return
    }
    return insurance.getName();
}

위 코드는 null 변수가 있으면 즉시 "Unknown"를 리턴합니다.

하지만 메서드에 4개의 출구가 생겼고, 출구 때문에 유지보수가 어려워집니다. 또한 각 변수마다 null값을 체크해야 되기 때문에 프로그래머의 실수를 유발할 가능성이 높아집니다.

 

 

null 때문에 발생하는 문제

  1. 에러의 근원
    • NullPointerException은 자바에서 가장 흔히 발생하는 에러이다.
    • 예외를 던지는 경우에는 스택 추적 전체를 캡처하는 비용문제가 있다.
    • null을 반환하는 경우에는 메서드를 호출하는 곳에서 null처리 코드를 추가해야 하고 만약 무시하면 언제 어디서 NPE를 만날지 모른다.
  2. 코드를 어지럽힘
    • 때로는 중첩된 null 확인 코드를 추가해야 하므로 null 때문에 코드 가독성이 떨어진다.
  3. 자바 철학에 위배
    • 자바는 개발자로부터 모든 포인터를 숨겼다. 하지만 예외가 있는데 그것이 바로 null 포인터다.
  4. 형식 시스템에 구멍을 만듦
    • null은 무형식이며 정보를 포함하고 있지 않으므로 모든 레퍼런스 형식에 null을 할당할 수 있다. 이런 식으로 null이 할당되기 시작하면서 시스템의 다른 부분으로 null이 퍼졌을 때 애초에 null이 어떤 의미로 사용되었는지 알 수 없다.

 

 

 

 

Class Optional<T>

 

 

Optional is intended to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result, " and using null for such was overwhelmingly likely to cause errors.
- Brian Goetz(Java Architect) -

 

 

자바 8은 Haskell과 Scala의 영향을 받아서 java.util.Optional라는 (자바의 고질적인 문제인 NullPointerException 문제를 해결할 수 있는) 새로운 클래스를 제공합니다.

Optional은 Null이 아닌 값을 포함할 수도 있고 포함하지 않을 수도 있는 컨테이너 개체입니다. 값이 있으면 isPresent()가 true를 반환하고 get()이 값을 반환합니다. orElse()(값이 없으면 기본값을 반환함) 및 ifPresent()(값이 있으면 코드 블록을 실행함)와 같이 포함된 값의 유무에 의존하는 추가 메서드가 제공됩니다.

자바 Optional은 값 기반 클래스로, 선택형 값을 캡슐화하는 클래스입니다. Optional(선택사항) 인스턴스에서 ID에 민감한 작업(참조 동일성(==), ID 해시 코드 또는 동기화 포함)을 사용하면 예기치 않은 결과가 발생할 수 있으므로 피해야 합니다.

값이 있으면 Optional 클래스는 값을 감싸고, 값이 없으면 Optional.empty 메서드로 Optional을 반환합니다. Optional.empty는 Optional의 특별한 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드입니다

 

 

Optional 클래스의 메서드

Optional 팩토리 메서드

  • 빈 Optional : 정적 팩토리 메서드 Optional.empty로 빈 Optional 객체를 얻을 수 있다.
Optional optCar = Optional.empty();
  • null이 아닌 값으로 Optional 만들기 : 정적 팩토리 메서드 Optional.of로 null이 아닌 값을 포함하는 Optional을 만들 수 있다.
Optional optCar = Optional.of(car);   // car이 null이라면 즉시 NullPointerException이 발생한다.

Optional 연산 메서드

  • map : Optional의 값을 추출하고 변환
Optional optInsurance = Optional.ofNullable(insurance);
Optional name = optInsurance.map(Insurance::getName);
  • filter : 특정값을 걸러냄
  • flatMap : Optional 객체 연결
// 옵셔널 사용
public String getCarInsuranceName(Person person) {
    return person.flatMap(Person::getCar)  // flatMap이 아니라 map 연산일 경우, Optional<optional<Car>>형식을 반환한다.
             .flatMap(Car::getInsurance)
             .map(Insurance::getName)	// 메서드 참조형
             .orElse("Unknown");
}
더보기
public class Person {
    private Optional<Car> car;
    public Optional<Car> getCar() { return car; }
}
public class Car {
    private Optional<Insurance> insurance;
    public Optional<Insurance> getInsurance() { return insurance; }
}
public class Insurance {
    private String name;
    public String getName() { return name; }
}

 

Optional 메서드

  • get() : 값을 읽는 가장 간단한 메서드면서 동시에 가장 안전하지 않은 메서드다. 메서드 get은 래핑된 값이 있으면 해당 값을 반환하고 값이 없으면 NoSuchElemnetException을 발생시킨다.
  • orElse(T other) : orElse 메서드를 이용하면 Optional이 값을 포함하지 않을 때 디폴트값을 제공할 수 있다.
  • orElseGet(Supplier other) : orElse 메서드에 대응하는 게으른 버전의 메서드다. Optional에 값이 없을 때만 Supplier가 실행되기 때문이다. 디폴트 메서드를 만드는 데 시간이 걸리거나 Optional이 비어있을 때만 디폴트값을 생성하고 싶을 때 사용한다.
  • orElseThrow(Supplier exceptionSupplier) : Optional이 비어있을 때 발생시킬 예외의 종류를 선택할 수 있다.
  • isPresent() : Optional이 값을 포함하면 true를 반환하고, 값을 포함하지 않으면 false를 반환한다.
  • ifPresent(Consumer consumer) : 값이 존재할 때 인수로 넘겨준 동작을 실행할 수 있다. 값이 없으면 아무 일도 일어나지 않는다.
.ifPresent(d -> System.out.println(d.getName());

 

 

 

Optional 성능 이슈

Optional을 사용하면 코드가 Null-Safe 해지고, 가독성이 좋아지며 애플리케이션이 안정적이 된다는 등과 같은 얘기들을 많이 접할 수 있습니다. 하지만 이는 Optional을 목적에 맞게 올바르게 사용했을 때의 이야기이고, Optional을 남발하는 코드는 오히려 다음과 같은 부작용(Side-Effect)을 유발할 수 있습니다.

 

Optional이 위험한 이유

  • NullPointerException 대신 NoSuchElementException가 발생함
    • Null-Safe 하기 위해 Optional을 사용하였는데, 값의 존재 여부를 판단하지 않고 접근한다면 NullPointerException는 피해도 NoSuchElementException가 발생할 수 있다.
  • 이전에는 없었던 새로운 문제들이 발생함
    • 기본적으로 Optional은 직렬화를 지원하지 않기 때문에 캐시나 메세지큐 등과 연동할 때 문제가 발생할 수 있다.
  • 과도한 사용은 코드의 가독성을 떨어뜨림
    • 과도한 사용은 남용될 수 있으며 가독성도 저하될 수 있다.
  • 시간적, 공간적 비용(또는 오버헤드)이 증가함
    • 공간적 비용: Optional은 객체를 감싸는 컨테이너 이므로 기존의 객체를 저장하기 위한 메모리에 더해 Optional 객체를 저장하기 위한 메모리가 할당된다. 이는 추가적인 메모리를 사용하는 것이다.
    • 시간적 비용: Optional 안에 있는 객체를 얻기 위해서는 Optional 객체를 통해 접근해야 하므로 접근 비용이 증가한다.

 

그렇다면, 무조건 Optional 사용을 지양해야하는가? 아닙니다. Stream 또는 삼항 연산자와 마찬가지로 적절하게 사용하면 가독성을 높이고 복잡성을 줄일 수 있습니다.

 

올바르게 Optional을 사용하는 방법

 

  • Optional 변수에 Null을 할당하지 않는다.
    • Optional은 container/boxing 클래스일 뿐이며, Optional 변수에 null을 할당하는 것은 Optional 변수 자체가 null인지 또 검사해야 하는 문제를 야기하므로 값이 없는 경우라면 Optional.empty()로 초기화하는 것이 좋다.
  • 값이 없을 때 Optional.orElseXXX()로 기본 값을 반환한다.
    • Optional의 장점 중 하나는 함수형 인터페이스를 통해 가독성 좋고 유연한 코드를 작성할 수 있다는 것이다. 가급적이면 isPresent()로 검사하고 get()으로 값을 꺼내기보다는 orElseGet 등을 활용해 처리하는 것이 좋다.
    • orElseGet은 값이 준비되어 있지 않은 경우, orElse는 값이 준비되어 있는 경우에 사용하면 된다. 만약 null을 반환해야 하는 경우라면 orElse(null)을 활용하도록 하자. 만약 값이 없어서 throw 해야 하는 경우라면 orElseThrow를 사용하면 되고 그 외에도 다양한 메서드들이 있으니 적당히 활용하면 된다. 추가적으로 Java9 부터는 ifPresentOrElse도 지원하고 있으며, Java 10부터는 orElseThrow()의 기본으로 NoSuchElementException()를 던질 수 있다. 만약 Java8이나 9를 사용 중이라면 명시적으로 넘겨주면 된다.
  • 단순히 값을 얻으려는 목적으로만 Optional을 사용하지 않는다.
    • 단순히 값을 얻으려고 Optional을 사용하는 것은 Optional을 남용하는 대표적인 경우이다. 이러한 경우에는 굳이 Optional을 사용해 비용을 낭비하는 것보다는 직접 값을 다루는 것이 좋다.
    • 메서드의 반환 값이 절대 null이 아니라면 Optional을 사용하지 않는 것이 좋다.
  • 생성자, 수정자, 메서드 파라미터 등으로 Optional을 넘기지 않는다.
    • Optional을 파라미터로 넘기는 것은 상당히 의미 없는 행동이다. 왜냐하면 넘겨온 파라미터를 위해 자체 null체크도 추가로 해주어야 하고, 코드도 복잡해지는 등 상당히 번거로워지기 때문이다.
    • Optional은 Java Bean으로 사용하도록 만들어진 것이 아님을 잊지 말아야 한다.
  • Collection의 경우 Optional을 사용하지 말고 빈 Collection으로 반환한다.
    • Collection, Stream, Array 같은 컨테이너 타입은 Optional로 감싸는 것보다 그냥 빈 객체를 반환하는 것이 처리가 가볍다.
    • Map의 값으로 Optional을 사용하면 절대 안 된다. Map 안에 키 자체가 없거나 키는 있지만 Optional인 경우 복잡성만 증가하고 오류 가능성을 키울 뿐이다.
  • 반환 타입으로만 사용한다.
    • Optional은 반환 타입으로써 에러가 발생할 수 있는 경우에 결과 없음을 명확히 드러내기 위해 만들어졌으며, Stream API와 결합되어 유연한 체이닝 api를 만들기 위해 탄생한 것이다.
    • 예를 들어 Stream API의 findFirst()나 findAny()로 값을 찾는 경우에 어떤 것을 반환하는 게 합리적 일지 Java 언어를 설계하는 사람이 되어 고민해보자. 언어를 만드는 사람의 입장에서는 Null을 반환하는 것보다 값의 유무를 나타내는 객체를 반환하는 것이 합리적일 것이다. Java 언어 설계자들은 이러한 고민 끝에 Optional을 만든 것이다. 그러므로 Optional이 설계된 목적에 맞게 반환 타입으로만 사용되어야 한다.

 

 

Null 체크와 Optional 성능 비교

위의 예제 코드로 cyclicBarrier를 사용하여 1000건 병목형상을 10번 수행하여 수행시간 평균값을 확인해보면 큰 차이가 없음을 확인 할  수 있습니다.

  1차 2차 3차 4차 5차 6차 7차 8차 9차 10차 평균
깊은 의심 32ms 34ms 36ms 33ms 37ms 30ms 29ms 41ms 31ms 37ms 34ms
많은 출구 29ms 36ms 27ms 34ms 42ms 30ms 30ms 33ms 49ms 31ms 34.1ms
Optional 36ms 33ms 33ms 35ms 36ms 35ms 36ms 34ms 33ms 29ms 34ms

 

 

 

정리

Optional은 null 또는 실제 값을 value로 갖는 wrapper로 감싸서 NPE(NullPointerException)로부터 자유로워지기 위해 나온 Wrapper 클래스입니다. 또한 Optional은 파라미터로 넘어가는 등이 아니라 반환 타입으로써 제한적으로 사용되도록 설계되었습니다. Optional은 값을 Wrapping 하고 다시 풀고, null일 경우에는 대체하는 함수를 호출하는 등의 오버헤드가 있으므로 잘못 사용하면 성능을 저하시킬 수 있습니다.

하지만 올바른 방식으로 사용한다면 성능 문제가 없기 때문에 Optional 사용을 무조건 지양하기 보단 올바르게 사용하는 법을 익히는 것이 좋을듯합니다.

 

 

 

[Refernece]

Java 8 in Action 자바 8 인 액션 전문가를 위한 최신 자바 기법 가이드 (한빛미디어)

https://dzone.com/articles/using-optional-correctly-is-not-optional

https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html

https://stackoverflow.com/questions/34696884/performance-of-java-optional

https://mangkyu.tistory.com/203 

https://www.baeldung.com/java-optional

https://homoefficio.github.io/2019/10/03/Java-Optional-바르게-쓰기/

728x90
반응형

댓글