Java8 In Action

[Part3] Java8 In action - Chapter10 - 1

신나게개발썰 2022. 8. 4. 10:00
반응형

해당 내용은 Java8 In Action 책을 요약 및 정리한 내용입니다.

좀 더 자세히 알고 싶으신 분들은 책을 사서 읽어보심을 추천드립니다.!

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

Entity

Car.java

package Part3.Chapter10.Chapter10_10_1.entity;

public class Car {
    private Insurance insurance;

    public Insurance getInsurance() {
        return this.insurance;
    }

}

Insurance.java

package Part3.Chapter10.Chapter10_10_1.entity;

public class Insurance {
    private String name;

    public String getName() {
        return this.name;
    }
}

Person.java

package Part3.Chapter10.Chapter10_10_1.entity;

public class Person {
    private Car car;

    public Car getCar() {
        return this.car;
    }

}

Main

package Part3.Chapter10.Chapter10_10_1;

import Part3.Chapter10.Chapter10_10_1.entity.Car;
import Part3.Chapter10.Chapter10_10_1.entity.Insurance;
import Part3.Chapter10.Chapter10_10_1.entity.Person;

/*
 * 10.1 값이 없는 상황을 어떻게 처리할까?
 */
public class Main_10_1 {

    /*
     * 코드에 아무 문제가 없는 것처럼 보이지만 몇 가지 문제가 있다.
     * Person객체가 null일수도 있고 pserson의 Car객체 null일수도 있으며
     * Car객체의 Insurance객체 마찬가지로 null일수도 있다.
     * 그러므로 총 3개의 객체가 null 일수도 있는 상황이 생긴다.
     * 이 경우라면 런타임에 NullPointerException이 발생되면서 프로그램이 중단될수도 있다.   
     */
    public String getCarInsuranceName(Person person) {
        return person.getCar().getInsurance().getName();
    }

    /*
     * 10.1.1 보수적인 자세로 NullPointerException 줄이기
     * 
     * 변수를 참조할 때마다 null을 확인하며 중간 과정에 하나라도 null이 존재하면 "Unknown" 문자열을 반환한다.
     * 상식적으로 모든 회사에는 이름이 있으므로 보험회사의 이름이 null인지는 확인하지 않았다.
     * 우리가 확실히 알고 있는 영역을 모델링할 때는 이런 지식을 활용해서 null 확인을 생략할 수 있지만,
     * 데이터를 자바 클래스로 모델링할 때는 이 같은(모든 회사는 반드시 이름을 갖는다) 사실을 단정하기 어렵다.
     * 
     * 모든 변수가 null인지 의미하므로 변수를 접근할 때마다 중첩된 if가 추가되면서 코드 들여쓰기 수준이 증가한다.
     * 이와 같은 반복패턴(recurring pattern)코드를 "깊은 의심-deep doubt"이라고 부른다.
     */
    public String getNullCheckCarInsuranceName(Person person) {
        if(person != null) {

            Car car = person.getCar();
            if(car != null) {

                Insurance insurance = car.getInsurance();
                if(insurance != null) {
                    return insurance.getName();
                }
            }
        }

        return "Unknown";
    }

    /*
     * 위 메서드와 다르겐 방법으로 중첩 if 블록을 없앴다.
     * 
     * 하지만 이 코드도 좋은 코드는 아니다. 메서드에 네 개의 출구가 생겼기 때문이다.
     * 출구가 많아지면 유지보수 하기가 어려워고 게다가 null일 때 반환 되는 기본값 "Unknown"이
     * 세 곳에서 반복되고 있다. 이 같은 경우는 문자열을 상수로 만들어서 이 문제를 해결할 수 있다.
     */
    public String getNullcheckCarInsuranceName2(Person person) {
        if(person == null) {
            return "Unknown";
        }

        Car car = person.getCar();
        if(car == null) {
            return "Unknown";
        }

        Insurance insurance = car.getInsurance();
        if(insurance == null) {
            return "Unknown";
        }

        return insurance.getName();
    }

