[모던 자바 인 액션] Chapter 11. null 대신 Optional

SunYerim·2024년 3월 3일
0

언어

목록 보기
11/11

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

11.1.2 null 때문에 발생하는 문제

  • 에러의 근원이다.
  • 코드를 어지럽힌다.
  • 아무 의미가 없다.
  • 자바 철학에 위배된다.
  • 형식 시스템에 구멍을 만든다.

자바 8에서는 ‘선택형값’ 개념의 영향을 받아서 java.util.Optional<T> 라는 새로운 클래스를 제공한다.


11.2 Optional 클래스 소개

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

값이 있으면 Optional 클래스는 값을 감싼다. 반면 값이 없으면 Optional.empty 메서드로 Optional을 반환한다.

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

null을 참조하려 하면 NullPointerException이 발생하지만 Optional.empty()는 Optional 객체이므로 다양한 방식으로 활용이 가능하다.

Optional을 사용하면 값이 없는 상황이 우리 데이터에 문제가 있는 것인지 아니면 알고리즘의 버그인지 명확하게 구분할 수 있다. 모든 null 참조를 Optional로 대치하는 것은 바람직하지 않다. Optional의 역할은 더 이해하기 쉬운 API를 설계하도록 돕는 것이다. 즉, 메서드의 시그니처만 보고도 선택형값인지 여부를 구별할 수 있다.


11.3 Optional 적용 패턴

Optional 형식을 이용해서 도메인 모델의 의미를 더 명확하게 만들 수 있었으며 null 참조 대신 값이 없는 상황을 표현할 수 있음을 확인했다.

11.3.1 Optional 객체 만들기

빈 Optional

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

null이 아닌 값으로 Optional 만들기

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

car가 null이라면 즉시 NullPointerException이 발생한다.

null값으로 Optional 만들기

정적 팩토리 메서드 Optional.ofNullable로 null값을 저장할 수 있는 Optional을 만들 수 있다.

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

car가 null이면 빈 Optional 객체가 반환된다.

Optional에서 get 메서드를 이용해서 값을 가져올 수 있는데, Optional이 비어있으면 get을 호출했을 때 예외가 발생한다.

11.3.2 맵으로 Optional의 값을 추출하고 변환하기

보통 객체의 정보를 추출할 때는 Optional을 사용할 때가 많다.

예를 들어 보험회사의 이름을 추출한다고 가정했을 때, 이름 정보에 접근하기 전 insurance가 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 메서드와 개념적으로 비슷하다. 스트림의 map은 스트림의 각 요소에 제공된 함수를 적용하는 연산이다.

Optional 객체를 최대 요소의 개수가 한 개 이하인 데이ㅒ터 컬렉션으로 생각할 수 있다. Optional이 값을 포함하면 map의 인수로 제공된 함수가 값을 바꾼다. Optional이 비어있으면 아무 일도 일어나지 않는다.

public String getCarInsuranceName(Person person) {
	return person.getCar().getInsurance().getName();
}

11.3.3 flatMap으로 Optional 객체 연결

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

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

변수 optPerson의 형식은 Optional이므로 map 메서드를 호출할 수 있다. 그러나, getCar은 Optional 형식의 객체를 반환한다. map 연산의 결과는 Optional<Optional<Car>> 형식의 객체다.

getInsurance는 또 다른 Optional 객체를 반환하므로 getInsurance 메서드를 지원하지 않는다. ⇒ 중첩 Optional 객체 구조

이를 해결해보자.

스트림의 flatMap은 함수를 인수로 받아서 다른 스트림을 반환하는 메서드다.

보통 인수로 받은 함수를 스트림의 각 요소에 적용하면 스트림의 스트림이 만들어진다. 하지만 flatMap은 인수로 받은 함수를 적용해서 생성된 각각의 스트림에서 콘텐츠만 남긴다.

함수를 적용해서 생성된 모든 스트림이 하나의 스트림으로 병합되어 평준화된다.

이를 적용해 위의 코드를 수정해보자.

public String getCarInsuranceName(Optional<Person> person) {
	return person.flatMap(Person::getCar)
							 .flatMap(Car::getInsurance)
							 .map(Insuracne::getName)
							 .orElse("Unknown"); // 결과 Optional이 비어있으면 기본값 사용
}

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

Optional을 사용하므로 도메인 모델과 관련한 암묵적인 지식에 의존하지 않고 명시적으로 형식 시스템을 정의할 수 있었다.

  1. Person을 Optional로 감싼 다음에 flatMap(Person::getCar)를 호출
    1. 첫번째 단계: Optional 내부의 Person에 Function을 적용한다. 여기서는 Person의 getCar메서드가 Funcion이 된다. getCar 메서드는 Optional를 반환하므로 Optional 내부의 Person이 Optional로 변환되면서 중첩 Optional이 생성된다. 따라서 flatMap 연산으로 Optional을 평준화한다. (평준화: 두 Optional을 합치는 기능을 수행하면서 둘 중 하나라도 null이면 빈 Optional을 생성하는 연산. flatMap을 빈 Optional에 호출하면 아무 일도 일어나지 않고 그대로 반환된다.)
    2. 두 번째 단계: 첫 번째 단계와 비슷하게 Optional를 Optional로 변환한다.
    3. 세 번째 단계: Optional를 Optional으로 변환한다. 해당 단계에서 Insurance.getName은 String을 반환하므로 flatMap을 사용할 필요가 없다.

