1. 서론
이번 포스팅에서는 Chapter11의 null 대신 Optional 클래스에 대해 진행하도록 하겠습니다.
2. 값이 없는 상황을 어떻게 처리할까?
책에서는 null 관련하여 아래 예제를 말합니다.
public class Person {
private Car car;
public Car getCar() {
return car;
}
}
public class Car {
private Insurance insurance;
public Insurance getInsurance() {
return insurance;
}
}
public class Insurance {
private String name;
public String getName() {
return name;
}
}
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
위의 코드는 Person이 차를 가지고 있지 않는 경우에는 NullPointerException 이 발생하게 됩니다.
1) 보수적인 자세로 NullPointerException 줄이기
위 예제에서 null를 예방하기 위해 if-else 를 사용하게 된다면 아래와 같은 코드가 될 것입니다.
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";
}
한눈에 봐도 코드가 난잡하며, 객체의 연관도가 깊을수록 if의 깊이는 증가됩니다.
가끔 깊이가 너무 깊어져 이를 예방하기 위해 아래와 같은 코드가 나올수도 있습니다.
public String getCarInsuranceName(Person person) {
if (person == null) {
return "Unknown";
}
Car car = person.getCar();
if (car == null) {
return "Unknown";
}
Insurance insurance = car.getInsurance();
if (insurance == null) {
return "Unknown";
}
return insurance.getName();
}
이 코드의 경우에는 깊이는 깊어지지 않지만 너무 많은 return 출구가 있어 유지보수가 어렵다는 단점을 가지게 됩니다.
2) null 때문에 발생하는 문제
위와 같이 null로 인해서 발생하는 문제로는 아래와 같습니다.
- 에러의 근원 : NullPointerException 은 자바에서 가장 흔히 발생하는 에러입니다.
- 코드를 어지럽힘 : null 체크를 통해 코드가 난잡하고 어지럽게 됩니다.
- 아무 의미가 없음 : null은 아무 의미도 표현하지 않으며, 이는 값이 없음을 표현하기에 부적합 합니다.
- 자바 철학에 위배된다 : 자바의 경우, 개발자에게 모든 포인터를 숨겼지만 null은 유일하게 포인터를 숨길 수 없었습니다.
- 형식 시스템에 구멍을 만든다 : null은 무형식이며 정보를 포함하고 있지 않으므로 모든 참조 형식에 null을 할당 할 수 있고, 이는 결국 위험한 코드를 만들게 됩니다.
3. Optional 클래스 소개
자바 8 에서는 null 문제를 해결하기 위해 Optional<T> 라는 클래스를 제공합니다.
이는 사실상 단순히 null를 위한 Wrapper 클래스입니다.
아래는 Optional 를 간단히 보여주는 그림입니다.
모든 null 참조를 Optional로 대치하는것은 바람직하지 않습니다.
Optional은 객체에 대해서 null 체크, null 인 경우 대처를 어떻게 할지를 도와주는 역할입니다.
때문에, Optional를 사용한다고 NullPointerException이 나지 않는것은 아닙니다.
4. Optional 적용 패턴
1) Optional 객체 만들기
Optional 도 클래스이기 때문에 객체를 생성해서 사용해야 합니다.
1. 빈 Optional
아래와 같이 빈 Optional 객체를 생성할 수 있습니다.
Optional<Car> optCar = Optional.empty();
2. null이 아닌 값으로 Optional 만들기
아래와 같은 기존에 있는 객체로 Optional 객체를 만들 수도 있습니다.
Optional<Car> optCar = Optional.of(car);
단, 이 경우 인자인 car가 null인 경우 NullPointerException이 발생합니다.
3. null 값으로 Optional 만들기
아래와 같은 방법으로도 Optional 객체를 만들 수 있습니다.
Optional<Car> optCar = Optional.ofNullable(car);
2번의 of 메서드와의 차이점으로는 car가 null인 경우 NullPointerException 가 아닌 빈 Optional을 반환한다는 점입니다.
2) 맵으로 Optional의 값을 추출하고 변환하기
Optional 클래스는 스트림 메서드와 비슷한 map 메서드를 지원합니다.
아래와 같이 map 을 사용할 수 있습니다.
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
만약, Optional이 비어있으면 아무일도 일어나지 않습니다.
3) flatMap으로 Optional 객체 연결
스트림 메서드의 flatMap 비슷한 기능의 메서드도 제공하고 있습니다.
Optional의 flatMap도 스트림 메서드와 동일하게 Optional<Optional<T>> 와 같은 depth 가 생기는 것을 Optional<T> 로 평준화 해주는 메서드입니다.
4) Optional로 자동차의 보험회사 이름 찾기
그럼 이제 배운 Optional를 사용하여 예제를 해결하게 되면 아래와 같은 코드가 나오게 됩니다.
public class Person {
private Car car;
public Optional<Car> getCarAsOptional() {
return Optional.ofNullable(car);
}
}
public class Car {
private Insurance insurance;
public Optional<Insurance> getInsuranceAsOptional() {
return Optional.ofNullable(insurance);
}
}
public class Insurance {
private String name;
public String getName() {
return name;
}
}
public String getCarInsuranceName(Person person) {
return Optional.of(person)
.flatMap(Person::getCarAsOptional)
.flatMap(Car::getInsuranceAsOptional)
.map(Insurance::getName)
.orElse("other value");
}
Optional 클래스는 필드 형식으로 사용할 것을 가정하지 않아, Serializable 인터페이스를 구현하지 않습니다.
때문에, 위와 같이 get 메서드에만 Optional 를 사용하는것을 권장합니다.
5) Optional 스트림 조작
자바 9 에서는 Optional 의 스트림 처리를 제공하기 위해 Optional에 stream() 메서드를 제공합니다.
아래는 Optional의 stream 함수를 사용한 예제입니다.
public Set<String> getCarInsuranceNames(List<Person> persons) {
return persons.stream()
.map(Person::getCarAsOptional)
.map(optCar -> optCar.flatMap(Car::getInsuranceAsOptional))
.map(optIns -> optIns.map(Insurance::getName))
.flatMap(Optional::stream)
.collect(Collectors.toSet());
}
stream 메서드를 지원함으로 위와 같이 더욱 간단하게 null 처리를 할 수 있게 되었습니다.
하지만, 마지막 결과를 얻기 위해서는 빈 Optional 은 제거하고 있는것은 언랩해야 하는 문제가 있습니다.
이는 위 예제에서 Optional::stream 메서드로 해결할 수 있습니다.
stream 메서드는 값이 있는 것 만을 Stream에 담아서 전달하기 때문입니다.
6) 디폴트 액션과 Optional 언랩
아래는 Optional 클래스가 가지고 있는 디폴트 액션입니다.
디폴트 액션 | 설명 |
get | Optional 의 값을 가져오는 메서드입니다. 만약, 값이 없다면 NoSuchElementException이 발생하기 때문에 위험한 메서드입니다. |
orElse(T other) | orElse는 값이 없는경우 인자인 other를 반환합니다. |
orElseGet(Supplier<? extends T> other) | orElseGet는 orElse의 게으른 버전입니다. 값이 없는경우에서야 Supplier 를 수행하여 값을 반환하기 때문입니다. |
orElseThrow(Supplier<? extends T> exceptionSupplier) | orElseThrow는 값이 없는 경우 예외를 발생합니다. |
ifPresend(Consumer<? super T> consumer) | ifPresend는 값이 존재할때만, 인자의 Consumer를 수행합니다. |
ifPresendOrElse(Consumer<? super T> action, Runnable emptyAction) | ifPresendOrElse는 자바 9에서 추가된 메서드로, 위의 ifPresent와의 차이점으로는 값이 비어있는 경우 Runnable 인자를 실행한다는 점입니다. |
7) 필터로 특정값 거르기
Optional은 filter 메서드를 지원하고 있습니다.
이 메서드는 프레디케이트를 인자로 받으며, Optional 객체가 값을 가지고 있는 경우 프레디 케이트를 적용하고, 값이 없는경우에는 빈 Optional를 반환합니다.
프레디 케이트 적용 결과가 false의 경우에도 빈 Optional 을 반환합니다.
아래는 filter 사용 예제 입니다.
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
optInsurance.filter(insurance -> "CabridgeInsurance".equals(insurance.getName()))
.ifPresent(x -> System.out.println("ok"));
6. 마무리
이번 포스팅에서는 Chapter11 null 대신 Optional 클래스에 대해 진행하였습니다.
다음에는 Chapter12 새로운 날짜와 시간 API에 대해 포스팅하겠습니다.
'Programming > ModernJavaInAction' 카테고리의 다른 글
(13) 디폴트 메서드 (0) | 2020.05.23 |
---|---|
(12) 새로운 날짜와 시간 API (0) | 2020.05.02 |
(10) 람다를 이용한 도메인 전용 언어 (0) | 2020.04.19 |
(9) 리팩터링, 테스팅, 디버깅 (0) | 2020.04.13 |
(8) 컬렉션 API 개선 (0) | 2020.04.13 |