    public static void main(String [] args) {
        /*
         * 10.1.2 null 때문에 발생하는 문제
         * 
         * 자바에서 null 레퍼런스를 사용하면서 발생할 수 있는 이론적, 실용적 문제를 살펴보자.
         * 
         * 에러의 근원이다.
         * NullPointerException은 자바에서 가장 흔히 발생하는 에러다.
         * 
         * 코드를 어지럽힌다.
         * 때로는 중첩된 null 확인 코드를 추가해야 하므로 null 때문에 코드 가독성이 떨어진다.
         * 
         * 아무 의미가 없다
         * null은 아무 의미도 표현하지 않는다. 특히 정적 형식 언어에서 값이 없음을 표현하는 방법으로는
         * 적철하지 않다.
         * 
         * 자바 철학에 위배된다.
         * 자바는 개발자로부터 모든 포인터를 숨겼다. 하지만 예외가 있는데 그것이 바로 null 포인터다.
         * 
         * 형식 시스템에 구멍을 만든다.
         * null은 무형식이며 정보를 포함하고 있지 않으므로 모든 레퍼런스 형식에 null을 할당할 수 있다.
         * 이런 식으로 null이 할당되기 시작하면서 시스템의 다른 부분으로 null이 퍼졌을 때 애초에 null이
         * 어떤 의미로 사용되었는지 알 수 없다.
         */

        /*
         * 10.1.3 다른 언어는 null 대신 무얼 사용하나?
         * 
         * 그루비 같은 언어는 "안전 내비게이션 연산자-safe navigation operator(..?)"를 도입해서 null 문제 해결.
         *         def carInsuranceName = person?.car?.insurance?.name
         * 
         * 하스켈은 선택형값(optional value)을 저장할 수 있는 Maybe라는 형식을 제공.
         * Maybe는 주어진 형식의 값을 갖거나 아니면 아무 값도 갖지 않을 수 있다.
         * 
         * 스칼라도 T 형식의 값을 갖거나 아무 값도 갖지 않을 수 있는 Option[T]라는 고제를 제공한다.
         * Option 형식에서 제공하는 연산을 사용해서 값이 있는지 여부를 명시적으로 확인해야 한다.(즉 null 확인)
         * 형식 시스템에서 이를 강제하므로 null과 관련한 문제가 일어날 가능성이 줄어든다.
         * 
         * 자바 8은 '선택형값' 개념의 영향을 받아 java.util.Optional<T>라는 새로운 클래스를 제공한다.
         */
    }
}

10.2 Optional 클래스 소개

Main

package Part3.Chapter10.Chapter10_10_2;

import java.util.Optional;

/*
 * 10.2 Optional 클래스 소개
 * 
 * 자바 8은 하스켈과 스칼라의 영향을 받아 java.util.Optional<T>라는 새로운 클래스를 제공한다.
 * Optional은 선택형값을 캡슐화하는 클래스다.
 * 
 * 값이 있으면 Optional 클래스는 값을 감싼다. 반면 값이 없으면 Optional.empty 메서드로 Optional을 반환한다.
 * Optional.empty는 Optional의 특별한 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드다.
 * 
 * null 레퍼런스와 Optional.empty()는 의미론상으론 둘이 비슷하지만 실제로는 차이점이 많다.
 * null을 참조하면 NullPointerException이 발생하지만 Optional.empty()는 Optional 객체이므로 이를 다양한 방식으로
 * 활용할 수 있다.
 */
public class Main_10_2 {

    public static void main(String[] args) {

    }
}

/*
 * Optinal 클래스를 사용하면서 모델의 의미(semantic)가 더 명확해졌음을 알수 있다.
 * 사람은 Optional<Car>를 참조하면서 차는 Optional<Insurace>를 참조하는데,
 * 이는 사람이 자동차가 있을수도 없을수도, 자동차는 보험이 있을수도 없을수도 있음을 명확히 설명한다.
 * 
 * 또한 보험회사 이름은 Optional<String>이 아니라 String 형식이므로 이는 보험회사는 반드시 이름을 가져야 함을 보여준다.
 * 따라서 보험회사 이름을 참조할 때 NullPointerException이 발생할 수도 있다는 정보를 확인할 수 있다.
 * 하지만 보험회사 이름이 null인지 확인하는 코드를 추가할 필요는 없다. 오히려 고쳐야 할 문제를 감추는 꼴이 되기 때문이다.
 * 보험회사는 이름을 반드시 가져야 하며 이름이 없는 보험회사를 발견했다면 예외를 처리하는 코드를 추가하는 것이 아니라
 * 보험회사 이름이 없는 이유가 무엇인지 밝혀서 문제를 해결해야 한다.
 * 
 * Optional을 이용하면 값이 없는 상황이 우리 데이터에 문제가 있는 것인지 아니면 알고리즘의 버그인지 명확하게 구분할 수 있다.
 * 모든 null 레퍼런스를 Optional로 대치하는 것은 바람직하지 않다.
 * Optional의 역할은 더 이해하기 쉬운 API를 설계하도록 돕는 것이다. 즉, 메서드의 시그너처만 보고도 선택형값인지 여부를 구별할 수 있다.
 * Optional이 등장하면 이를 언랩해서 값이 없을 수도 있는 상황에 적절하게 대응하도록 강제하는 효과가 있다.
 */