Optional이 비어있을 때 기본값을 제공하는 orElse라는 메서드를 사용.

11.3.4 Optional 스트림 조작

Optional을 포함하는 스트림을 쉽게 처리할 수 있도록 Optional에 stream()메서드를 추가했다.

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

위의 세 번의 변환 과정을 거친 결과 Stream<Optional>를 얻는데 사람이 차를 가지고 있지 않거나 또는 차가 보험에 가입되어 있지 않아 결과가 비어있을 수 있다.

Optional 덕분에 널 걱정없이 처리가 가능하지만, 결과를 얻으려면 빈 Optional을 제거하고 값을 언랩해야 한다는 것이 문제다.

아래와 같이 filter, map을 순서적으로 이용해 결과를 얻을 수 있다.

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

해당 메서드는 각 Optional이 비어있는지 아닌지에 따라 Optional을 0개 이상의 항목을 포함하는 스트림으로 변환한다.

11.3.5 디폴트 액션과 Optional 언랩

  • get()은 값을 읽는 가장 간단한 메서드면서 동시에 가장 안전하지 않는 메서드다. 메서드 get은 래핑된 값이 있으면 해당 값을 반환하고 값이 없으면 NoSuchElementException을 발생시킨다. Optional에 값이 반드시 있다고 가정할 수 있는 상황이 아니면 get메서드 사용을 지양하자!
  • orElse 메서드를 사용하면 Optional이 값을 포함하지 않을 때 기본값을 제공할 수 있다.
  • orElseGet(Supplier<? extends T> other)는 orElse 메서드에 대응하는 게으른 버전의 메서드다. Optional에 값이 없을 때만 Supplier가 실행된다.
  • orElseThrow(Supplier<? extends X> exceptionSupplier)는 Optional이 비어있을 때 예외를 발생시킨다는 점에서 get 메서드와 비슷하나 발생시킬 예외의 종류를 선택할 수 있다.
  • ifPresent(Consumer<? super T> consumer)를 이용함녀 값이 존재할 때 인수로 넘겨준 동작을 실행할 수 있으며 값이 없으면 아무 일도 일어나지 않는다.
  • ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction). 해당 메서드는 Optional이 비었을 때 실행할 수 있는 Runnable을 인수로 받는다는 점만 ifPresent와 다르다.

11.3.6 두 Optional 합치기

두 Optional을 인수로 받아서 Optional를 반환하는 null 안전 버전의 메서드를 구현해야한다고 가정하자. 인수로 전달한 값 중 하나라도 비어있으면 빈 Optional를 반환한다.

Optional 클래스는 Optional이 값을 포함하는지 여부를 알려주는 isPresent 메서드도 제공한다.

public Optional<Insurance> nullSafeFindCheapstInsurance(
				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의 시그니처만으로 둘 다 아무 값도 반환하지 않을 수 있다는 정보를 명시적으로 보여준다는 것이다.

11.3.7 필터로 특정값 거르기

Optional 객체에 filter 메서드를 이용해서 코드를 작성할 수 있다.

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

filter 메서드는 프레디케이트를 인수로 받는다.

Optional 객체가 값을 가지며 프레디케이트와 일치하면 filter 메서드는 그 값을 반환하고 그렇지 않으면 빈 Optional 객체를 반환한다.

Optional객체는 최대 한 개의 요소를 포함할 수 있는 스트림과 같다


11.4 Optional을 사용한 실용 예제

11.4.1 잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기

null을 반환하는 것보다는 Optional을 반환하는 것이 더 바람직하다. get 메서드의 반환값은 Optional로 감쌀 수 있다.

Map<String, Object> 형식의 맵이 있는데, 다음처럼 key로 값에 접근한다고 가정하자.

Object value = map.get("key");

key에 해당하는 값이 없으면 null이 반환될 것이다.

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

map에서 반환하는 값을 Optional로 감싸서 이를 개선한 것이다.

11.4.2 예외와 Optional 클래스

정수로 변환할 수 없는 문자열 문제를 빈 Optional로 해결할 수 있다. 즉, parseInt가 Optional을 반환하도록 모델링할 수 있다.

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

11.4.3 기본형 Optional을 사용하지 말아야 하는 이유

스트림이 많은 요소를 가질 때는 기본형 특화 스트림을 이용해서 성능을 향상시킬 수 있다.

하지만 Optional의 최대 요소 수는 한 개 이므로 Optional에서는 기본형 특화 클래스로 성능을 개선할 수 없다.

profile
내 안에 있는 힘을 믿어라.

0개의 댓글