(모던 Java 인 액션) 11장 null 대신 Optional 클래스

  • by

✏️(모던 Java 인 액션, 전문가를 위한 Java 8, 9, 10 기법 가이드) 스터디 관련 본 내용을 정리한 문장입니다.


📌 이 장의 내용

  • null 참조 문제와 null을 멀리하는 이유
  • null 대신 Optional: null에서 보안 도메인 모델을 다시 구현
  • 옵션 활용: null 인증 코드 삭제
  • Optional에 저장된 값을 확인하는 방법
  • 값이 없을 수 있는 상황을 고려한 프로그래밍

11.1 값이없는 상황을 어떻게 처리합니까?

11.1.1 보수적 인 자세로 NullPointerException을 줄입니다.

대부분의 프로그래머는 null 예외 문제를 해결하는 데 필요한 위치에 다양한 null 확인 코드를 추가합니다.

보다 보수적인 프로그래머는 반드시 필요하지 않은 경우에도 null 확인 코드를 추가하여 신뢰성을 높이려고 합니다.

이것에 의해, NullPointerException 가 발생하는 경우를 줄일 수가 있습니다.

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가 추가되면 코드의 들여쓰기 수준이 높아집니다.

이러한 반복 패턴 코드 깊은 의심라고 부릅니다.

즉, 변수가 null 라고 의심되어 중첩된 if 블록을 추가하면(자), 코드의 들여쓰기 레벨이 증가합니다.

이러한 패턴을 반복하면 코드 구조가 혼란스럽고 읽기 쉽습니다.

이러한 문제를 해결하려면 다른 방법을 적용해야 합니다.

다음 예제는 다른 방법으로 이 문제를 해결하는 코드입니다.

public String getCarInsuranceName(Person persone) {
   if (person == null) { // null 확인 코드마다 출구가 생깁니다.

return "Unknown"; } Car car = person.getCar(); if (car == null) { // null 확인 코드마다 출구가 생깁니다.

return "Unknown"; } Insurance insurance = car.getInsurance(); if (insurance == null) { // null 확인 코드마다 출구가 생깁니다.

return "Unknown"; } return insurance.getName(); }

위의 코드는 중첩된 if 문을 제거하기 위해 모든 변수가 null인지 여부를 확인하는 방법을 사용합니다.

그러나 이 방식을 코드가 복잡하고 읽기 쉬워지고, 메소드의 출구가 4개가 되어 메인터넌스가 어려워집니다.

또한 “Unknown”이라는 문자열이 세 곳에서 중복 사용되며 코드를 수정할 때 오류와 같은 문제가 발생할 수 있습니다.

이러한 문제를 해결하려면 문자열 “Unknown”을 상수로 정의하거나 null 대신 다른 값으로 바꾸는 것이 좋습니다.

메소드로부터 돌려주어지는 값이 같은 형태인 경우, 그 값의 추론이 가능한 경우도 있습니다.

이러한 경우를 활용하면 코드의 가독성과 서비스 가능성을 향상시킬 수 있습니다.

11.1.2 null 문제

  • 오류의 출처입니다 : NullPointerException은 Java에서 가장 일반적인 오류입니다.

  • 코드를 방해 : 때로는 중첩 된 null 확인 코드를 추가해야하므로 null로 인해 코드 가독성이 떨어집니다.

  • 의미가 없다 : null은 어떤 의미도 표현하지 않습니다.

    특히 정적 형식 언어에 값이 없음을 나타내는 방법에는 적합하지 않습니다.

  • 자바 철학을 위반하는 : Java는 개발자의 모든 포인터를 숨겼습니다.

    하지만 예외가 있지만 null 포인터입니다.

  • 포맷 시스템에 구멍 만들기: null은 무형식이며 정보를 포함하지 않으므로 모든 참조 형식에 null을 할당할 수 있습니다.

    이런 식으로 null이 할당되기 시작하고 시스템의 다른 부분에 null이 퍼졌을 때 처음에 null이 어떤 의미로 사용되었는지 알 수 없습니다.


11.2 Optional 클래스 소개

Java 8은 java.util.Optional 라는 새로운 클래스를 제공합니다.

Optional은 선택적 값을 캡슐화하는 클래스입니다.

값이 있으면 Optional 클래스는 값을 감싸고 값이 없으면 Optional.empty 메서드를 사용하여 Optional을 반환합니다.

