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.of로 null이 아닌 값을 포함하는 Optional을 만들 수 있다.
◦
Optional<Car> optCar = Optional.of(car);
•
null값으로 Optional 만들기
◦
정적 팩토리 메서드 Optional.ofNullable로 null값을 지정할 수 있는 Optional을 만들 수 있다.
◦
Optional<Car> optCar = Optional.ofNullable(car);
◦
car가 null이면 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을 포함하는 스트림을 쉽게 처리할 수 있도록 Optional에 stream() 메서드를 추가했다.
•
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이 비어있는 상황에서 기본값을 반환하도록 orElse로 Optional을 읽었다.
•
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
복사
•
person과 car의 시그니처만으로 둘 다 아무 값도 반환하지 않을 수 있다는 정보를 명시적을 주지만, 구현 코드는 null 확인 코드와 크게 다르지 않다.
•
다음 코드처럼 한 줄의 코드로 메서드를 재구현할 수 있다.
public Optional<Insutrance> nullSafeFindCheapestInsurance(
Optional<Person> person, Optional<Car> car) {
return person.flatMap( p -> car.map( c -> findCheapestInsurance(p, c)));
}
Java
복사
◦
첫 번째 Optional에 flatMap을 호출했으므로 첫 번째 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
복사
•
위 코드와 같이 정수로 변환할 수 없는 문자열은 빈 Optional로 return하도록 구현할 수 있다.
3.
기본형 Optional을 사용하지 말아야 하는 이유
•
Optional도 기본형으로 특화된 OptionalInt, OptionalLong, OptionalDouble 등의 클래스를 제공한다.
•
하지만 Optional의 최대 요소 수는 한개이므로 기본형 특화 Optional로 성능을 향상시킬 수 없다.
•
또한 기본형 특화 Optional은 map, 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값이 사용되거나 반환되는지 예측할 수 있다.