////
Search
Duplicate
🌑

null 대신 Optional 클래스

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

우리가 null인 참조 변수를 호출하려 한다면 어떻게 될까?
불우하게도 대부분의 경우, NullPointerException이 발생할 것이다.
1.
보수적인 자세로 NullPointerException 줄이기
예기치 않은 NullPointerException을 피하기 위해 대부분의 프로그래머는 필요한 곳에 null 확인 코드를 추가해서 예외 문제를 해결하려 할 것이다.
호출하는 과정에서 변수가 null인지 확인하는 작업이 추가되면서 코드 들여쓰기 수준이 증가한다.
⇒ 이와 같은 반복 패턴 코드를 깊은 의심이라고 부른다.
이를 반복하다보면 코드의 구조가 엉망이 되고 가독성도 떨어진다.
따라서 값이 있거나 없음을 표현하는 좋은 방법이 필요하다.
2.
null 때문에 발생하는 문제
에러의 근원이다.
NullPointerException은 자바에서 에러를 야기하는 흔한 원인이다.
코드를 어지럽힌다.
중첩된 null 확인 코드 때문에 가독성이 떨어진다.
아무 의미가 없다.
말 그대로 null은 아무 의미가 없다.
자바 철학에 위배된다.
자바는 개발자로부터 메모리 관리의 부담을 떨쳐주기 위해 모든 포인터를 숨겼다. 하지만 예외가 하나 있다면 그것이 null 포인터다.
타입 시스템에 구멍을 만든다.
무타입이며 정보를 포함하지 않아 모든 참조 타입에 할당할 수 잇다. 이런 식으로 사용된다면 우리는 나중에 이 의미를 유추하기 힘들어질 것이다.
3.
다른 언어는 null 대신 무엇을 사용하나?
그루비 같은 언어에서는 안전 내비게이션 연산자(?.)을 도입해서 null 문제를 해결했다. 다음은 예시 코드다.
def carInsuranceName = person?.car?.insurance?.name
Groovy
복사
호출 체인에 null인 값이 존재한다면 결과로 null이 반환된다.
하스켈, 스칼라 등의 함수형 언어는 아예 다른 관점에서 null 문제를 접근한다.
하스켈은 선택형값을 저장할 수 있는 Maybe라는 타입을 제공한다.
이는 주어진 타입의 값을 갖거나 아니면 아무 값도 갖지 않을 수 있다.
스칼라도 T 타입의 값을 갖거나 아무 값도 갖지 않을 수 있는 Option[T]라는 구조를 제공한다.
Option 타입에서 제공하는 연산을 사용해서 값이 있는지 여부를 명시적으로 확인해야 한다.
자바는 선택형값에서 영감을 받아 Optional<T>라는 새로운 클래스를 제공한다.

2. Optional 클래스 소개

자바 8은 하스켈과 스칼라의 영향을 받아서 Optional<T>라는 새로운 클래스를 제공한다.
Optional은 선택형값을 캡슐화하는 클래스로 어떠한 문자혈 타입의 값이 존재하지 않을 수 있다면 Optional<String>과 같이 사용한다.
값이 있으면 Optional 클래스는 값을 감싼다. 값이 없다면 Optional.empty 메서드로 Optional을 반환한다.
Optional로 인해 의미가 더 명확해질 수 있다. 클래스를 처음 봤을 때, Optional로 감싸진 객체들은 존재하지 않을 수도 있는 값이라는 것이다.
Optional의 역할은 더 이해하기 쉬운 API를 설계하도록 돕는 것이다.

3. Optional 적용 패턴