Optional.empty는 특별한 Optional 싱글 톤 인스턴스를 반환하는 정적 팩토리 메서드입니다.

null 를 참조하려고 하면 NullPointerException 가 발생합니다만, Optional.empty( ) 는 Optional 객체이므로, 이것을 다양한 방법으로 활용할 수 있습니다.


11.3 선택적 적용 패턴

Optional 유형은 도메인 모델의 의미를 명확히 하고 값이 없는 상황을 표현하는 데 사용됩니다.

Optional 객체를 사용하려면 메서드를 사용하여 값을 추출하거나 변환하고 기본값을 설정하거나 값이 존재하는지 확인할 수 있습니다.

11.3.1 Optional 객체 만들기

Optional은 값이 있거나 없을 수 있는 개체를 처리할 때 사용되며, Optional 개체를 만드는 방법에는 Optional.of(), Optional.ofNullable(), Optional.empty() 등이 있습니다.

빈 Optional
정적 팩토리 메서드 Optional.empty 로 빈 Optional 객체를 가져올 수 있습니다.

Optional<Car> optCar = Optional.empty();

NULL이 아닌 값으로 Optional 작성
또는 정적 팩토리 메소드 Optional.of를 사용하여 널이 아닌 값을 포함하는 Optional을 작성할 수 있습니다.


car 가 null 의 경우, nullPointerException 가 슬로우 됩니다.

Optional<Car> optCar = Optional.of(car);

null 값으로 Optional 만들기
마지막으로 정적 팩토리 메서드 Optional.ofNullable에서 null 값을 저장할 수 있는 Optional을 만들 수 있습니다.


car 가 null 의 경우, 하늘의 Optional 오브젝트가 돌려주어집니다.

Optional<Car> optCar = Optional.ofNullable(car);

Optional은 get() 메서드에 값을 가져올 수 있지만 빈 Optional에서 get()을 호출하면 예외가 발생합니다.

이를 방지하기 위해 Optional은 명시적 null 검사를 제거하는 기능을 제공합니다.

이 기능은 스트림 조작에서 영감을 받았습니다.

11.3.2 맵으로 Optional 값 추출 및 변환

일반적으로 오브젝트에서 정보를 추출할 때 Optional을 사용하는 경우가 많습니다.

다음 코드와 같이 액세스하기 전에 null인지 확인하십시오.

String name = null;
if (insurance !
= null) { name = insurance.getName(); }

이 유형의 패턴에서 사용할 수 있도록 다음 코드와 마찬가지로 Optional은 map 메소드를 지원합니다.

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

Optional의 map 메소드는 스트림의 map 메소드와 유사합니다.

Optional에 값이 포함되어 있으면 지정된 함수를 적용하여 값을 변환하고 Optional이 비어 있으면 아무 작업도 수행하지 않습니다.

Optional은 최대 요소 수가 하나인 데이터 콜렉션으로 생각할 수 있습니다.

11.3.3 flatMap으로 Optional 객체 연결

다음과 같이 맵을 사용하여 코드를 다시 구현할 수 있습니다.

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

위의 코드는 컴파일되지 않았습니다.

optPeople.map()의 결과는 Optional > 형식이기 때문입니다.

이 문제를 해결하려면 flatMap 메서드를 사용할 수 있습니다.

flatMap은 함수를 적용하여 생성된 모든 스트림을 하나로 병합하여 1차원 Optional로 평준화합니다.

11.3.4 Optional 스트림 조작

Java 9에서는 Optional에 stream() 메소드를 추가하여 Optional을 포함하는 스트림을 쉽게 처리할 수 있습니다.

이 기능은 Optional 스트림을 값이 있는 스트림으로 변환하는 데 유용합니다.

11.3.4절에서는 다른 예를 사용하여 Optional 스트림을 어떻게 다루는지 설명한다.

이 예에서는 Person/Car/Insurance 코드에서 List 를 인수로 받고 자동차를 소유한 사람이 가입한 보험회사의 이름을 Set 로 반환하는 메서드를 구현합니다.