class Person {
    // 사람은 차가 있을수도 없을수도 있으므로 Optional로 정의한다.
    private Optional<Car>  car;

    public Optional<Car> getCar() {
        return this.car;
    }

}

class Car {
    // 자동차가 보험에 가입되어 있을수도 없을수도 있으므로 Optional로 정의한다.
    private Optional<Insurance> insurance;

    public Optional<Insurance> getInsurance() {
        return this.insurance;
    }

}

class Insurance {
    // 보험회사에는 반드시 이름이 있다.
    private String name;

    public String getName() {
        return this.name;
    }
}

10.3 Optional 적용 패턴

Main

package Part3.Chapter10.Chapter10_10_3;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;

/*
 * 10.3 Optional 적용 패턴
 */
public class Main_10_3 {

    public static void main(String[] args) {
        /*
         * 10.3.1 Optional 객체 만들기
         */

        /*
         *  빈 Optional
         *  정적 팩토리 메서드 Optional.empty로 빈 Optional 객체를 얻을수 있다.
         */
        Optional<Car> optCar = Optional.empty();
        System.out.println("빈 Optional : " + optCar);

        Car car = new Car("avante");
        Insurance insurance = new Insurance("sinnake");
        /*
         * null이 아닌 값으로 Optional 만들기
         * 정적 팩토리 메서드 Optional.of로 null이 아닌 값을 포함하는 Optional을 만들 수 있다.
         * 
         * 이제 car가 null이라면 즉시 NullPointerException이 발생한다.
         * (Optional을 사용하지 않았다면 car의 프로퍼티에 접근하려 할 때 에러가 발생했을 것이다.)
         */
        optCar = Optional.of(car);
        System.out.println("null이 아닌 값으로 Optional 만들기 : " + optCar);

        /*
         * null로 Optional 만들기
         * 
         * 정적 팩토리 메서드 Optional.ofNullable로 null을 저장할 수 있는 Optional을 만들 수 있다.
         * car가 null이면 빈 Optional 객체가 반환된다.
         */
        car = null;
        optCar = Optional.ofNullable(car);
        System.out.println("null로 Optional 만들기 : " + optCar);

        /*
         * 10.3.2 맵으로 Optional의 값을 추출하고 변환하기
         * 
         * 보통 객체의 정보를 추출할 때는 Optional을 사용할 때가 많다.
         * 예를 들어 보험회사의 이름을 추출한다고 가정하자.
         * 아래 코드처럼 이름 정보에 접근하기 전에 insurance가 null인지 확인해야 한다.
         */
        String name = null;
        if(insurance != null) {
            name = insurance.getName();
        }
        System.out.println("10.3.2 맵으로 Optional의 값을 추출하고 변환하기 : [name] = " + name);

        /*
         * 이런 유형의 패턴에 사용할 수 있도록 Optional을 map 메서드를 지원한다.
         * 
         * Optional의 map 메서드는 스트림의 map 메서드와 개념적으로 비슷하다.
         * 스트림의 map은 스트림의 각 요소에 제공된 함수를 적용하는 연산이다.
         * 여기서 Optional 객체를 최대 요소의 개수가 한 개 이하인 데이터 컬렉션으로 생각할 수 있다.
         * 
         * Optional이 값을 포함하면 map의 인수로 제공된 함수가 값을 바꾼다.
         * Optional이 비어있으면 아무 일도 일어나지 않는다.
         */
        Optional<Insurance> optInsurance = Optional.ofNullable(new Insurance("sinnake"));
        Optional<String> optName = optInsurance.map(Insurance::getName);
        System.out.println("10.3.2 맵으로 Optional의 값을 추출하고 변환하기 - map : [name] = " + optName);

        /*
         * 10.3.3 flatMap으로 Optional 객체 연결 
         */
        Optional<Person> mapPerson = Optional.ofNullable(new Person("kim sung wook")) ;
        Optional<Car> mapCar = Optional.ofNullable(new Car("avante"));        
        Optional<Insurance> mapInsurance = Optional.ofNullable(new Insurance("sinnake"));        

        mapCar.get().setInsurance(mapInsurance);
        mapPerson.get().setCar(mapCar);

        /*
         * mapPerson
         *         .map(Person::getCar)
         *         .map(Car::getInsurance)
         *         .map(Insurance::getName)
         * 
         * 위 코드는 컴파일이 되지 않는다.
         * mapPerson의 형식은 Optional<Person>이므로 map 메서드를 호출할 수 있다.
         * 하지만 getCar는 Optional<Car> 형식의 객체를 반환한다.
         * 즉, map 연산의 결과는 Optional<Optional<Car>> 형식의 객체다.
         * getInsurance는 또 다른 Optional 객체를 반환하므로 getInsurance 메서드를 지원하지 않는다. 
         */

        /*
         * Optional은 스트림의 flatMap처럼 인수로 받은 함수를 적용해서 생성된 각각의 스트림에서 콘텐츠만 남길 수 있다.
         * 즉 이차원 Optional을 일차원 Optional로 평준화를 시킬 수 있다.
         */
        System.out.println("10.3.3 flatMap으로 Optional 객체 연결 - flatMap : " + getCarInsuranceName(mapPerson));

        /*
         * Optional을 이용한 Person/Car/Insurance 참조 체인
         * 
         * Person을 Optional로 감싼 다음에 flatMap(Person::getCar)를 호출했다. 이 호출은 두 단계의 논리적 과정으로 생각할 수 있다.
         * 첫 번째 단계에서는 Optional 내부의 Person에 Function을 적용한다. 여기서는 Person의 getCar 메서드가 Function이다.
         * getCar 메서드는 Optional<Car>를 반환하므로 Optional 내부의 Person이 Optional<Car>로 변환되면서 중첩 Optional이 생성된다.
         * 따라서 flatMap연산으로 Optional을 평준화한다.
         * 
         * 평준화 과정이란 이론적으로 두 Optional을 합치는 기능을 수행하면서 둘 중 하나라도 null이면 빈 Optional을 생성하는 연산이다.
         * flatMap을 빈 Optional에 호출하면 아무 일도 일어나지 않고 그대로 반환된다.
         * 반면 Optional이 Person을 감싸고 있다면 flatMap에 전달된 Function이 Person에 적용된다.
         * Function을 적용한 결과가 이미 Optional이므로 flatMap 메서드는 결과를 그대로 반환할 수 있다.
         * 
         * 두 번째 단계도 첫 번째 단계와 비슷하게 Optional<Car>를 Optional<Insurance>로 변환한다.
         * 세 번째 단계에서는 Optional<Insurance>를 Optional<String>으로 변환한다.
         * 세 번재 단계에서 Insurance.getName()은 String을 반환하므로 flatMap을 사용할 필요가 없다.
         * 
         * 호출 체인 중 어떤 메서드가 빈 Optional을 반환한다면 전체 결과로 빈 Optional을 반환하고 아니면 관련 보험회사의 이름을 포함하는
         * Optional을 반환한다.
         * 호출 체인의 결과로 Optional<String>이 반환되는데 여기에 회사 이름이 저장되어 있을수도 없을수도 있다.
         * Optional이 비어있을 때 디폴트 값(default value)을 제공하는 orElse 메서드를 사용했다.
         * Optional은 디폴트값을 제공하거나 Optional을 언랩(unwrap)하는 다양한 메서드를 제공한다.
         */

        /*
         * 10.3.4 디폴트 액션과 Optional 언랩
         * 
         * Optional이 비어있을 때 디폴트값을 제공할 수 있는 orElse 메서드로 값을 읽자.
         * Optional 클래스는 Optional 인스턴스에서 값을 읽을 수 있는 다양한 인스턴스 메서드를 제공한다.
         * 
         * - get()은 값을 읽는 가장 간단한 메서드면서 동시에 가장 안전하지 않는 메서드다.
         * 래핑된 값이 있으면 값을 반환하고 값이 없으면 NoSuchElementException을 발생시킨다.
         * 따라서 Optional에 값이 반드시 있다고 가정할 수 없으면 get 메서드를 사용하지 않는것이 좋다.
         * 결국 이 상황은 중첩된 null 확인 코드를 넣는 상황과 다르지 않을수 있다.
         * 
         * - orElse(T other) 메서드를 이용하면 Optional이 값을 포함하지 않았을 때 디폴트값을 제공한다.
         * 
         * - orElseGet(Supplier<? extends T> other)는 orElse 메서드에 대응하는 게으른 버전 메서드다.
         * Optional에 값이 없을 때만 Supplier가 실행되기 때문이다.
         * 디폴트 메서드를 만드는데 시간이 걸리거나(효율성 때문에) Optional이 비었을 때만 디폴트값을 생성하고 싶다면
         * (디플트값이 반드시 필요한 상황) orElseGet(Supplier<? extends T> other)를 사용해야 한다.
         * 
         * - orElseThrow(Supplier<? extends X> exceptionSupplier)는 Optional이 비어있을 때 예외를 발생시킨다.
         * get 메서드와 다르게 해당 메서드는 발생시킬 예외의 종류를 선택할 수 있다.
         * 
         * - ifPresent(Consumer<? super T> consumer)를 이용하면 값이 존재할 때 인수로 넘겨준 동작을 실행할 수 있다.
         * 값이 없으면 아무 일도 일어나지 않는다.
         */

        /*
         * 정 map으로 하고 싶다면 아래 메서드처럼 할순 있다.
         */
        System.out.println("10.3.3 flatMap으로 Optional 객체 연결 - map : " + getCarInsuranceNameMAP(mapPerson));

        /*
         * 도메인 모델에 Optional을 사용했을 때 데이터를 직렬화할 수 없는 이유
         * 
         * Optional 클래스는 필드 형식으로 사용할 것을 가정하지 않았으므로 Serializable 인터페이스를 구현하지 않았다.
         * 따라서 우리 도메인 모델에 Optional을 사용한다면 직렬화(serializable)모델을 사용하는 도구나 프레임워크에서 문제가 생길 수 있다.
         * 이와 같은 단점에도 불구하고 여전히 optional을 사용해서 도메인 모델을 구성하는 것이 바람직하다고 생각한다.
         * 특히 객체 그래프에서 일부 또는 전체가 null일 수 있는 상황이라면 더욱 그렇다.
         * 직렬화 모델이 필요하다면 아래 코드처럼 Optional로 값을 반환받을 수 있는 메서드를 추가하는 방식을 권장한다.
         * 
         * public class Person {
         *         private Car car;
         * 
         *         public Optional<Car> getCarAsOptional() {
         *             return Optional.ofNullable(car);
         *         }
         * }
         */

        /*
         * 10.3.5 두 Optional 합치기
         */
        mapPerson = Optional.ofNullable(new Person("김성욱")) ;
        mapCar = Optional.ofNullable(new Car("아반테"));        

        mapPerson.get().setCar(Optional.ofNullable(new Car("아반테")) );
        System.out.println("10.3.5 두 Optional 합치기 : " + nullSafefindCheapestInsurance(mapPerson, mapCar).get());
        System.out.println("10.3.5 두 Optional 합치기 : " + nullSafefindCheapestInsuranceQuiz(mapPerson, mapCar).get());

        /*
         * 10.3.6 필터로 특정값 거르기
         * 
         * filter 메서드는 프레디케이트를 인수로 받는다.
         * Optional 객체가 값을 가지고 프레디케이트와 일치하면 filter 메서드는 그 값을 반환하고 그렇지 않으면 빈 Optional 객체를 반환한다.
         * Optional이 비어있다면 filter 연산은 아무 동작도 하지 않는다. Optional에 값이 있으면 그 값에 프레디케이트를 적용한다.
         * 프레디케이트 적용 결과가 true면 Optional에는 아무 변화도 일어나지 않고 false면 값은 사라지고 빈 Optional이 된다.
         */
        System.out.println(findInsurance("아반테"));

        /*
         * Optional 클래스의 메서드
         * 
         * 메서드 : empty 
         * 설명 : 빈 Optional 인스턴스 반환
         * 
         * 메서드 : filter
         * 설명 : 값이 존재하고 프레디케이트와 일치하면 값을 포함하는 Optional 반환하고 
         * 값이 없거나 프레디케이트와 일치하지 않으면 빈 Optional을 반환.
         * 
         * 메서드 : flatMap
         * 설명 : 값이 존재하면 인수로 제공된 함수를 적용한 결과 Optional을 반환하고
         * 값이 없으면 빈 Optional을 반환. 
         * 
         * 메서드 : get
         * 설명 : 값이 존재하면 Optional이 감싸고 있는 값을 반환하고, 값이 없으면
         * NoSuchElementException이 발생.
         * 
         * 메서드 : ifPresent
         * 설명 : 값이 존재하면 지정된 Consumer를 실행하고 값이 없으면 아무 일도 일어나지 않음.
         * 
         * 메서드 : isPresent
         * 설명 : 값이 존재하면 true 반환, 값이 없으면 false 반환.
         * 
         * 메서드 : map
         * 설명 : 값이 존재하면 제공된 매핑 함수를 적용함.
         * 
         * 메서드 : of
         * 설명 : 값이 존재하면 값을 감싸는 Optional을 반환하고, 값이 null이면 NullPointerException을 발생.
         * 
         * 메서드 : ofNullable
         * 설명 : 값이 존재하면 값을 감싸는 Optional을 반환하고, 값이 null이면 빈 Optional을 반환.
         * 
         * 메서드 : orElse
         * 설명 : 값이 존재하면 값을 반환, 값이 없으면 디폴트값을 반환.
         * 
         * 메서드 : orElseGet
         * 설명 : 값이 존재하면 값을 반환, 값이 없으면 Supplier에서 제공하는 값을 반환.
         * 
         * 메서드 : orElseThrow
         * 설명 : 값이 존재하면 값을 반환, 값이 없으면 Supplier에서 생성한 예외를 발쌩.
         */
    }

