FeGwan-Training / FeGwan

0 stars 0 forks source link

Chapter 11. null 대신 Optional 클래스 #25

Closed MyeoungDev closed 1 year ago

MyeoungDev commented 1 year ago

Discussed in https://github.com/FeGwan-Training/FeGwan/discussions/24

Originally posted by **MyeoungDev** June 14, 2023 # 11.0.0 null 대신 Optional 클래스 - 1965년 토니 호어(Tony Hoare) 라는 영국 컴퓨터 과학자가 최초의 프로그래밍 언어 중 하나인 알골(ALGOL W) 를 설계하면서 처음 null 참조가 등장했다. - ‘구현하기가 쉬웠기 때문에 null을 도입했다고 ‘ 라고 말하며 null이 세상에 등장하게 되었다. - 따라서, null 과 관련된 예외들이 미친듯이 생겨났다. - 토니 호어는 당시 null을 만든 결정을 가리켜 ‘십억 달러짜리 실수’라고 표현했다. - 프로그래밍역사가 50년이 넘은 지금 해당 액수는 더 클것이다. 이것이 null이다. # 11.1 값이 없는 상황을 어떻게 처리할까? ```java 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; } } ``` ```java public String getCarInsuranceName(Person person) { return person.getCar().getInsurance().getName(); } ``` 위의 코드는 상당히 문제가 많다. `person` 객체에 `car` 가 존재하지 않는다면? `car` 객체는 존재하더라도 `insurance` 가 존재하지 않는다면? 이는 런타임에 `NullPointerException` 을 발생시킬 문제가 다분한 코드이다. ## 11.1.1 보수적인 자세로 NullPointerException 줄이기 매우매우 보수적으로 `NullPointerException` 을 줄이는 코드를 봐보자. ```java public String getCarInsuranceName(Person person) { if (person != null) { Car car = person.getCar(); if (car != null) { Insurance insurance = car.getInsurance(); if (insurance != null) { return insurance.getName(); } } } return "Unknown"; } ``` 위와 같이 작성이 가능하지만, 모든 변수가 null인지 의심하므로 변수를 접근할 때마다 중첩된 if문이 추가되면서 코드 들여쓰기 수준이 증가한 모습이다. 이와 같은 **반복 패턴(recurring pattern)코드**를 **깊은 의심(Deep Doubt)** 라고 부른다. 이게 반복되면 **코드 구조가 엉망이되고, 가독성이 떨어진다.** ```java public String getCarInsuranceName(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(); } ``` 앞서 본 코드를 위의 코드처럼 변경할 수 있다. 하지만, 이 코드또한 좋은 코드는 아니다. 중첩 if문을 없애면서 들여쓰기가 존재하지는 않지만, **하나의 메서드에 4개의 출구**가 생겼기 때문이다. 따라서, 각 return 문들 때문에 유지보수가 힘들어지게 된다. ## 11.1.2 null 때문에 발생하는 문제 - **에러의 근원이다.** - `NullPointerException` 은 자바에서 가장 흔히 발생하는 에러다. - **코드를 어지럽힌다.** - 때로는 **중첩된 null 확인 코드를 추가**해야 하므로 **null 때문에 코드 가독성이 떨어진다.** - **아무 의미가 없다.** - **null은 아무 의미도 표현하지 않는다.** 특히 정적 형식 언어에서 값이 없음을 표현하는 방법으로는 적절하지 않다. - **자바 철학에 위배된다.** - **자바는 개발자로부터 모든 포인터를 숨겼다.** - 하지만, 예외가 있는데 그것이 바로 null 포인터다. - **형식 시스템에 구멍을 만든다.** - **null은 무형식**이며 정보를 포함하고 있지 않으므로 **모든 참조 형식에 null을 할당할 수 있다.** - 이런 식으로 **null이 할당되기 시작하면서** 시스템의 다른 부분으로 **null이 퍼졌을 때** 애초에 **null이 어떤 의미로 사용되었는지 알 수 없다.** ## 11.1.3 다른 언어는 null 대신 무얼 사용하나? - 그루비(Groovy) - 안전 내비게이션 연산자 (Safe Navigation Operator ?.) - `def carinsuranceName = person?.car?.insurance?.name` - 호출체인에 null이 있으면 결과로 null 반환 - 하스켈(Haskell) - 선택형 값(Optional Value)을 지정할 수 있는 `Maybe` 형식 - 스칼라(Scala) - T 형식의 값을 갖거나 아무 값도 갖지 않을 수 있는 `Option[T]` 형식 제공. # 11.2 Optional 클래스 소개 Java 8은 하스켈과 스칼라의 영향을 받아서 `java.util.Optional` 라는 새로운 클래스를 제공한다. `Optional` 은 **선택형값을 캡슐화하는 클래스이다.** ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/56027263-8091-4e38-a7ed-0c1b7aa063bf) 값이 있으면 `Optional` 클래스는 값을 감싼다. 반면, 값이 없으면 `Optional.empty` 메서드로 `Optional` 을 반환한다. `Optional.empty()` 는 `Optional.empty` 는 `Optional` 의 특별한 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드다. ### 그렇다면 null과 Optional.empty()는 무엇이 다를까? - null을 참조하려 하면 `NullPointerException` 이 발생한다. - `Optional.empty()` 는 `Optional` 객체이므로 이를 다양한 방식으로 활용할 수 있다. - `Optional` 클래스를 사용하면 값이 없을 수 있음을 명시적으로 보여준다. 즉 semantic이 명확해진다. - `Optional` 을 이용하면 값이 없는 상황에 우리 데이터에 문제가 있는 것인지 아니면 알고리즘의 버그인지 명확하게 구분할 수 있다. - 반면, 일반 객체 형식을 사용할 경우 null 참조가 할당될 수 있는데 이것이 올바른 값인지 잘못된 값인지 판단할 수 없다 - 그러나, 모든 null 참조를 `Optional` 로 대치하는 것은 바람직하지 않다. - `Optional` 의 역할은 더 이해하기 쉬운 API를 설계하도록 돕는 것이다. - 즉, 메서드의 시그니처만 보고도 선택형값인지 여부를 구별할 수 있다. - `Optional` 을 언랩해서 값이 없을 수 있는 상황에 적절하게 대응하도록 강제하는 효과가 있다. ## 11.3 Optional 적용 패턴 `Optional` 형식을 이용해서 도메인 모델의 의미를 더 명확하게 만들 수 있으며, null 참조 대신 값이 없는 상황을 표현할 수 있다. ## 11.3.1 Optional 객체 만들기 - **빈 Optional** ```java /* 정적 팩토리 메서드 Optional.empty()로 빈 Optional 객체를 얻을 수 있다. */ Optional optCar = Optional.empty(); ``` - **null이 아닌 값으로 Optional 만들기** ```java /* 정적 팩토리 메서드 Optional.of로 null이 아닌 값을 포함하는 Optional을 만들 수 있다. */ Optional optCar = Optional.of(car); ``` - **null값으로 Optional 만들기** ```java /* 정적 팩토리 메서드 Optional.ofNullable로 null값을 저장할 수 있는 Optional을 만들 수 있다. * car가 null이면 빈 Optional 객체가 반환된다. */ Optional optCar = Optional.ofNullable(car); ``` ## 11.3.2 맵으로 Optional의 값을 추출하고 변환하기 `Optional` 에서 `get` 메서드를 이용하면 안에 존재하는 값을 가져올 수 있다. 그러나, `Optional` 이 비어있으면 `get` 을 호출했을 때 예외가 발생한다. 즉, `Optional` 을 잘못 사용하면 결국 null을 사용했을 때와 같은 문제를 겪을 수 있다. 따라서 다른 방법을 사용하는 것이 좋다. ```java /* 일반적인 코드 */ String name = null; if(insurance != null) { name = insurance.getName(); } /* 위와 같은 유형의 패턴에서 사용할 수 있는 Optional.map */ Optional optInsurance = Optional.ofNullable(insurance); // Optional이 값을 포함하면 map의 인수로 제공된 함수가 값을 바꾼다. // 비어있으면 아무 일도 일어나지 않는다. Optional name = optInsurance.map(Insurance::getName); ``` ## 11.3.3 flatMap으로 Optional 객체 연결 위와 같은 코드에서는 필드값을 `Optional` 로 변경해서 사용하는 것이 좋다. 각 필드값이 null일 수 있다는 것을 명시적으로 표현이 가능하고, 그에 대한 처리를 사용자에게 강제할 수 있기 때문이다. 따라서 Person, Car 클래스를 이와 같이 변경해서 사용할 수 있다. ```java public class Person { private Optional car; public Optional getCar() { return car; } } public class Car{ private Optional insuarnce; public Optional getInsuarnce() { return insuarnce; } } ``` 그리고 `map` 메서드를 이용하여 `getCarInsuranceName` 메서드를 아래와 같이 변경할 수 있다. ```java /* 변경 전 코드 */ public String getCarInsuranceName(Person person) { return person.getCar().getInsurance().getName(); } /* 변경 후 코드 */ Optional name = optPerson.map(Person::getCar) .map(Car::getInsurance) .map(Insurance::getName); ``` 그러나, 위의 코드는 컴파일 되지 않는다. `optPerson` 의 경우 `Optional` 이므로 `map` 메서드를 호출할 수 있다. 그러나, 그 이후 변수는 `Optional>` 으로 이중으로 `Optional` 객체로 감싸져 있는 형태를 띄게 된다. 따라서, 그 이후 메서드 체인에서 `map` 메서드를 호출하지 못하게 된다. 그렇다면 이 메서드를 어떻게 변경해야 될까? 정답은 `Optional.flatMap` 메서드를 이용하여 `Stream.flatMap` 메서드 처럼 이중 `Optional` 객체를 1차원 `Optional` 객체로 변경하여 사용할 수 있다. ```java /* Optional.flatMap 을 사용한 코드 */ public String getCarInsuranceNameOptional(Optional person) { return person.flatMap(Person::getCar) .flatMap(Car::getInsurance) .map(Insurance::getName) .orElse("Unknown"); // Default Value } ``` ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/41efaf1e-a7cb-49c8-98e1-58ad4aed0688) > 도메인 모델에 Optional을 사용했을 때 데이터를 직렬화할 수 없는 이유 > > 자바 언어 아키텍트인 브라이언 고츠(Brian Goetz)는 `Optional` 의 용도가 선택형 반환값을 지원하는 것이라고 명확하게 지정하였다. > > 따라서, `Optional` 클래스는 필드 형식으로 사용할 것을 가정하지 않았으므로 `Serializable` 인터페이스를 구현하지 않는다. > > 즉, 우리 도메인 모델에 `Optional` 을 사용한다면 직렬화(Serializable) 모델을 사용하는 도구나 프레임워크에 문제가 생길 수 있다. > > 이와 같은 단점에도 불구하고 여전히 `Optional` **을 사용해서 도메인 모델을 구성하는 것이 바람직하다고 책에서는 기술하고 있다. > 특히 객체 그래프에서 일부 또는 전체 객체가 null일 수 있는 상황이라면 더욱 그렇다고 한다. > > 만약, 직렬화 모델이 필요하다면 이와같이** `Optional` **값을 반환받을 수 있는 메서드를 추가하는 방식을 권장한다고 한다.** > > 참고로, **Jackson 라이브러리**는 jackson-modules-java8 project 프로젝트부터 **Optional을 지원**하여, **empty일 경우에는 null**을 **> > 값이 있는 경우**에는 **값을 꺼내서 처리**하도록 지원하고 있다 **** > ```java public class Person { private Car car; public Optional getCarAsOptional() { return Optional.ofNullable(car); } } ``` ## 11.3.4 Optional 스트림 조작 Java 9에서 `Optional` 을 포함하는 스트림 값을 쉽게 처리할 수 있도록 `Optional` 에 `stream()` 메서드를 추가 했다. `Optional` 스트림을 값을 가진 스트림으로 변환할 때 이 기능을 유용하게 활용할 수 있다. ```java public Set getCarInsuranceNames(List persons) { return persons.stream() // 사람 목록을 각 사람이 보유한 자동차의 Optional 스트림으로 변환 .map(Person::getCar) // FlatMap 연산을 이용해 Optional 을 해당 Optional 로 변환 .map(optCar -> optCar.flatMap(Car::getInsurance)) // Optional 를 해당 이름의 Optional 으로 매핑 .map(optIns -> optIns.map(Insurance::getName)) // Stream>을 현재 이름을 포함하는 Stream 으로 변환 .flatMap(Optional::stream) // 결과 문자열을 중복되지 않은 값을 갖도록 집합으로 수집 .collect(Collectors.toSet()); } ``` 위의 예제는 `Optional` 로 값이 감싸져 있으므로 기존의 Stream 보다 과정이 조금 더 복잡해 졌다. 사람이 차를 갖고있지 않거나 또는 차가 보험에 가입되어 있지 않아 결과가 비어있을 수 있다. `Optional` 덕분에 이런 종류의 연산을 `null` 걱정없이 안전하게 처리할 수 있지만 마지막 결과를 얻으려면 빈 `Optional` 을 제거하거 값을 언랩해야 한다는 문제점이 존재한다. ## 11.3.5 디폴트 액션과 Optional 언랩 빈 `Optional` 인 상황에서 기본값을 반환하도록 `orElse` 로 `Optional` 을 읽었다. `Optional` 클래스는 이 외에도 인스턴스에 포함된 값을 읽는 다양한 방법을 제공한다. - `get()` - 값을 읽는 가장 간단한 메서드면서 동시에 **가장 안전하지 않은 메서드**. - 래핑된 **값이 존재하면 값을 반환**, **없으면** `NoSuchElementException` - `Optional` 에 값이 반드시 있다고 가정할 수 있는 상황이 아니면 사용하지 않는 것이 바람직함. - `orElse()` - `Optional` 이 **값을 포함하지 않을 때 기본값을 제공.** - `orElseGet(Supplier)` - `orElse` 메서드에 대응하는 Lazy 버전의 메서드이다. - `Optional` 에 값이 없을 때만 `Supplier` 가 실행되기 때문이다. - 디폴트 값 생성 시간이 오래걸리거나, 기본값이 반드시 필요한 상황에 사용한다. - `orElseThrow(Supplier consumer)` - `Optional` 이 **비어있을 때 예외를 발생**시킨다. - `ifPresent(Consumer consumer)` - 값이 존재할 때 인수로 넘겨준 동작을 실행할 수 있다. - 값이 없으면 아무 일도 일어나지 않는다. - `ifPresentOrElse(Consumer action, Runnable emptyAction)` - `Optional` 이 비었을 때 실행할 수 있는 `Runnable` 을 인수로 받는다는 점만 `ifPresent` 와 다르다. ## 11.3.6 두 Optional 합치기 두개의 인자를 매개변수로 받는 복잡한 비즈니스 로직이 구현된 메서드가 있다고 가정하겠다. ```java public Insurance findCheapestInsurance(Person person, Car car) { // 다양한 보험회사가 제공하는 서비스 조회 // 모든 결과 데이터 비교 return cheapestCompany; } ``` 위의 메서드에서 두 인자를 `Optional` 로 받아서 null-safe 한 메서드를 구현할 경우는 아래와 같다. ```java public Optional nullSafeFindCheapestInsurance( Optional, Optional car) { if(person.isPresent() && car.isPresent) { return Optional.of(findCheapestInsurance(person.get(), car.get())); } else { return Optional.empty(); } } ``` 위 메서드의 장점은 `person` 과 `car` 의 시그니처만으로 둘 다 아무 값도 반환하지 않을 수 있다는 정보를 명시적으로 보여준다는 것이다. 그렇다면 앞서 얘기했던 `map` 과 `flatMap` 을 이용하여 코드를 변경할 수 있지 않을까? ```java public Optional nullSafeFindCheapestInsurance( Optional person, Optional car) { return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c))); } ``` (솔직히 말해서 바로 위와 같은 코드를 짜내기는 숙련이 부족한것 같다.) - 첫 번째 `Optional` 에 `flatMap` 을 호출했으므로 첫 번째 `Optional` 이 비어있다면 인수로 전달한 람다 표현식이 실행되지 않고 그대로 빈 `Optional` 을 반환한다. - 반면 `person` 값이 있으면 `flatMap` 메서드에 필요한 `Function` 의 입력으로 `person` 을 사용한다. - 두 번째 `Optional` 에 `map` 을 호출하므로 `Optional` 이 `car` 값을 포함하지 않으면 `Function` 은 빈 `Optional` 을 반환하므로 결국 해당 메서드는 빈 `Optional` 을 반환하게 된다. - 마지막으로 모두 존재하면 `map` 메서드로 전달한 람다 표현식이 `findCheapestInsurance` 메서드를 안전하게 호출 할 수 있다. ## 11.3.7 필터로 특정값 거르기 `Optional` 의 `filter` 메서드는 `Predicate` 를 인수로 받아, 조건과 일치하면 그 값을 반환하고 그렇지 않으면 빈 `Optional` 객체를 반환한다. `filter` 메서드를 통해 특정 값을 가져올 수 있게 된다. ```java Optional optInsurance = ...; optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName())) .ifPresent(x -> System.out.println("ok")); ``` ```java /* minAge 이상의 나이를 먹은 사람만 선택하도록 Perdicate를 설정해서 * filter 메서드에 전달하는 방식으로 Optional filtering 하는 예제 */ public String getCarInsuranceName(Optional person, int minAge) { return person.filter(p -> p.getAge() >= minAge) .flatMap(Person::getCar) .flatMap(Car::getInsurance) .map(Insurance::getName) .orElse("Unknown"); } ``` # 11.4 Optional을 사용한 실용 예제 ## 11.4.1 잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기 ```java /* Map 에서 key 값으로 데이터 조회하기 */ Object value = map.get("key"); /* to Optional */ Optional value = Optional.ofNullable(map.get("key")); ``` - 위의 코드처럼 `Optional` 로 감싸주면 if-else 문 보다 깔끔한 null-safe한 코드를 작성할 수 있다. ## 11.4.2 예외와 Optional 클래스 문자열을 숫자형으로 변환할 경우 대표적으로 `Integer.parseInt(String str)` 메서드를 사용한다. 이때, 정수형으로 변경하지 못할 경우 `NumberFormatException` 예외를 발생시킨다. 이러한 문제를 `Optional` 을 통해 해결할 수 있다. ```java public static Optional stringToInteger(String s) { try { return Optional.of(Integer.parseInt(s)); } catch(NumberFormatException e) { return Optional.empty(); } } ``` ## 11.4.3 기본형 특화 Optional을 사용하지 말아야 하는 이유 - `Optional` 도 스트림처럼 기본형으로 특화된 `OptionalInt` , `OptionalLong` , `OptionalDouble` 등의 클래스를 제공한다. - 스트림의 경우 때에 따라 기본형 특화스트림으로 성능을 향상시킬 수 있었다. - 그러나, `Optional` 의 경우 최대 요소 수는 한 개이므로 기본형 특화 클래스로 성능을 개선할 수 없다. - 또한, `map` , `flatMap` 등의 메서드를 지원하지 않으므로 기본형 특화 `Optional` 을 사용할 것을 권장하지 않는다.
HoFe-U commented 1 year ago

수고하셨습니다.