public Set<String> getCarInsuranceNames(List<Person> persons) {
   return persons.stream()
                 .map(Person::getCar)
                 .map(optCar -> optCar.flatMap(Car::getInsurance))
                 .map(optCar -> optIns.map(Insurance::getName))
                 .flatMap(Optional::new)
                 .collect(toSet());

위의 예에서 getCar() 메서드는 Optional 를 반환하기 때문에 사람이 차를 갖지 않는 상황이 있고 첫 map 변환을 실행하면 stream > 를 가져옵니다.

그런 다음 두 맵 작업을 사용하여 Optional Optional 로 변환하고 스트림 대신 각 요소를 Optional 로 변환합니다.

최종 결과로 Stream>를 얻지만 사람이 차를 가지고 있지 않거나 자동차가 보험에 가입하지 않았기 때문에 결과가 비어있을 수 있습니다.

Optional을 사용하면 NULL에 대해 걱정하지 않고 작업을 안전하게 처리할 수 있지만 최종 결과를 얻으려면 빈 Optional을 제거하고 값을 래핑해야 하는 문제가 있습니다.

다음 코드와 같이 filter, map을 차례로 이용하여 결과를 얻을 수 있습니다.

Stream<Optional<String>> stream = ...
Set<String> result = stream.filter(Optional::isPresent)
                           .map(Optional::get)
                           .collect(toSet()):

Optional 클래스의 stream() 메서드를 사용하면 스트림의 요소를 한 번의 조작으로 Optional 객체가 포함된 스트림으로 변환할 수 있습니다.

이 메서드를 flatMap 메서드와 함께 사용하면 두 수준의 스트림을 일반 스트림으로 변환할 수 있습니다.

이 기법을 사용하면 Optional을 제거하고 빈 Optional을 건너뛸 수 있습니다.

11.3.5 기본 동작 및 선택적 언랩

Optional 클래스는 orElse를 포함한 다양한 메소드를 제공하며 빈 Optional의 경우 기본값이나 예외를 반환할 수 있습니다.

  • get() 메소드는 Optional 에 값이 있는 경우는 그 값을 돌려주어, 값이 없는 경우는 NoSuchElementException 예외를 슬로우 합니다.

    이 때문에, get() 메소드를 사용하는 것은, 중첩된 null 확인 코드를 사용하는 것과 크게 변하지 않습니다.

  • orElse 메소드를 사용하면 Optional에 값이 포함되어 있지 않을 때 기본값을 지정할 수 있습니다.

  • orElseGet (Supplier other)는 Optional에 값이 없는 경우에만 Supplier를 실행하는 버전의 메서드입니다.

    이것은, 디폴트의 메소드를 작성하는 시간, 또는 Optional 가 하늘의 경우에만 디폴트치를 생성하고 싶은 경우에 편리합니다.

  • ifPresent(Consumer consumer)를 사용하면 Optional에 값이 있을 때 인수에 전달된 조치를 수행할 수 있습니다.

    값이 없으면 아무 일도 일어나지 않습니다.

Java 9에서는 다음 인스턴스 메소드가 추가되었습니다.

  • ifPresentOrElse (Consumer action, Runnable (emptyAction) 는, Optional 가 하늘의 경우에 실행할 수 있는 Runnable 를 인수로서 받는다고 하는 점만 ifPresent 와는 다른 메소드입니다.

11.3.6 두 가지 옵션 조합

Dayun은 Person과 Car 정보를 사용하여 가장 저렴한 보험료를 제공하는 보험 회사를 찾기 위해 몇 가지 복잡한 비즈니스 로직을 구현하는 외부 서비스가 있다고 가정하여 구현 한 코드입니다.

public Insurance findCheapestInsurance(Person person, Car car) {
   // 다양한 보험회사가 제공하는 서비스 조회
   // 모든 결과 데이터 비교
   return cheapestCompany;
}

두 Optional 인스턴스를 모두 인수로 사용하고 Optional 를 반환하는 null 안전 메서드를 구현해야 한다고 가정합니다.

인수로 전달한 값 중 하나가 비어 있으면 빈 Optional 를 반환합니다.

이 시점에서 Optional 클래스의 isPresent() 메서드를 사용하여 값이 존재하는지 확인할 수 있습니다.

따라서 isPresent를 사용하여 다음과 같이 코드를 구현할 수 있습니다.

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

이 메서드의 장점은, person 과 car 의 시그니쳐만으로 어느쪽도 값을 돌려주지 않을 가능성이 있다고 하는 정보를 명시적으로 나타내는 것입니다.

그러나 구현 코드에는 null 확인 코드와 크게 다른 점이 없다는 단점이 있습니다.

Optional 클래스와 Stream 인터페이스는, map 메소드와 flatMap 메소드에 가세해, 다양한 같은 기능을 공유합니다.

11.3.7 필터로 특정 값을 여과

오브젝트의 메소드를 호출해 프로퍼티을 확인할 때는, 그 오브젝트가 null 인 것을 확인하지 않으면 안전하게 작업을 실시할 수가 있습니다.

예를 들어, 보험 회사 이름이 “CambridgeInsurance”인지 확인해야 하는 경우 Insurance 개체가 null인지 확인한 다음 getName 메서드를 호출하는 것이 안전한 방법입니다.

Insurance insurance = ...;
if (insurance !
= null && "CambridgeInsurance".equals(insurance.getName())) { System.out.println("ok"); } // Optional 객체에 filter 메서드를 이용해서 다음과 같이 코드를 재구현 할 수 있습니다.

Optional<Insurance> optInsurance = ...; optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName())) .ifPresent(x -> System.out.println("ok"));

