ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Part3] Java8 In action - Chapter10 - 1
    Java8 In Action 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값이 사용되거나 반환되는지 예측할 수 있다.
    반응형

    댓글

Designed by Tistory.