    public static Insurance findInsurance(String insuranceName) {        

        return Optional.ofNullable(new Insurance("신나게", "아반테", 10_000_000))
            .filter(i -> i.getCarKind().equals(insuranceName))
            .orElseGet(() -> new Insurance("없음", "없음", 0));
            //.orElse(new Insurance("없음", "없음", 0));
    }

    public static Optional<Insurance> nullSafefindCheapestInsuranceQuiz(Optional<Person> person, Optional<Car> car) {
        /*
         * 첫 번재 Optional에 flatMap을 호출했으므로 첫 번째 Optional이 비어있다면 인수로 전달한 람다 표현식이 
         * 실행되지 않고 그대로 빈 Optional을 반환한다.
         * 반면 persion 값이 있으면 flatMap 메서드에 필요한 Optional<Insurance>를 반환하는 Function의 입력으로
         * person을 사용한다.
         * 이 함수 바디에서는 두 번째 Optional에 map을 호출하므로 Optional이 car 값을 포함하지 않으면 Function은
         * 빈 Optional을 반환하므로 결국 해당 메서드는 빈 Optional을 반환한다.
         */
        return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
    }

    public static Optional<Insurance> nullSafefindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
        if(person.isPresent() && car.isPresent()) {
            return Optional.of(findCheapestInsurance(person.get(), car.get())) ;
        }