filter 메서드는 Optional 객체에 프레디케이트를 적용하고 값이 일치하면 값을 반환하고 일치하지 않으면 빈 Optional을 반환하는 메서드입니다.

Optional이 비어 있으면 filter는 아무 작업도 수행하지 않습니다.

값이 있으면 프레디케이트를 적용하고 일치하지 않으면 빈 Optional으로 만들고 일치하면 아무 일도 일어나지 않습니다.


11.4 Optional을 사용한 실용 예

새로운 Optional 클래스를 효과적으로 사용하려면 잠재적으로 존재하지 않는 값을 처리하는 방법을 대체해야 합니다.

즉, 코드의 구현만을 변경하는 것이 아니라, 네이티브 Java API 와 상호 작용하는 방법도 변경할 필요가 있습니다.

옵션 기능을 사용할 수 있도록 코드에 작은 유틸리티 메서드를 추가하여 문제를 해결할 수 있습니다.

11.4.1 잠재적으로 null이 될 수 있는 타겟을 Optional에 랩한다

레거시 Java API에서는 요청된 값이 없거나 계산에 실패할 때 null을 반환하는 경우가 많습니다.

그러나 null을 반환하는 것보다 Optional을 반환하는 것이 더 안전하고 바람직합니다.

예를 들어, Map의 get 메소드는 반환값을 Optional에 래핑할 수 있습니다.

이렇게 하면 코드에서 null 검사를 수행하지 않고 NPE를 방지할 수 있습니다.

Map 형식의 맵이 있어 캐릭터 라인 ‘key’ 에 대응하는 값이 없는 경우는 null 가 돌려주어집니다.

map이 반환하는 값은 Optional.ofNullable을 사용하여 개선할 수 있습니다.

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

이러한 코드를 사용하여 null 값을 Optional로 안전하게 변환할 수 있습니다.

11.4.2 예외 및 Optional 클래스

Java API에서는 값을 제공할 수 없을 때 null 대신 예외를 throw할 수 있습니다.

그러나 이러한 경우에도 Optional을 사용하여 null 검사를 수행하지 않고 NPE를 피할 수 있습니다.

이렇게 하려면 parseInt를 감싸는 작은 유틸리티 메서드를 구현하고 Optional을 반환하도록 모델링할 수 있습니다.

예를 들어 stringToInt라는 메서드를 구현하여 문자열을 Optional 로 변환할 수 있습니다.

이를 포함하는 OptionalUtility 클래스를 작성하면 거대한 try/catch 논리를 사용하지 않고 문자열을 Optional로 변환할 수 있습니다.

11.4.3 기본 유형 Optional을 사용하지 마십시오.

Optional 클래스에는, 기본형에 특화한 OptionalInt, OptionalLong, OptionalDouble 등이 준비되어 있습니다.

그러나 Optional은 요소의 최대 개수가 하나만 있기 때문에 기본 특수 클래스에서 성능을 향상시킬 수 없으며 map, flatMap, filter 등의 메서드를 지원하지 않으므로 권장하지 않습니다.

또, 기본형 특화 Optional과 일반 Optional은 혼용할 수 없습니다.