ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Optional - 모던 자바 인 액션
    BackEnd/자바 2022. 4. 4. 23:48

    자바8은 하스켈과 스칼라의 영향을 받아 Optional 클래스를 제공한다. 선택형값을 캡슐화하는 클래스이다.

    null 참조와 Optional.empty() 의 차이
    null 을 참조하려 하면 NPE 이 발생하지만 Optional 은 객체라서 활용할 수 있다.

    null 대신 Optional 을 사용하면 값이 없을 수도 있음을 명시적으로 보여준다.
    반면 객체 자체를 사용하면 null 참조가 할당될 경우 이것이 올바른 값인지 잘못된 값인지 판단할 정보가 없다.

    아래는 Person 이 Car 를 소유할 수도 있고 소유하지 않을 수도 있음을 명시적으로 보여준다.
    도메인 모델의 의미를 더 명확하게 만들 수 있는 것을 보여준다.

    public class Person {
        private Optional<Car> car;
    
        public Optional<Car> getCar() {
            return car;
        }
    }


    Optional.empty() : 빈 Optional 객체를 얻을 수 있다.
    Optional.of(obj) : null 이 아닌 객체를 포함하는 Optional 을 만들 수 있다.
    Optional.ofNullable(obj) : null 이 될 수도 있는 객체를 포함하는 Optional 을 만들 수 있다.

    Optional.get 으로 null 인 객체를 꺼내려하면 NPE 가 발생한다. 그럼 null 을 사용할 때와 동일한 문제가 발생하는데,
    명시적인 null 검사를 생략하려면, Optional 의 map 메서드를 사용하면 된다.
    스트림의 map 은 스트림의 각 요소에 제공된 함수를 적용하는 연산이다. 여기서 Optional 객체를 최대 요소의 개수가 한 개 이하인 데이터 컬렉션으로 생각할 수 있다.
    Optional 이 값을 포함하면 map 의 인수로 제공된 함수가 값을 바꾸고 Optional 이 비어있으면 아무 일도 일어나지 않는다.

    Optional<Insurance> optIns = Optional.ofNullable(insurance);
    Optional<String> name = optIns.map(Insurance::getName);


    여러개의 메서드를 호출할 때는 아래와 같이 구현이 가능할 것 같지만 아래 코드는 2번째 줄부터 컴파일 오류가 난다.
    optPerson 이 Optional 로 감싸져 있는데 getCar을 하면 Optional<Optional<Car>> 형식의 객체가 반환되기 때문이다.

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


    flatmap 을 대신 사용한다. flatmap 은 인수로 받은 함수를 적용해서 생성된 각각의 스트림에서 컨텐츠만 남긴다.
    즉, 함수를 적용해 생성된 모든 스트림이 하나의 스트림으로 병합되어 평준화된다.
    이차원 Optional 을 일차원 Optional 로 평준화하는 것이다.

    public String getCarInsuranceName(Optional<Person> optPerson){
        return  optPerson.flatMap(Person::getCar)
                .flatMap(Car::getInsurance)
                .map(Insurance::getName)
                .orElse("Unknown");
        
    }

    이런 방식을 null 을 확인하느라 조건 분기문을 추가해 코드를 복잡하게 만들지 않으면서 쉽게 이해할 수 있다.

    또한 Optional 을 사용하므로 도메인 모델과 관련한 암묵적인 지식에 의존하지 않고 명시적으로 형식 시스템을 정의할 수 있다. Optional 을 인수로 받거나 Optional 을 반환하는 메서드를 정의한다면 결과적으로 이 메서드를 사용하는 모든 사람에게 이 메서드가 빈 값을 받거나 빈 값을 반환할 수 있음을 잘 문서화해서 제공하는 것과 같다.

    Optional 은 Serializable 인터페이스를 구현하지 않아, 도메인 모델에 Optional 을 사용하면 직렬화 할 수 없다.
    그래도 객체 그래프에서 일부 또는 전체가 null 일 수 있는 상황이라면 Optional 을 사용하는 것이 좋다.
    직렬화 모델이 필요하다면 Optional 값을 반환받을 수 있는 메서드를 추가하는 방식을 권장한다.

    public class Person2 {
        private Car car;
    
        public Optional<Car> getCarOptional() {
            return Optional.ofNullable(car);
        }
    }

    Optional Stream 조작
    Optional 의 stream() 메서드는 Optional 스트림을 값을 가진 스트림으로 변환할 때 유용하다.
    아래 체인에서 Optional::stream 을 사용하지 않았다면 filter(Optional::isPresent).map(Optional::get).collect.. 를 구현해야한다.


    Optional 언랩

    get은 가장 간단한 메서드지만 가장 안전하지 않은 메서드다. 요소가 없으면 NoSuchElementException 을 발생시켜 null 을 넣는 상황과 크게 다르지 않다.

    orElse 는 Optional 이 값을 포함하지 않을 때 기본값을 제공한다.
    orElseGet(Supplier) 는 orElse 메서드에 대응하는 게이른 버전의 메서드다. Optional 에 값이 없을 때만 Supplier 가 실행된다.
    디폴트 메서드를 만드는 데 시간이 걸리거나 Optional 이 비었을 때만 기본값을 생성하고 싶다면 이를 사용해야한다.
    orElseThrow 값이 없으면 exception 을 발생시키는 점에서 get과 같지만, exception 을 선택할 수 있다는 점에서 다르다.
    ifPresent(Consumer) 값이 존재할 때 supplier 동작을 실행하고 값이 없으면 아무일도 하지 않는다.
    ifPresentOrElse(Consumer,Runnable) Optional 이 비었을 때 실행할 Runnable 을 인수로 받는다.


    잠재적 null 감싸기

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


    예외와 Optional 클래스
    Integer.parseInt(String) => 변환 불가 시 NumberFormatException 발생
    유틸리티 메서드를 구현해 Optional 을 반환

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


    기본형 특화 Optional 의 사용 지양
    OptionalInt, OptionalLong,OptionalDouble 등 기본형 특화 Optional 이 있는데,
    Stream 에서는 기본형 Stream 을 사용하는 것이 성능 개선을 해주었지만, Optional 의 요소는 한개로 제한되어 있어
    성능 개선과 관련이 없으며, 기본형 Optional 에는 map, flatmap, filter 등을 지원하지 않는다.
    또 스트림과 마찬가지로 기본형 특화 Optional 로 생성한 결과는 다른 일반 Optional 과 혼용할 수 없다.


    public int readDuration(Properties props, String name){
        Optional<String> value = Optional.ofNullable(props.getProperty(name));
        return value.flatMap(Main::stringToInt)
                .filter(i -> i > 0).orElse(0);
    }

    반응형
Designed by Tistory.