본문 바로가기

프로그래밍(TA, AA)/JVM 언어

[자바] 자바8 Optional 클래스

자바에서 null 레퍼런스를 사용하면서 발생할수 있는 이론적/실용적 문제를 확인하자.

 

  1. NullPointerException은 자바에서 가장 흔히 발생하는 에러다.
  2. 때로는 중첩된 null 확인 코드를 추가해야 하므로 null 때문에 코드 가독성이 떨어진다.
  3. null은 아무 의미도 표현하지 않는다. 특히 정적 형식 언어에서 값이 없음을 포현하는 방법으로는 적절하지 ㅇ낳다.
  4. 자바는 개발자로부터 모든 포인터를 숨겼다. 하지만 예외가 있는데 그것이 바로 null 포인터이다.
  5. null은 무형식이며 정보를 포함하고 있지 않으므로 모든 레퍼런스 형식에 null을 할당할 수 있다. 이런 식으로 null이 할당되기 시작하면서 시스템의 다른 부분으로 null이 퍼졌을때 애초에 null이 어떤 의미로 사용되었는지 알 수 없다.

자바8은 '선택형값' 개념의 영향을 받아서 java.util.Optional<T>라는 새로운 클래스를 제공한다. java.util.Optional<T>를 이용해서 값이 없는 상황을 모델링할 수 있다. 궁극적으로 더 좋은 API를 설계하게 된다. API 사용자는 메서드의 시그니처만 보고도 선택형값을 기대해야 하는지 판단할 수 있다.

 


Optional 클래스 소개

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

 

Optional 클래스를 사용하면서 모델의 의미가 더 정화해질수 있다. 특정 모델이 있을수도 아닐수도 있음을 코드로 명확히 설명한다.

 

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

 

 

Optional 적용 패턴

Optional을 사용하려면 Optional 객체를 만들어야 한다. 다양한 방법으로 Optional 객체를 만들 수 있다.

 

 

빈 Optional

정적 팩토리 메서드 Optional.empty로 빈 Optional 객체를 얻을 수 있다.

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

 

null이 아닌 값으로 Optional 만들기

또는 정적 팩토리 메서드 Optional.of로 null이 아닌 값을 포함하는 Optional을 만들 수 있다.

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

이제 car가 null이라면 즉시 NullPointerException이 발생한다. (Optional을 사용하지 않았다면 car의 프로퍼티에 접근하려 할때 에러가 발생했을 것이다)

 

null값으로 Optional 만들기

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

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

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

 

 

Optional에서는 get 메서드를 이용해서 Optional의 값을 가져올 수 있다. 그런데 Optional이 비어있으면 get을 호출했을때 예외가 발생한다. 즉, Optional을 잘못 사용하면 결국 null을 사용했을 때와 같은 문제를 겪을 수 있다. 따라서 먼저 Optional로 명시적인 검사를 제거할 수 있는 방법을 살펴보겠다.

 

 

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

보통 객체의 정보를 추출할 때는 Optional을 사용할 때가 많다. 다음 코드처럼 이름 정보에 접근하기 전에 insurance가 null인지 확인해야 한다.

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

 

이런 유형의 패턴에 사용할 수 있도록 Optional은 map 메서드를 지원한다.

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

 

Optional의 map 메서드는 스트림의 map 메서드와 개념적으로 비슷하다. 스트림의 map은 스트리므이 각 요소에 제공된 함수를 적용하는 연산이다. 여기서 Optional 객체를 최대 요소의 개수가 한 개 이하인 데이터 컬렉션으로 생각할 수 있다. Optional이 값을 포함하면 map의 인수로 제공된 함수가 값을 바꾼다. Optional이 비어있으면 아무 일도 일어나지 않는다.

 

 

flatMap으로 Optional 객체 연결

스트림의 flatMap은 함수를 임수로 받아서 다른 스트림을 반환하는 메서드다. 보통 인수로 받은 함수를 스트림의 각 요소에 적용하면 스트림의 스트림이 만들어진다. 하지만 flatMap은 인수로 받은 함수를 적용해서 생성된 각각의 스트림에서 콘텐츠만 남긴다. 즉, 함수를 적용해서 생성된 모든 스트림이 하나의 스트림으로 병합되어 평준화된다.

 

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

즉, null을 확인하느라 조건 분기문을 추가해서 코드를 복잡하게 만들지 않으면서도 쉽게 이해할 수 있는 코드를 완성했다. flatMap 연산으로 Optional을 평준화한다. 평준화 과정이란 이론적으로 두 Optional을 합치는 기능을 수행하면서 둘 중 하나라도 null이면 빈 Optional을 생성하는 연산이다. flatMap을 빈 Optional에 호출하면 아무 일도 일어나지 않고 그대로 반환된다.

 

Optional이 비어있을때 디폴트값을 제공하는 orElse라는 메서드를 사용했다. Optional은 디폴트값을 제공하거나 Optional을 언랩하는 다양한 메서드를 제공한다. 

 

Optional을 인수로 받거나 Optional을 반환하는 메서드를 정의한다면 결과적으로 이 메서드를 사용하는 모든 사람에게 이 메서드가 빈 값을 받거나 빈 결과를 반환할 수 있음을 잘 문서화해서 제공하는 것과 같다.

 


  • get()은 값을 읽는 가장 간단한 메서드면서 동시에 가장 안전하지 않은 메서드다. 메서드 get은 래핑된 값이 있으면 해당 값을 반환하고 값이 없으면 NoSuchElementException을 발생시킨다. 따라서 Optional에 값이 반드시 있다고 가정할 수 있는 상황이 아니면 get 메서드를 사용하지 않는것이 바람직하다. 결국 이 상황은 중첩된 nul 확인 코드를 넣는 상황과 크게 다르지 않는다.
  • orElse 메서드를 이용하면 Optional이 값을 포함하지 않을때 디폴트값을 제공할 수 있다.
  • orElseGet(Supplier<? extends T> other)는 orElse 메서드에 대응하는 게으른 버전 메서드다. Optional에 값이 없을때만 Supplier가 실행되기 때문이다. 디폴트 메서드를 만드는데 시간이 걸리거나 Optional이 비어있을때만 디폴트 값을 생성하고 싶다면 orElseGet을 사용하면 된다.
  • orElseThrow는 Optional이 비어있을때 예외를 발생시킨다는 점에서 get 메서드와 비슷하다. 하지만 이 메서드는 발생시킬 예외의 종류를 선택할 수 있다.
  • ifPresent를 이용하면 값이 존재할때 인수로 넘겨준 동작을 실행할 수 있다. 값이 없으면 아무 일도 일어나지 않는다.