-
📌 이 장의 내용
-
11.1 값이 없는 상황을 어떻게 처리할까?
-
11.1.1 보수적인 자세로 NullPointerException 줄이기
-
11.1.2 null 때문에 발생하는 문제
-
11.2 Optional 클래스 소개
-
11.3 Optional 적용 패턴
-
11.3.1 Optional 객체 만들기
-
11.3.2 맵으로 Optional의 값을 추출하고 변환하기
-
11.3.3 flatMap으로 Optional 객체 연결
-
11.3.4 Optional 스트림 조작
-
11.3.5 디폴트 액션과 Optional 언랩
-
11.3.6 두 Optional 합치기
-
11.3.7 필터로 특정값 거르기
-
11.4 Optional을 사용한 실용 예제
-
11.4.1 잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기
-
11.4.2 예외와 Optional 클래스
-
11.4.3 기본형 Optional을 사용하지 말아야 하는 이유
✏️[모던 자바 인 액션, 전문가를 위한 자바 8, 9, 10 기법 가이드] 스터디 관련 책 내용을 정리한 글입니다.

📌 이 장의 내용
- null 참조의 문제점과 null을 멀리해야 하는 이유
- null 대신 Optional : null로부터 안전한 도메인 모델 재구현하기
- Optional 활용 : null 확인 코드 제거하기
- Optional에 저장된 값을 확인하는 방법
- 값이 없을 수도 있는 상황을 고려하는 프로그래밍
11.1 값이 없는 상황을 어떻게 처리할까?
11.1.1 보수적인 자세로 NullPointerException 줄이기
대부분의 프로그래머들은 null 예외 문제를 해결하기 위해 필요한 곳에 다양한 null 확인 코드를 추가합니다. 더욱 보수적인 프로그래머들은 반드시 필요하지 않은 경우에도 null 확인 코드를 추가하여 안정성을 높이려고 할 것입니다.
이를 통해 NullPointerException이 발생하는 경우를 줄일 수 있습니다.
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";
}
위의 메서드에서는 모든 변수가 null인지 의심하기 때문에 변수를 접근할 때마다 중첩된 if가 추가되면서 코드 들여 쓰기 수준이 증가합니다. 이와 같은 반복 패턴 코드를 깊은 의심이라고 부릅니다. 즉, 변수가 null인지 의심되어 중첩 if 블록을 추가하면 코드 들여 쓰기 수준이 증가합니다. 이러한 패턴을 반복하다 보면 코드의 구조가 혼란스러워지고 가독성이 떨어집니다. 이러한 문제를 해결하기 위해 다른 방법을 적용할 필요가 있습니다.
다음 예제는 다른 방법으로 이 문제를 해결하는 코드입니다.
public String getCarInsuranceName(Person persone) {
if (person == null) { // null 확인 코드마다 출구가 생깁니다.
return "Unknown";
}
Car car = person.getCar();
if (car == null) { // null 확인 코드마다 출구가 생깁니다.
return "Unknown";
}
Insurance insurance = car.getInsurance();
if (insurance == null) { // null 확인 코드마다 출구가 생깁니다.
return "Unknown";
}
return insurance.getName();
}
위 코드는 중첩된 if문을 없애기 위해 모든 변수를 null 여부에 따라 확인하는 방식을 사용합니다. 하지만 이 방식을 코드가 복잡해 가독성이 떨어지고, 메서드의 출구가 네 개가 되어 유지보수가 어려워집니다. 또한, "Unknown"이라는 문자열이 세 곳에서 중복되어 사용되고 있어서 코드를 수정할 때 오타 등의 문제가 발생할 수 있습니다. 이러한 문제를 해결하기 위해 "Unknown"이라는 문자열을 상수로 정의하거나 null 대신 다른 값으로 대체하는 것이 좋습니다.
메서드에서 반환되는 값이 동일한 타입인 경우, 그 값의 추론이 가능한 경우도 있습니다. 이러한 경우를 활용하면 코드의 가독성과 유지보수성을 향상할 수 있습니다.
11.1.2 null 때문에 발생하는 문제
- 에러의 근원이다 : NullPointerException은 자바에서 가장 흔히 발생하는 에러다.
- 코드를 어지럽힌다 : 때로는 중첩된 null 확인 코드를 추가해야 하므로 null 때문에 코드 가독성이 떨어진다.
- 아무 의미가 없다 : null은 아무 의미도 표현하지 않는다. 특히 정적 형식 언어에서 값이 없음을 표현하는 방법으로는 적절하지 않다.
- 자바 철학에 위배된다 : 자바는 개발자로부터 모든 포인터를 숨겼다. 하지만 예외가 있는데 그것이 바로 null 포인터다.
- 형식 시스템에 구멍을 만든다 : null은 무형식이며 정보를 포함하고 있지 않으므로 모든 참조 형식에 null을 할당할 수 있다. 이런 식으로 null이 할당되기 시작하면서 시스템의 다른 부분으로 null이 퍼졌을 때 애초에 null이 어떤 의미로 사용되었는지 알 수 없다.
11.2 Optional 클래스 소개
자바 8은 java.util.Optional <T>라는 새로운 클래스를 제공합니다. Optional은 선택형 값을 캡슐화하는 클래스입니다.
값이 있으면 Optional 클래스는 값을 감싸고, 값이 없으면 Optional.empty 메서드로 Optional을 반환합니다. Optional.empty는 Optional의 특별한 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드입니다. null을 참조하려 하면 NullPointerException이 발생하지만 Optional.empty( )는 Optional 객체이므로 이를 다양한 방식으로 활용할 수 있습니다.
11.3 Optional 적용 패턴
Optional 형식은 도메인 모델의 의미를 명확하게 하고, 값이 없는 상황을 표현하는 데 사용됩니다. Optional 객체를 활용하려면, 메서드를 사용하여 값을 추출하거나 변환하고, 기본값을 설정하거나, 값의 존재 여부를 확인할 수 있습니다.
11.3.1 Optional 객체 만들기
Optional은 값이 있을 수도 있고 없을 수도 있는 객체를 다룰 때 사용되며, Optional 객체를 생성하는 방법으로는 Optional.of(), Optional.ofNullable(), Optional.empty() 등이 있습니다.
빈 Optional
정적 팩토리 메서드 Optional.empty로 빈 Optional 객체를 얻을 수 있습니다.
Optional<Car> optCar = Optional.empty();
null이 아닌 값으로 Optional 만들기
또는 정적 팩토리 메서드 Optional.of로 null이 아닌 값을 포함하는 Optional을 만들 수 있습니다.
car가 null이라면 nullPointerException이 발생합니다.
Optional<Car> optCar = Optional.of(car);
null값으로 Optional 만들기
마지막으로 정적 팩토리 메서드 Optional.ofNullable로 null값을 저장할 수 있는 Optional을 만들 수 있습니다.
car가 null이면 빈 Optional 객체가 반환됩니다.
Optional<Car> optCar = Optional.ofNullable(car);
Optional은 get() 메서드로 값을 가져올 수 있지만, 비어있는 Optional에서 get()을 호출하면 예외가 발생합니다. 이를 방지하기 위해 Optional은 명시적인 null 검사를 제거할 수 있는 기능을 제공합니다. 이 기능은 스트림 연산에서 영감을 받았습니다.
11.3.2 맵으로 Optional의 값을 추출하고 변환하기
보통 객체의 정보를 추출할 때는 Optional을 사용할 때가 많습니다. 다음 코드처럼 접근하기 전에 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 메서드와 유사합니다. Optional이 값을 포함하면 제공된 함수를 적용하여 값을 변환하고, Optional이 비어있으면 아무 작업도 수행하지 않습니다. Optional은 최대 요소 개수가 1개인 데이터 컬렉션으로 생각할 수 있습니다.
11.3.3 flatMap으로 Optional 객체 연결
다음처럼 map을 이용해서 코드를 재구현할 수 있습니다.
Optional<Person> optPerson = Optional.of(person);
Optional<String> name = optPerson.map(Person::getCar)
.map(Car::getInsurance)
.map(Insurance::getName);
위 코드는 컴파일되지 않는데, optPeople.map()의 결과는 Optional <Optional <Car>> 형식이기 때문입니다. 이 문제를 해결하기 위해 flatMap 메서드를 사용할 수 있습니다. flatMap은 함수를 적용하여 생성된 모든 스트림을 하나로 병합하여 일차원 Optional로 평준화합니다.
11.3.4 Optional 스트림 조작
Java 9에서는 Optional에 stream() 메서드를 추가하여 Optional을 포함하는 스트림을 쉽게 처리할 수 있게 되었습니다. 이 기능은 Optional 스트림을 값을 가진 스트림으로 변환할 때 유용합니다. 11.3.4절에서는 다른 예제를 이용해 Optional 스트림을 어떻게 다루는지 설명합니다.
이 예제는 Person/Car/Insurance 코드에서 List<Person>을 인수로 받아 자동차를 소유한 사람들이 가입한 보험 회사의 이름을 Set <String>으로 반환하는 메서드를 구현한 것입니다.
public Set<String> getCarInsuranceNames(List<Person> persons) {
return persons.stream()
.map(Person::getCar)
.map(optCar -> optCar.flatMap(Car::getInsurance))
.map(optCar -> optIns.map(Insurance::getName))
.flatMap(Optional::new)
.collect(toSet());
위 예제는 getCar() 메서드가 Optional<Car>를 반환하기 때문에 사람이 자동차를 가지지 않을 수 있는 상황이 있어 첫 번째 map 변환을 수행하면 stream <Optional <Car>>를 얻습니다. 이후 두 개의 map 연산을 사용하여 Optional <Car>를 Optional <Insurance>로 변환하고, 스트림 대신 각 요소를 Optional <String>으로 변환합니다. 최종 결과로 Stream <Optional <String>>을 얻지만, 사람이 차를 가지지 않거나 차가 보험에 가입되어 있지 않아 결과가 비어있을 수 있습니다. Optional을 사용하면 널에 대한 걱정 없이 연산을 안전하게 처리할 수 있지만, 최종 결과를 얻으려면 빈 Optional을 제거하고 값을 언랩해야 하는 문제가 있습니다.
다음 코드처럼 filter, map을 순서적으로 이용해 결과를 얻을 수 있습니다.
Stream<Optional<String>> stream = ...
Set<String> result = stream.filter(Optional::isPresent)
.map(Optional::get)
.collect(toSet()):
Optional 클래스의 stream() 메서드를 이용하면 스트림의 요소를 한 번의 연산으로 Optional 객체를 포함하는 스트림으로 변환할 수 있습니다. 이 메서드를 flatMap 메서드와 함께 사용하면 두 수준의 스트림을 평면 스트림으로 변환할 수 있습니다. 이 기법을 사용하면 Optional을 언랩하고 비어있는 Optional을 건너뛸 수 있습니다.
11.3.5 디폴트 액션과 Optional 언랩
Optional 클래스는 orElse를 비롯한 다양한 메서드를 제공하여 빈 Optional일 경우 기본값이나 예외를 반환할 수 있습니다.
- get() 메서드는 Optional에 값이 있으면 해당 값을 반환하고 값이 없으면 NoSuchElementException 예외를 발생시키는데, 이는 값이 반드시 있다고 가정할 수 있는 상황이 아니면 사용하지 않는 것이 좋습니다. 이 때문에 get() 메서드를 사용하는 것은 중첩된 null 확인 코드를 사용하는 것과 크게 다르지 않습니다.
- orElse 메서드를 이용하면 Optional이 값을 포함하지 않을 때 기본값을 제공할 수 있습니다.
- orElseGet(Supplier<? extends T> other)는 Optional에 값이 없을 경우만 Supplier를 실행하는 버전의 메서드입니다. 이를 사용하면 디폴트 메서드를 만드는 시간이나 Optional이 비어있을 때만 기본값을 생성하고 싶은 경우에 유용합니다.
- ifPresent(Consumer <? super T> consumer)를 사용하면 Optional에 값이 존재할 때 인수로 넘겨준 동작을 실행할 수 있습니다. 값이 없으면 아무 일도 일어나지 않습니다.
자바 9에서는 다음의 인스턴스 메서드가 추가되었습니다.
- ifPresentOrElse(Consumer <? super T> action, Runnable emptyAction)은 Optional이 비어있을 때 실행할 수 있는 Runnable을 인수로 받는다는 점만 ifPresent와 다른 메서드입니다.
11.3.6 두 Optional 합치기
다은은 Person과 Car 정보를 이용해서 가장 저렴한 보험료를 제공하는 보험회사를 찾는 몇몇 복잡한 비즈니스 로직을 구현한 외부 서비스가 있다고 가정하고 구현한 코드입니다.
public Insurance findCheapestInsurance(Person person, Car car) {
// 다양한 보험회사가 제공하는 서비스 조회
// 모든 결과 데이터 비교
return cheapestCompany;
}
두 Optional 인스턴스를 인수로 받아 Optional<Insurance>를 반환하는 null 안전 메서드를 구현해야 한다고 가정합니다. 인수로 전달한 값 중 하나라도 비어있으면 빈 Optional <Insurance>를 반환하며, 이때 Optional 클래스의 isPresent() 메서드를 사용하여 값이 존재하는지 여부를 확인할 수 있습니다.
따라서 isPresent를 이용해서 다음처럼 코드를 구현할 수 있습니다.
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();
}
}
이 메서드의 장점은 person과 car의 시그니처만으로도 둘 다 아무 값도 반환하지 않을 수 있다는 정보를 명시적으로 보여준다는 것입니다. 그러나 구현 코드는 null 확인 코드와 크게 다른 점이 없다는 단점이 있습니다.
Optional 클래스와 Stream 인터페이스는 map과 flatMap 메서드 이외에도 다양한 비슷한 기능을 공유합니다.
11.3.7 필터로 특정값 거르기
객체의 메서드를 호출해서 프로퍼티를 확인할 때, 해당 객체가 null인지 확인해야 안전하게 작업을 수행할 수 있습니다. 예를 들어 보험 회사 이름이 'CambridgeInsurance'인지 확인해야 할 때, Insurance 객체가 null인지 확인한 다음 getName 메서드를 호출하는 것이 안전한 방법입니다.
Insurance insurance = ...;
if (insurance != null && "CambridgeInsurance".equals(insurance.getName())) {
System.out.println("ok");
}
// Optional 객체에 filter 메서드를 이용해서 다음과 같이 코드를 재구현 할 수 있습니다.
Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName()))
.ifPresent(x -> System.out.println("ok"));
filter 메서드는 Optional 객체에 프레디케이트를 적용하여 값이 일치하면 해당 값을 반환하고, 일치하지 않으면 빈 Optional을 반환하는 메서드이다. Optional이 비어있다면 filter는 아무 동작도 하지 않으며, 값이 있다면 프레디케이트를 적용하여 일치하지 않으면 빈 Optional로 만들고, 일치하면 아무 변화도 일어나지 않는다.
11.4 Optional을 사용한 실용 예제
새 Optional 클래스를 효과적으로 이용하려면 잠재적으로 존재하지 않는 값의 처리 방법을 바꿔야 합니다. 즉, 코드 구현만 바꾸는 것이 아니라 네이티브 자바 API와 상호작용하는 방식도 바꿔야 합니다. Optional 기능을 활용할 수 있도록 코드에 작은 유틸리티 메서드를 추가하는 방식으로 문제를 해결할 수 있습니다.
11.4.1 잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기
기존의 자바 API에서는 요청한 값이 없거나 계산에 실패했을 때 null을 반환하는 경우가 많습니다. 하지만 null을 반환하는 것보다는 Optional을 반환하는 것이 더 안전하고 바람직합니다. 예를 들어, Map의 get 메서드는 반환값을 Optional로 감쌀 수 있습니다. 이렇게 하면 코드에서 null 체크를 하지 않고도 NPE를 방지할 수 있습니다.
Map <String, Object> 형식의 맵이 있는데, 문자열 'key'에 해당하는 값이 없으면 null이 반환됩니다. map에서 반환하는 값을 Optional.ofNullable을 이용해서 개선할 수 있습니다.
Optional<Object> value = Optional.ofNullable(map.get("key"));
이와 같은 코드를 이용해서 null일 수 있는 값을 Optional로 안전하게 변환할 수 있습니다.
11.4.2 예외와 Optional 클래스
자바 API에서는 값을 제공할 수 없을 때 null 대신 예외를 발생시키는 경우가 있습니다.. 이를 해결하기 위해 예외를 발생시키는 메서드에서는 try/catch 블록을 사용해야 합니다. 하지만 이러한 경우에도 Optional을 사용하여 null 체크를 하지 않고 NPE를 방지할 수 있습니다. 이를 위해 parseInt를 감싸는 작은 유틸리티 메서드를 구현하여 Optional을 반환하도록 모델링할 수 있습니다. 예를 들어, stringToInt라는 메서드를 구현하여 문자열을 Optional <Integer>로 변환할 수 있습니다. 이를 포함하는 OptionalUtility 클래스를 만들면 거추장스러운 try/catch 로직을 사용하지 않고도 문자열을 Optional로 변환할 수 있습니다.
11.4.3 기본형 Optional을 사용하지 말아야 하는 이유
Optional 클래스도 기본형 특화된 OptionalInt, OptionalLong, OptionalDouble 등이 제공되며, 스트림과 마찬가지로 기본형 특화 Optional을 사용하면 성능을 향상할 수 있습니다. 하지만 Optional은 최대 요소 수가 한 개뿐이므로 기본형 특화 클래스로 성능을 개선할 수 없으며, map, flatMap, filter 등의 메서드를 지원하지 않아서 권장되지 않습니다. 또한 기본형 특화 Optional과 일반 Optional은 혼용할 수 없습니다.
'📚 서적 및 학습자료 > 모던 자바 인 액션[2회독]' 카테고리의 다른 글
[모던 자바 인 액션] 13장 디폴트 메서드 (0) | 2023.03.26 |
---|---|
[모던 자바 인 액션] 12장 새로운 날짜와 시간 API (0) | 2023.03.26 |
[모던 자바 인 액션] 10장 람다를 이용한 도메인 전용 언어 (0) | 2023.03.23 |
[모던 자바 인 액션] 9장 리팩터링, 테스팅, 디버깅 (0) | 2023.03.21 |
[모던 자바 인 액션] 8장 컬렉션 API 개선 (0) | 2023.03.18 |
✏️[모던 자바 인 액션, 전문가를 위한 자바 8, 9, 10 기법 가이드] 스터디 관련 책 내용을 정리한 글입니다.