1.
Optional 객체 만들기
Optional
정적 팩토리 메서드 Optional.empty로 빈 Optioanl 객체를 만들 수 있다.
Optional<Car> optCar = Optional.empty()
null이 아닌 값으로 Optional 만들기
정적 팩토리 메서드 Optional.ofnull이 아닌 값을 포함하는 Optional을 만들 수 있다.
Optional<Car> optCar = Optional.of(car);
null값으로 Optional 만들기
정적 팩토리 메서드 Optional.ofNullablenull값을 지정할 수 있는 Optional을 만들 수 있다.
Optional<Car> optCar = Optional.ofNullable(car);
carnull이면 null을 반환한다.
2.
map으로 Optional의 값을 추출하고 변환하기
보통 객체의 정보를 호출할 때는 Optional을 사용할 때가 많다.
null인지 아닌지 명시적인 검사를 수행하는 것으로 이런 유형의 패턴에 사용할 수 있도록 map 메서드를 지원한다.
Optional<Insurance> optInsurance = Optional.ofNullable(insurance); Optional<String> name = optInsurance.map(Insurance::getName);
Java
복사
3.
flatMap으로 Optional 객체 연결
Optional이 중첩 객체 구조로 되어있는 경우, map은 정상적으로 동작하지 않는다.
flatMap을 사용하면 이차원 Optional이 하나의 값을 포함하는 하나의 Optional로 바뀐다.
public String getCarInsuranceName(Optional<Person> person) { return person.flatMap(Person::getCar) .flatMap(Car::getInsurance) .map(Insurance::getName) .orElse("Unknown"); }
Java
복사
4.
Optional 스트림 조작
자바 9에서는 Optional을 포함하는 스트림을 쉽게 처리할 수 있도록 Optionalstream() 메서드를 추가했다.
Optional 스트림을 값을 가진 스트림으로 변환하고자 할 때, 이 기능을 유용하게 사용할 수 있다.
persons.stream() .map(Person::getCar) // Stream<Optional<Car>> .map(optCar -> optCar.flatMap(Car::getInsurance)) // Stream<Optional<Insurance>> .map(optIns -> optIns.map(Insurance::getName)) // Stream<Optional<String>> .flatMap(Optional::stream) // Stream<String> .collect(toSet()); // Set<String>>
Java
복사
5.
디폴트 액션과 Optional 언랩
Optional이 비어있는 상황에서 기본값을 반환하도록 orElseOptional을 읽었다.
Optional 클래스는 이외에도 Optional 인스턴스에 포함된 값을 읽는 다양한 방법을 제공한다.
get()
get()은 값을 읽는 가장 간단하면서 안전하지 않은 메서드다.
래핑된 값이 있으면 해당 값을 반환하고 없으면 NoSuchElementException을 발생시킨다.
orElse(T other)
Optional이 래핑된 값이 없을 때, 기본값을 제공해줄 수 있다.
orElseGet(Supplier<? extends T> other)
Optional에 값이 없을 때만 Supplier가 실행된다. 즉 orElse의 게으른 버전이다.
기본값이 반드시 필요한 상황이라면 사용해야한다.
orElseThrow(Supplier<? extends X> exceptionSupplier
Optional에 값이 없을 때, 예외를 발생시킨다는 점에서 get 메서드와 비슷하지만 이 메서드는 발생시킬 예외를 선택할 수 있다.
ifPresent(Consumer<? super T> consuer)
값이 존재할 때, 인수로 넘겨준 동작을 실행할 수 있다. 값이 존재하지 않으면 아무일도 일어나지 않는다.
ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)
Optional이 비었을 때, 실행할 수 있는 Runnable을 인수로 받는다는 점만 다르다.
6.
Optional 합치기
다음과 같은 메소드를 구현해야한다고 생각해보자.
public Insurance findCheapestInsurance(Person person, Car car) { ... return cheapestCompany; }
Java
복사
Optional을 인수로 받아서 Optional<Insurance>를 반환하는 null 안전 버전의 메서드며 인수로 전달한 값 중 하나라도 비어있으면 빈 Optional<Insurance>를 반환한다.
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(); } }
Java
복사
personcar의 시그니처만으로 둘 다 아무 값도 반환하지 않을 수 있다는 정보를 명시적을 주지만, 구현 코드는 null 확인 코드와 크게 다르지 않다.
다음 코드처럼 한 줄의 코드로 메서드를 재구현할 수 있다.
public Optional<Insutrance> nullSafeFindCheapestInsurance( Optional<Person> person, Optional<Car> car) { return person.flatMap( p -> car.map( c -> findCheapestInsurance(p, c))); }
Java
복사
첫 번째 OptionalflatMap을 호출했으므로 첫 번째 Optional이 비어있다면 인수로 전달된 표현식이 실행되지 않고 빈 Optional을 반환한다.
두 번째 Optional도 마찬가지이고, 두 값이 모두 존재한다면 map 메서드로 전달한 람다 표현식이 안전하게 findCheapestInsurance 메서드를 호출한다.
7.
필터로 특정값 거르기
객체의 메서드를 호출해서 프로퍼티를 확인해야 하는 경우 다음과 구현할 수 있다.
Insurance insurance = ...; if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){ System.out.println("ok"); }
Java
복사

4. Optional을 사용한 실용 예제

1.
잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기
Object value=map.get("key");
위 코드와 같이 null이 반환될 수 있는 코드를 Optional로 감싸서 개선할 수 있다.
Optional<Object> value= Optional.ofNullable(map.get("key"));
2.
예외와 Optional 클래스
null을 확인할 때는 if문을 사용했지만 예외를 발생하시키는 메서드는 try/catch 블록을 사용해야 한다.
public static Optional<Integer> stringToInt(String s) { try { return Optional.of(Integer.parseInt(s)); } catch (NumberFormatException e) { return Optional.empty(); } }
Java
복사
위 코드와 같이 정수로 변환할 수 없는 문자열은 빈 Optionalreturn하도록 구현할 수 있다.
3.
기본형 Optional을 사용하지 말아야 하는 이유
Optional도 기본형으로 특화된 OptionalInt, OptionalLong, OptionalDouble 등의 클래스를 제공한다.
하지만 Optional의 최대 요소 수는 한개이므로 기본형 특화 Optional로 성능을 향상시킬 수 없다.
또한 기본형 특화 Optionalmap, flatMap, filter등을 제공하지 않고, 생성한 결과를 다른 일반 Optional과 혼용할 수 없다.

5. 마치며

역사적으로 프로그래밍 언어에서는 null 참조로 값이 없는 상황을 표현해왔다.
자바 8에서는 값이 있거나 없음을 표현할 수 있는 클래스 Optional<T>를 제공한다.
팩토리 메서드 Optioanl.empty, Optioanl.of, Optional.ofNullable 등을 이용해서 Optional 객체를 만들 수 있다.
Optional 클래스는 스트림과 비슷한 연산을 map, flatMap, filter 등의 메서드를 제공한다.
Optional로 값이 없는 상황을 적절하게 처리하도록 강제할 수 있다. 즉 Optional로 예상치 못한 null 예외를 방지할 수 있다.
Optional을 활용하면 더 좋은 API를 설계할 수 있다. 즉 사용자는 메서드의 시그니처만 보고도 Optional값이 사용되거나 반환되는지 예측할 수 있다.