        return Optional.empty();
    }

    public static Insurance findCheapestInsurance(Person person, Car car) {
        List<Insurance> insurancesList = Arrays.asList(
            new Insurance("신나게_1", "아반테", 70_000_0)
            , new Insurance("신나게_2", "그렌져", 20_000_0)
            , new Insurance("신나게_3", "비엠더블유", 30_000_0)
            , new Insurance("신나게_4", "벤츠", 40_000_0)
            , new Insurance("신나게_4", "벤츠", 20_000_0)
            , new Insurance("신나게_5", "아반테", 50_000_0)
            , new Insurance("신나게_6", "아반테", 60_000_0)
        );

        Insurance cheapesInsurace = insurancesList.stream()
            .filter((insurances) -> Optional.ofNullable(car)
                .map(Car::getName)
                .orElse("Unknown")
                .equals(insurances.getCarKind() ))
            .filter((insurances) -> Optional.ofNullable(person.getCar())
                .map(Optional::get)
                .map(Car::getName)
                .orElse("Unknown")
                .equals(insurances.getCarKind() ))
            .min(Comparator.comparingInt(Insurance::getPrice))
            .orElse(new Insurance("없음", "없음", 0));

        return cheapesInsurace;
    }    

    public static String getCarInsuranceName(Optional<Person> person) {
        return person
            .flatMap((p) -> Optional.ofNullable(p.getCar()).map(c -> c.get()) )
            .flatMap((c) -> Optional.ofNullable(c.getInsurance()).map(i -> i.get()) )
            .map(Insurance::getName)
            .orElse("Unknown");
    }

    public static String getCarInsuranceNameMAP(Optional<Person> person) {
        return person
            .map(Person::getCar)
            .map(i -> i.get().getInsurance())
            .map(s -> s.get().getName())
            .orElse("Unknown");
    }
}