📌 이 장의 내용
- null 참조의 문제점과 null을 멀리해야 하는 이유
- null 대신 Optional : null로부터 안전한 도메인 모델 재구현하기
- Optional 활용 : null 확인 코드 제거하기
- Optional에 저장된 값을 확인하는 방법
- 값이 없을 수도 있는 상황을 고려하는 프로그래밍
11.1 값이 없는 상황을 어떻게 처리할까?
11.1.1 보수적인 자세로 NullPointerException 줄이기
대부분의 프로그래머들은 null 예외 문제를 해결하기 위해 필요한 곳에 다양한 null 확인 코드를 추가합니다. 더욱 보수적인 프로그래머들은 반드시 필요하지 않은 경우에도 null 확인 코드를 추가하여 안정성을 높이려고 할 것입니다.
이를 통해 NullPointerException이 발생하는 경우를 줄일 수 있습니다.
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";
}
위의 메서드에서는 모든 변수가 null인지 의심하기 때문에 변수를 접근할 때마다 중첩된 if가 추가되면서 코드 들여 쓰기 수준이 증가합니다. 이와 같은 반복 패턴 코드를 깊은 의심이라고 부릅니다. 즉, 변수가 null인지 의심되어 중첩 if 블록을 추가하면 코드 들여 쓰기 수준이 증가합니다. 이러한 패턴을 반복하다 보면 코드의 구조가 혼란스러워지고 가독성이 떨어집니다. 이러한 문제를 해결하기 위해 다른 방법을 적용할 필요가 있습니다.
다음 예제는 다른 방법으로 이 문제를 해결하는 코드입니다.
public String getCarInsuranceName(Person persone) {
if (person == null) { // null 확인 코드마다 출구가 생깁니다.
return "Unknown";
}
Car car = person.getCar();
if (car == null) { // null 확인 코드마다 출구가 생깁니다.
return "Unknown";
}
Insurance insurance = car.getInsurance();
if (insurance == null) { // null 확인 코드마다 출구가 생깁니다.
return "Unknown";
}
return insurance.getName();
}
위 코드는 중첩된 if문을 없애기 위해 모든 변수를 null 여부에 따라 확인하는 방식을 사용합니다. 하지만 이 방식을 코드가 복잡해 가독성이 떨어지고, 메서드의 출구가 네 개가 되어 유지보수가 어려워집니다. 또한, "Unknown"이라는 문자열이 세 곳에서 중복되어 사용되고 있어서 코드를 수정할 때 오타 등의 문제가 발생할 수 있습니다. 이러한 문제를 해결하기 위해 "Unknown"이라는 문자열을 상수로 정의하거나 null 대신 다른 값으로 대체하는 것이 좋습니다.
메서드에서 반환되는 값이 동일한 타입인 경우, 그 값의 추론이 가능한 경우도 있습니다. 이러한 경우를 활용하면 코드의 가독성과 유지보수성을 향상할 수 있습니다.
11.1.2 null 때문에 발생하는 문제
- 에러의 근원이다 : NullPointerException은 자바에서 가장 흔히 발생하는 에러다.
- 코드를 어지럽힌다 : 때로는 중첩된 null 확인 코드를 추가해야 하므로 null 때문에 코드 가독성이 떨어진다.
- 아무 의미가 없다 : null은 아무 의미도 표현하지 않는다. 특히 정적 형식 언어에서 값이 없음을 표현하는 방법으로는 적절하지 않다.
- 자바 철학에 위배된다 : 자바는 개발자로부터 모든 포인터를 숨겼다. 하지만 예외가 있는데 그것이 바로 null 포인터다.
- 형식 시스템에 구멍을 만든다 : null은 무형식이며 정보를 포함하고 있지 않으므로 모든 참조 형식에 null을 할당할 수 있다. 이런 식으로 null이 할당되기 시작하면서 시스템의 다른 부분으로 null이 퍼졌을 때 애초에 null이 어떤 의미로 사용되었는지 알 수 없다.
11.2 Optional 클래스 소개
자바 8은 java.util.Optional <T>라는 새로운 클래스를 제공합니다. Optional은 선택형 값을 캡슐화하는 클래스입니다.
값이 있으면 Optional 클래스는 값을 감싸고, 값이 없으면 Optional.empty 메서드로 Optional을 반환합니다. Optional.empty는 Optional의 특별한 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드입니다. null을 참조하려 하면 NullPointerException이 발생하지만 Optional.empty( )는 Optional 객체이므로 이를 다양한 방식으로 활용할 수 있습니다.
11.3 Optional 적용 패턴
Optional 형식은 도메인 모델의 의미를 명확하게 하고, 값이 없는 상황을 표현하는 데 사용됩니다. Optional 객체를 활용하려면, 메서드를 사용하여 값을 추출하거나 변환하고, 기본값을 설정하거나, 값의 존재 여부를 확인할 수 있습니다.
11.3.1 Optional 객체 만들기
Optional은 값이 있을 수도 있고 없을 수도 있는 객체를 다룰 때 사용되며, Optional 객체를 생성하는 방법으로는 Optional.of(), Optional.ofNullable(), Optional.empty() 등이 있습니다.
빈 Optional
정적 팩토리 메서드 Optional.empty로 빈 Optional 객체를 얻을 수 있습니다.
Optional<Car> optCar = Optional.empty();
null이 아닌 값으로 Optional 만들기
또는 정적 팩토리 메서드 Optional.of로 null이 아닌 값을 포함하는 Optional을 만들 수 있습니다.
car가 null이라면 nullPointerException이 발생합니다.
Optional<Car> optCar = Optional.of(car);
null값으로 Optional 만들기
마지막으로 정적 팩토리 메서드 Optional.ofNullable로 null값을 저장할 수 있는 Optional을 만들 수 있습니다.
car가 null이면 빈 Optional 객체가 반환됩니다.
Optional<Car> optCar = Optional.ofNullable(car);
Optional은 get() 메서드로 값을 가져올 수 있지만, 비어있는 Optional에서 get()을 호출하면 예외가 발생합니다. 이를 방지하기 위해 Optional은 명시적인 null 검사를 제거할 수 있는 기능을 제공합니다. 이 기능은 스트림 연산에서 영감을 받았습니다.
11.3.2 맵으로 Optional의 값을 추출하고 변환하기
보통 객체의 정보를 추출할 때는 Optional을 사용할 때가 많습니다. 다음 코드처럼 접근하기 전에 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 메서드와 유사합니다. Optional이 값을 포함하면 제공된 함수를 적용하여 값을 변환하고, Optional이 비어있으면 아무 작업도 수행하지 않습니다. Optional은 최대 요소 개수가 1개인 데이터 컬렉션으로 생각할 수 있습니다.
11.3.3 flatMap으로 Optional 객체 연결
다음처럼 map을 이용해서 코드를 재구현할 수 있습니다.
Optional<Person> optPerson = Optional.of(person);
Optional<String> name = optPerson.map(Person::getCar)
.map(Car::getInsurance)
.map(Insurance::getName);
위 코드는 컴파일되지 않는데, optPeople.map()의 결과는 Optional <Optional <Car>> 형식이기 때문입니다. 이 문제를 해결하기 위해 flatMap 메서드를 사용할 수 있습니다. flatMap은 함수를 적용하여 생성된 모든 스트림을 하나로 병합하여 일차원 Optional로 평준화합니다.
11.3.4 Optional 스트림 조작
Java 9에서는 Optional에 stream() 메서드를 추가하여 Optional을 포함하는 스트림을 쉽게 처리할 수 있게 되었습니다. 이 기능은 Optional 스트림을 값을 가진 스트림으로 변환할 때 유용합니다. 11.3.4절에서는 다른 예제를 이용해 Optional 스트림을 어떻게 다루는지 설명합니다.
이 예제는 Person/Car/Insurance 코드에서 List<Person>을 인수로 받아 자동차를 소유한 사람들이 가입한 보험 회사의 이름을 Set <String>으로 반환하는 메서드를 구현한 것입니다.
public Set<String> getCarInsuranceNames(List<Person> persons) {
return persons.stream()
.map(Person::getCar)
.map(optCar -> optCar.flatMap(Car::getInsurance))
.map(optCar -> optIns.map(Insurance::getName))
.flatMap(Optional::new)
.collect(toSet());
위 예제는 getCar() 메서드가 Optional<Car>를 반환하기 때문에 사람이 자동차를 가지지 않을 수 있는 상황이 있어 첫 번째 map 변환을 수행하면 stream <Optional <Car>>를 얻습니다. 이후 두 개의 map 연산을 사용하여 Optional <Car>를 Optional <Insurance>로 변환하고, 스트림 대신 각 요소를 Optional <String>으로 변환합니다. 최종 결과로 Stream <Optional <String>>을 얻지만, 사람이 차를 가지지 않거나 차가 보험에 가입되어 있지 않아 결과가 비어있을 수 있습니다. Optional을 사용하면 널에 대한 걱정 없이 연산을 안전하게 처리할 수 있지만, 최종 결과를 얻으려면 빈 Optional을 제거하고 값을 언랩해야 하는 문제가 있습니다.
다음 코드처럼 filter, map을 순서적으로 이용해 결과를 얻을 수 있습니다.
Stream<Optional<String>> stream = ...
Set<String> result = stream.filter(Optional::isPresent)
.map(Optional::get)
.collect(toSet()):
Optional 클래스의 stream() 메서드를 이용하면 스트림의 요소를 한 번의 연산으로 Optional 객체를 포함하는 스트림으로 변환할 수 있습니다. 이 메서드를 flatMap 메서드와 함께 사용하면 두 수준의 스트림을 평면 스트림으로 변환할 수 있습니다. 이 기법을 사용하면 Optional을 언랩하고 비어있는 Optional을 건너뛸 수 있습니다.
11.3.5 디폴트 액션과 Optional 언랩
Optional 클래스는 orElse를 비롯한 다양한 메서드를 제공하여 빈 Optional일 경우 기본값이나 예외를 반환할 수 있습니다.
- get() 메서드는 Optional에 값이 있으면 해당 값을 반환하고 값이 없으면 NoSuchElementException 예외를 발생시키는데, 이는 값이 반드시 있다고 가정할 수 있는 상황이 아니면 사용하지 않는 것이 좋습니다. 이 때문에 get() 메서드를 사용하는 것은 중첩된 null 확인 코드를 사용하는 것과 크게 다르지 않습니다.
- orElse 메서드를 이용하면 Optional이 값을 포함하지 않을 때 기본값을 제공할 수 있습니다.
- orElseGet(Supplier<? extends T> other)는 Optional에 값이 없을 경우만 Supplier를 실행하는 버전의 메서드입니다. 이를 사용하면 디폴트 메서드를 만드는 시간이나 Optional이 비어있을 때만 기본값을 생성하고 싶은 경우에 유용합니다.
- ifPresent(Consumer <? super T> consumer)를 사용하면 Optional에 값이 존재할 때 인수로 넘겨준 동작을 실행할 수 있습니다. 값이 없으면 아무 일도 일어나지 않습니다.
자바 9에서는 다음의 인스턴스 메서드가 추가되었습니다.
- ifPresentOrElse(Consumer <? super T> action, Runnable emptyAction)은 Optional이 비어있을 때 실행할 수 있는 Runnable을 인수로 받는다는 점만 ifPresent와 다른 메서드입니다.
11.3.6 두 Optional 합치기
다은은 Person과 Car 정보를 이용해서 가장 저렴한 보험료를 제공하는 보험회사를 찾는 몇몇 복잡한 비즈니스 로직을 구현한 외부 서비스가 있다고 가정하고 구현한 코드입니다.
public Insurance findCheapestInsurance(Person person, Car car) {
// 다양한 보험회사가 제공하는 서비스 조회
// 모든 결과 데이터 비교
return cheapestCompany;
}
두 Optional 인스턴스를 인수로 받아 Optional<Insurance>를 반환하는 null 안전 메서드를 구현해야 한다고 가정합니다. 인수로 전달한 값 중 하나라도 비어있으면 빈 Optional <Insurance>를 반환하며, 이때 Optional 클래스의 isPresent() 메서드를 사용하여 값이 존재하는지 여부를 확인할 수 있습니다.
따라서 isPresent를 이용해서 다음처럼 코드를 구현할 수 있습니다.
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();
}
}
이 메서드의 장점은 person과 car의 시그니처만으로도 둘 다 아무 값도 반환하지 않을 수 있다는 정보를 명시적으로 보여준다는 것입니다. 그러나 구현 코드는 null 확인 코드와 크게 다른 점이 없다는 단점이 있습니다.
Optional 클래스와 Stream 인터페이스는 map과 flatMap 메서드 이외에도 다양한 비슷한 기능을 공유합니다.
11.3.7 필터로 특정값 거르기
객체의 메서드를 호출해서 프로퍼티를 확인할 때, 해당 객체가 null인지 확인해야 안전하게 작업을 수행할 수 있습니다. 예를 들어 보험 회사 이름이 'CambridgeInsurance'인지 확인해야 할 때, Insurance 객체가 null인지 확인한 다음 getName 메서드를 호출하는 것이 안전한 방법입니다.
Insurance insurance = ...;
if (insurance != null && "CambridgeInsurance".equals(insurance.getName())) {
System.out.println("ok");
}
// Optional 객체에 filter 메서드를 이용해서 다음과 같이 코드를 재구현 할 수 있습니다.
Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName()))
.ifPresent(x -> System.out.println("ok"));
filter 메서드는 Optional 객체에 프레디케이트를 적용하여 값이 일치하면 해당 값을 반환하고, 일치하지 않으면 빈 Optional을 반환하는 메서드이다. Optional이 비어있다면 filter는 아무 동작도 하지 않으며, 값이 있다면 프레디케이트를 적용하여 일치하지 않으면 빈 Optional로 만들고, 일치하면 아무 변화도 일어나지 않는다.
11.4 Optional을 사용한 실용 예제
새 Optional 클래스를 효과적으로 이용하려면 잠재적으로 존재하지 않는 값의 처리 방법을 바꿔야 합니다. 즉, 코드 구현만 바꾸는 것이 아니라 네이티브 자바 API와 상호작용하는 방식도 바꿔야 합니다. Optional 기능을 활용할 수 있도록 코드에 작은 유틸리티 메서드를 추가하는 방식으로 문제를 해결할 수 있습니다.
11.4.1 잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기
기존의 자바 API에서는 요청한 값이 없거나 계산에 실패했을 때 null을 반환하는 경우가 많습니다. 하지만 null을 반환하는 것보다는 Optional을 반환하는 것이 더 안전하고 바람직합니다. 예를 들어, Map의 get 메서드는 반환값을 Optional로 감쌀 수 있습니다. 이렇게 하면 코드에서 null 체크를 하지 않고도 NPE를 방지할 수 있습니다.
Map <String, Object> 형식의 맵이 있는데, 문자열 'key'에 해당하는 값이 없으면 null이 반환됩니다. map에서 반환하는 값을 Optional.ofNullable을 이용해서 개선할 수 있습니다.
Optional<Object> value = Optional.ofNullable(map.get("key"));
이와 같은 코드를 이용해서 null일 수 있는 값을 Optional로 안전하게 변환할 수 있습니다.
11.4.2 예외와 Optional 클래스
자바 API에서는 값을 제공할 수 없을 때 null 대신 예외를 발생시키는 경우가 있습니다.. 이를 해결하기 위해 예외를 발생시키는 메서드에서는 try/catch 블록을 사용해야 합니다. 하지만 이러한 경우에도 Optional을 사용하여 null 체크를 하지 않고 NPE를 방지할 수 있습니다. 이를 위해 parseInt를 감싸는 작은 유틸리티 메서드를 구현하여 Optional을 반환하도록 모델링할 수 있습니다. 예를 들어, stringToInt라는 메서드를 구현하여 문자열을 Optional <Integer>로 변환할 수 있습니다. 이를 포함하는 OptionalUtility 클래스를 만들면 거추장스러운 try/catch 로직을 사용하지 않고도 문자열을 Optional로 변환할 수 있습니다.
11.4.3 기본형 Optional을 사용하지 말아야 하는 이유
Optional 클래스도 기본형 특화된 OptionalInt, OptionalLong, OptionalDouble 등이 제공되며, 스트림과 마찬가지로 기본형 특화 Optional을 사용하면 성능을 향상할 수 있습니다. 하지만 Optional은 최대 요소 수가 한 개뿐이므로 기본형 특화 클래스로 성능을 개선할 수 없으며, map, flatMap, filter 등의 메서드를 지원하지 않아서 권장되지 않습니다. 또한 기본형 특화 Optional과 일반 Optional은 혼용할 수 없습니다.
'📚 서적 및 학습자료 > 모던 자바 인 액션[2회독]' 카테고리의 다른 글
[모던 자바 인 액션] 13장 디폴트 메서드 (0) | 2023.03.26 |
---|---|
[모던 자바 인 액션] 12장 새로운 날짜와 시간 API (0) | 2023.03.26 |
[모던 자바 인 액션] 10장 람다를 이용한 도메인 전용 언어 (0) | 2023.03.23 |
[모던 자바 인 액션] 9장 리팩터링, 테스팅, 디버깅 (0) | 2023.03.21 |
[모던 자바 인 액션] 8장 컬렉션 API 개선 (0) | 2023.03.18 |