class Person {
    // 사람은 차가 있을수도 없을수도 있으므로 Optional로 정의한다.
    private Optional<Car> car;
    private String name;

    public Person() { }

    public Person(String name) {
        this.name = name;
    }

    public Optional<Car> getCar() {
        return this.car;
    }

    public void setCar(Optional<Car> car) {
        this.car = car;
    }

    public String getName() {
        return this.name;
    }
}

class Car {
    // 자동차가 보험에 가입되어 있을수도 없을수도 있으므로 Optional로 정의한다.
    private Optional<Insurance> insurance;
    private String name;

    public Car() { }

    public Car(String name) {
        this.name = name;
    }

    public Optional<Insurance> getInsurance() {
        return this.insurance;
    }

    public void setInsurance(Optional<Insurance> insurance) {
        this.insurance = insurance;
    }

    public String getName() {
        return this.name;
    }
}

class Insurance {
    // 보험회사에는 반드시 이름이 있다.
    private String name;
    private String carKind;
    private int price;

    public Insurance() { }

    public Insurance(String name) {
        this.name = name;
    }

    public Insurance(String name, String carKind, int price) {
        this.name = name;
        this.carKind = carKind;
        this.price = price;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setCarKind(String carKind) {
        this.carKind = carKind;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public String getName() {
        return this.name;
    }

    public String getCarKind() {
        return carKind;
    }

    public int getPrice() {
        return price;
    }

    @Override
    public String toString() {
        return "{"
            + "price : " + this.price
            + ", name : " + this.name
            + ", carKind : " + this.carKind
            +"}";
    }
}

10.4 Optional을 사용한 실용 예제

Process

OptionalUtil.java

package Part3.Chapter10.Chapter10_10_4.optional;

import java.util.Optional;

public class OptionalUtil {

    public static Optional<Integer> stringToInt(String s) {
        /*
         * 정수로 변환할 수 없는 문자열 문제를 빈 Optional로 해결할 수 있다.
         * 즉, parseInt가 Optional을 반환하도록 모델링할 수 있다.
         */
        try {
            return Optional.of(Integer.parseInt(s));
        } catch(NumberFormatException e) {
            return Optional.empty();
        }
    }

    public static Optional<Long> stringToLong(String s) {
        try {
            return Optional.of(Long.parseLong(s));
        } catch(NumberFormatException e) {
            return Optional.empty();
        }
    }    
}

Main

package Part3.Chapter10.Chapter10_10_4;

import java.util.HashMap;
import java.util.Optional;
import java.util.Properties;

import Part3.Chapter10.Chapter10_10_4.optional.OptionalUtil;

/*
 * 10.4 Optional을 사용한 실용 예제
 * 
 * 자바 8에서 제공하는 Optional 클래스를 효과적으로 사용하려면 값이 없는 상황을 처리하던 기존의 알고리즘과는
 * 다른 과점에서 접근해야 한다. 즉, 코드 구현만 바꾸는 것이 아니라 네이티브 자바 API와 상호작용하는 방식도 바꿔야 한다.
 */
public class Main_10_4 {

    public static void main(String[] args) {
        HashMap<String, String> hashMap = new HashMap<String, String>() {
            private static final long serialVersionUID = 1L;

            {
                put("number", "123456");
                put("string", "sinnake");
            }
        };

        /*
         * 10.4.1 잠재적으로 null이 될 수 있는 대상으 Optional로 감싸기
         * 
         * 기존 자바 API에서는 null을 반환하면서 요청한 값이 없거나 어떤 문제로 계산에 실패했음을 알린다.
         * 예를 들어 Map의 get 메서드는 요청한 키에 대응하는 값을 찾지 못했을 때 null을 반환한다.
         * get 메서드의 시그너처는 우리가 고칠 수 없지만 get 메서드의 반환값은 Optional로 감쌀 수 있다.
         */
        String value = hashMap.get("key");
        System.out.println("[Normal] key - key Value : " + value);

        /*
         * map에서 반환하는 값을 Optional로 감싸서 개선할 수 있다.
         * 코드가 복잡한 기존 if-then-else를 사용하지 않고 Optional.ofNullable를 이용할 수 있다. 
         */
        value = Optional.ofNullable(hashMap.get("string")).orElse("value");
        System.out.println("[Optional] key - key Value : " + value);

        /*
         * 10.4.2 예외와 Optional
         * 
         * 자바 API는 값을 제공할 수 없을 때 null을 반환하는 대신 예외를 발생시킬 때도 있다.
         * 전형적인 예가 Integer.parseInt(String) 정적 메서드다. 이 메서드는 문자열을 정수로 바꾸지 못했을 때
         * NumberFormatException을 발생시킨다.
         * 기존 값이 null일 수도 있을 때는 null 여부를 확인했지만 예외를 발생시키는 메서드는 try/catch 블록을 사용해야 한다는 점이 다르다.
         * 
         * OptionalUtil 같은 유틸리티 클래스를 만들어서 사용하면 유용하게 사용할 수 있다.
         */
        System.out.println("10.4.2 예외와 Optional - 값이 문자열인 경우 : " + OptionalUtil.stringToInt(hashMap.get("string")).orElse(0));
        System.out.println("10.4.2 예외와 Optional - 값이 숫자인 경우 : " + OptionalUtil.stringToInt(hashMap.get("number")).orElse(0));
        System.out.println("10.4.2 예외와 Optional - null인 경우 : " + OptionalUtil.stringToInt(hashMap.get("key")).orElse(-1));

        /*
         * 기본형 Optional과 이를 사용하지 말아야 하는 이유
         * 
         * 기본형으로 특화된 OptionalInt, OptionalLong, OptionalDouble 등의 클래스를 제공한다.
         * 스트림이 많은 요소를 가질 때는 기본형 특화 스트림을 이용해서 성능을 향상시킬수 있다고 설명했다.
         * 하지만 Optional의 최대 요소 수는 한 개이므로 Optional에서는 성능 개선을 할 수 없다.
         * 
         * 그리고 기본 특화형 Optional은 map, flatMap, filter 등을 지원하지 않으므로 권장하지 않는다.
         * 게다가 기본 특화형 Optional로 생성한 결과는 다른 일반 Optional과 혼용할 수 없다.
         */

        /*
         * 10.4.3 응용
         * 
         * Properties를 읽어서 값을 초 단위의 지속시간(duration)으로 해석하는 예제이다.
         * 지속시간은 양수여야 하고 문자열이 양의 정수를 가리키면 해당 정수를 반환하고 그 외에는 0을 반환한다.
         */
        Properties props = new Properties();
        props.put("a", "5");
        props.put("b", "true");
        props.put("c", "-3");

        System.out.println("10.4.3 응용 - readDuration method : " + readDuration(props, "a"));
        System.out.println("10.4.3 응용 - readDuration method : " + readDuration(props, "b"));
        System.out.println("10.4.3 응용 - readDuration method : " + readDuration(props, "c"));
        System.out.println("10.4.3 응용 - readDuration method : " + readDuration(props, "d"));
        System.out.println("10.4.3 응용 - readDuration method : " + readDuration(props, null));

        System.out.println("10.4.3 응용 - readDurationQuiz method : " + readDurationQuiz(props, "a"));
        System.out.println("10.4.3 응용 - readDurationQuiz method : " + readDurationQuiz(props, "b"));
        System.out.println("10.4.3 응용 - readDurationQuiz method : " + readDurationQuiz(props, "c"));
        System.out.println("10.4.3 응용 - readDurationQuiz method : " + readDurationQuiz(props, "d"));
        System.out.println("10.4.3 응용 - readDurationQuiz method : " + readDurationQuiz(props, null));
    }

    public static int readDurationQuiz(Properties props, String name) {

        return Optional.ofNullable(props.getProperty(Optional.ofNullable(name).orElse("")) )
            .flatMap(OptionalUtil::stringToInt)
            .filter(p -> p > 0)
            .orElse(0);

/*
        return Optional.ofNullable(props.getProperty(Optional.ofNullable(name).orElse("")) )
            .flatMap(p -> {
                try {
                    return Optional.ofNullable(Integer.parseInt(p));
                } catch(NumberFormatException e) {
                    return Optional.empty();
                }
            })
            .filter(p -> p > 0)
            .orElse(0);    
*/
    }

    public static int readDuration(Properties props, String name) {
        String value = "";

        if(name != null ) {
            value = props.getProperty(name);
        }

        if(value != null) {
            try {
                int i = Integer.parseInt(value);

                if(i > 0) {
                    return i;
                }
            } catch(NumberFormatException e) { }
        }        

        return 0;
    }

}

요약

  • 역사적으로 프로그래밍 언어에서는 null 레퍼런스로 값이 없는 상황을 표현해왔다.
  • 자바 8에서는 값이 있거나 없음을 표현할 수 있는 클래스 java.util.Optional를 제공한다.
  • 팩토리 메서드 Optional.empty, Optional.of, Optional.ofNullable 등을 이용해서 Optional 객체를 만들 수 있다.
  • Optional 클래스는 스트림과 비슷한 연산을 수행하는 map, flatMap, filter 등의 메서드를 제공한다.
  • Optional로 값이 없는 상황을 적절하게 처리하도록 강제할 수 있다. 즉, Optional로 예상치 못한 null 예외를 방지할 수 있다.
  • Optional을 활용하면 더 좋은 API를 설계할 수 있다. 즉, 사용자는 메서드의 시그너처만 보고도 Optional값이 사용되거나 반환되는지 예측할 수 있다.
반응형