해당 내용은 Java8 In Action 책을 요약 및 정리한 내용입니다.
좀 더 자세히 알고 싶으신 분들은 책을 사서 읽어보심을 추천드립니다.!
10.1 값이 없는 상황을 어떻게 처리할까
Entity
Car.java
package Part3.Chapter10.Chapter10_10_1.entity;
public class Car {
private Insurance insurance;
public Insurance getInsurance() {
return this.insurance;
}
}
Insurance.java
package Part3.Chapter10.Chapter10_10_1.entity;
public class Insurance {
private String name;
public String getName() {
return this.name;
}
}
Person.java
package Part3.Chapter10.Chapter10_10_1.entity;
public class Person {
private Car car;
public Car getCar() {
return this.car;
}
}
Main
package Part3.Chapter10.Chapter10_10_1;
import Part3.Chapter10.Chapter10_10_1.entity.Car;
import Part3.Chapter10.Chapter10_10_1.entity.Insurance;
import Part3.Chapter10.Chapter10_10_1.entity.Person;
/*
* 10.1 값이 없는 상황을 어떻게 처리할까?
*/
public class Main_10_1 {
/*
* 코드에 아무 문제가 없는 것처럼 보이지만 몇 가지 문제가 있다.
* Person객체가 null일수도 있고 pserson의 Car객체 null일수도 있으며
* Car객체의 Insurance객체 마찬가지로 null일수도 있다.
* 그러므로 총 3개의 객체가 null 일수도 있는 상황이 생긴다.
* 이 경우라면 런타임에 NullPointerException이 발생되면서 프로그램이 중단될수도 있다.
*/
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
/*
* 10.1.1 보수적인 자세로 NullPointerException 줄이기
*
* 변수를 참조할 때마다 null을 확인하며 중간 과정에 하나라도 null이 존재하면 "Unknown" 문자열을 반환한다.
* 상식적으로 모든 회사에는 이름이 있으므로 보험회사의 이름이 null인지는 확인하지 않았다.
* 우리가 확실히 알고 있는 영역을 모델링할 때는 이런 지식을 활용해서 null 확인을 생략할 수 있지만,
* 데이터를 자바 클래스로 모델링할 때는 이 같은(모든 회사는 반드시 이름을 갖는다) 사실을 단정하기 어렵다.
*
* 모든 변수가 null인지 의미하므로 변수를 접근할 때마다 중첩된 if가 추가되면서 코드 들여쓰기 수준이 증가한다.
* 이와 같은 반복패턴(recurring pattern)코드를 "깊은 의심-deep doubt"이라고 부른다.
*/
public String getNullCheckCarInsuranceName(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 블록을 없앴다.
*
* 하지만 이 코드도 좋은 코드는 아니다. 메서드에 네 개의 출구가 생겼기 때문이다.
* 출구가 많아지면 유지보수 하기가 어려워고 게다가 null일 때 반환 되는 기본값 "Unknown"이
* 세 곳에서 반복되고 있다. 이 같은 경우는 문자열을 상수로 만들어서 이 문제를 해결할 수 있다.
*/
public String getNullcheckCarInsuranceName2(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();
}
public static void main(String [] args) {
/*
* 10.1.2 null 때문에 발생하는 문제
*
* 자바에서 null 레퍼런스를 사용하면서 발생할 수 있는 이론적, 실용적 문제를 살펴보자.
*
* 에러의 근원이다.
* NullPointerException은 자바에서 가장 흔히 발생하는 에러다.
*
* 코드를 어지럽힌다.
* 때로는 중첩된 null 확인 코드를 추가해야 하므로 null 때문에 코드 가독성이 떨어진다.
*
* 아무 의미가 없다
* null은 아무 의미도 표현하지 않는다. 특히 정적 형식 언어에서 값이 없음을 표현하는 방법으로는
* 적철하지 않다.
*
* 자바 철학에 위배된다.
* 자바는 개발자로부터 모든 포인터를 숨겼다. 하지만 예외가 있는데 그것이 바로 null 포인터다.
*
* 형식 시스템에 구멍을 만든다.
* null은 무형식이며 정보를 포함하고 있지 않으므로 모든 레퍼런스 형식에 null을 할당할 수 있다.
* 이런 식으로 null이 할당되기 시작하면서 시스템의 다른 부분으로 null이 퍼졌을 때 애초에 null이
* 어떤 의미로 사용되었는지 알 수 없다.
*/
/*
* 10.1.3 다른 언어는 null 대신 무얼 사용하나?
*
* 그루비 같은 언어는 "안전 내비게이션 연산자-safe navigation operator(..?)"를 도입해서 null 문제 해결.
* def carInsuranceName = person?.car?.insurance?.name
*
* 하스켈은 선택형값(optional value)을 저장할 수 있는 Maybe라는 형식을 제공.
* Maybe는 주어진 형식의 값을 갖거나 아니면 아무 값도 갖지 않을 수 있다.
*
* 스칼라도 T 형식의 값을 갖거나 아무 값도 갖지 않을 수 있는 Option[T]라는 고제를 제공한다.
* Option 형식에서 제공하는 연산을 사용해서 값이 있는지 여부를 명시적으로 확인해야 한다.(즉 null 확인)
* 형식 시스템에서 이를 강제하므로 null과 관련한 문제가 일어날 가능성이 줄어든다.
*
* 자바 8은 '선택형값' 개념의 영향을 받아 java.util.Optional<T>라는 새로운 클래스를 제공한다.
*/
}
}
10.2 Optional 클래스 소개
Main
package Part3.Chapter10.Chapter10_10_2;
import java.util.Optional;
/*
* 10.2 Optional 클래스 소개
*
* 자바 8은 하스켈과 스칼라의 영향을 받아 java.util.Optional<T>라는 새로운 클래스를 제공한다.
* Optional은 선택형값을 캡슐화하는 클래스다.
*
* 값이 있으면 Optional 클래스는 값을 감싼다. 반면 값이 없으면 Optional.empty 메서드로 Optional을 반환한다.
* Optional.empty는 Optional의 특별한 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드다.
*
* null 레퍼런스와 Optional.empty()는 의미론상으론 둘이 비슷하지만 실제로는 차이점이 많다.
* null을 참조하면 NullPointerException이 발생하지만 Optional.empty()는 Optional 객체이므로 이를 다양한 방식으로
* 활용할 수 있다.
*/
public class Main_10_2 {
public static void main(String[] args) {
}
}
/*
* Optinal 클래스를 사용하면서 모델의 의미(semantic)가 더 명확해졌음을 알수 있다.
* 사람은 Optional<Car>를 참조하면서 차는 Optional<Insurace>를 참조하는데,
* 이는 사람이 자동차가 있을수도 없을수도, 자동차는 보험이 있을수도 없을수도 있음을 명확히 설명한다.
*
* 또한 보험회사 이름은 Optional<String>이 아니라 String 형식이므로 이는 보험회사는 반드시 이름을 가져야 함을 보여준다.
* 따라서 보험회사 이름을 참조할 때 NullPointerException이 발생할 수도 있다는 정보를 확인할 수 있다.
* 하지만 보험회사 이름이 null인지 확인하는 코드를 추가할 필요는 없다. 오히려 고쳐야 할 문제를 감추는 꼴이 되기 때문이다.
* 보험회사는 이름을 반드시 가져야 하며 이름이 없는 보험회사를 발견했다면 예외를 처리하는 코드를 추가하는 것이 아니라
* 보험회사 이름이 없는 이유가 무엇인지 밝혀서 문제를 해결해야 한다.
*
* Optional을 이용하면 값이 없는 상황이 우리 데이터에 문제가 있는 것인지 아니면 알고리즘의 버그인지 명확하게 구분할 수 있다.
* 모든 null 레퍼런스를 Optional로 대치하는 것은 바람직하지 않다.
* Optional의 역할은 더 이해하기 쉬운 API를 설계하도록 돕는 것이다. 즉, 메서드의 시그너처만 보고도 선택형값인지 여부를 구별할 수 있다.
* Optional이 등장하면 이를 언랩해서 값이 없을 수도 있는 상황에 적절하게 대응하도록 강제하는 효과가 있다.
*/
class Person {
// 사람은 차가 있을수도 없을수도 있으므로 Optional로 정의한다.
private Optional<Car> car;
public Optional<Car> getCar() {
return this.car;
}
}
class Car {
// 자동차가 보험에 가입되어 있을수도 없을수도 있으므로 Optional로 정의한다.
private Optional<Insurance> insurance;
public Optional<Insurance> getInsurance() {
return this.insurance;
}
}
class Insurance {
// 보험회사에는 반드시 이름이 있다.
private String name;
public String getName() {
return this.name;
}
}
10.3 Optional 적용 패턴
Main
package Part3.Chapter10.Chapter10_10_3;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
/*
* 10.3 Optional 적용 패턴
*/
public class Main_10_3 {
public static void main(String[] args) {
/*
* 10.3.1 Optional 객체 만들기
*/
/*
* 빈 Optional
* 정적 팩토리 메서드 Optional.empty로 빈 Optional 객체를 얻을수 있다.
*/
Optional<Car> optCar = Optional.empty();
System.out.println("빈 Optional : " + optCar);
Car car = new Car("avante");
Insurance insurance = new Insurance("sinnake");
/*
* null이 아닌 값으로 Optional 만들기
* 정적 팩토리 메서드 Optional.of로 null이 아닌 값을 포함하는 Optional을 만들 수 있다.
*
* 이제 car가 null이라면 즉시 NullPointerException이 발생한다.
* (Optional을 사용하지 않았다면 car의 프로퍼티에 접근하려 할 때 에러가 발생했을 것이다.)
*/
optCar = Optional.of(car);
System.out.println("null이 아닌 값으로 Optional 만들기 : " + optCar);
/*
* null로 Optional 만들기
*
* 정적 팩토리 메서드 Optional.ofNullable로 null을 저장할 수 있는 Optional을 만들 수 있다.
* car가 null이면 빈 Optional 객체가 반환된다.
*/
car = null;
optCar = Optional.ofNullable(car);
System.out.println("null로 Optional 만들기 : " + optCar);
/*
* 10.3.2 맵으로 Optional의 값을 추출하고 변환하기
*
* 보통 객체의 정보를 추출할 때는 Optional을 사용할 때가 많다.
* 예를 들어 보험회사의 이름을 추출한다고 가정하자.
* 아래 코드처럼 이름 정보에 접근하기 전에 insurance가 null인지 확인해야 한다.
*/
String name = null;
if(insurance != null) {
name = insurance.getName();
}
System.out.println("10.3.2 맵으로 Optional의 값을 추출하고 변환하기 : [name] = " + name);
/*
* 이런 유형의 패턴에 사용할 수 있도록 Optional을 map 메서드를 지원한다.
*
* Optional의 map 메서드는 스트림의 map 메서드와 개념적으로 비슷하다.
* 스트림의 map은 스트림의 각 요소에 제공된 함수를 적용하는 연산이다.
* 여기서 Optional 객체를 최대 요소의 개수가 한 개 이하인 데이터 컬렉션으로 생각할 수 있다.
*
* Optional이 값을 포함하면 map의 인수로 제공된 함수가 값을 바꾼다.
* Optional이 비어있으면 아무 일도 일어나지 않는다.
*/
Optional<Insurance> optInsurance = Optional.ofNullable(new Insurance("sinnake"));
Optional<String> optName = optInsurance.map(Insurance::getName);
System.out.println("10.3.2 맵으로 Optional의 값을 추출하고 변환하기 - map : [name] = " + optName);
/*
* 10.3.3 flatMap으로 Optional 객체 연결
*/
Optional<Person> mapPerson = Optional.ofNullable(new Person("kim sung wook")) ;
Optional<Car> mapCar = Optional.ofNullable(new Car("avante"));
Optional<Insurance> mapInsurance = Optional.ofNullable(new Insurance("sinnake"));
mapCar.get().setInsurance(mapInsurance);
mapPerson.get().setCar(mapCar);
/*
* mapPerson
* .map(Person::getCar)
* .map(Car::getInsurance)
* .map(Insurance::getName)
*
* 위 코드는 컴파일이 되지 않는다.
* mapPerson의 형식은 Optional<Person>이므로 map 메서드를 호출할 수 있다.
* 하지만 getCar는 Optional<Car> 형식의 객체를 반환한다.
* 즉, map 연산의 결과는 Optional<Optional<Car>> 형식의 객체다.
* getInsurance는 또 다른 Optional 객체를 반환하므로 getInsurance 메서드를 지원하지 않는다.
*/
/*
* Optional은 스트림의 flatMap처럼 인수로 받은 함수를 적용해서 생성된 각각의 스트림에서 콘텐츠만 남길 수 있다.
* 즉 이차원 Optional을 일차원 Optional로 평준화를 시킬 수 있다.
*/
System.out.println("10.3.3 flatMap으로 Optional 객체 연결 - flatMap : " + getCarInsuranceName(mapPerson));
/*
* Optional을 이용한 Person/Car/Insurance 참조 체인
*
* Person을 Optional로 감싼 다음에 flatMap(Person::getCar)를 호출했다. 이 호출은 두 단계의 논리적 과정으로 생각할 수 있다.
* 첫 번째 단계에서는 Optional 내부의 Person에 Function을 적용한다. 여기서는 Person의 getCar 메서드가 Function이다.
* getCar 메서드는 Optional<Car>를 반환하므로 Optional 내부의 Person이 Optional<Car>로 변환되면서 중첩 Optional이 생성된다.
* 따라서 flatMap연산으로 Optional을 평준화한다.
*
* 평준화 과정이란 이론적으로 두 Optional을 합치는 기능을 수행하면서 둘 중 하나라도 null이면 빈 Optional을 생성하는 연산이다.
* flatMap을 빈 Optional에 호출하면 아무 일도 일어나지 않고 그대로 반환된다.
* 반면 Optional이 Person을 감싸고 있다면 flatMap에 전달된 Function이 Person에 적용된다.
* Function을 적용한 결과가 이미 Optional이므로 flatMap 메서드는 결과를 그대로 반환할 수 있다.
*
* 두 번째 단계도 첫 번째 단계와 비슷하게 Optional<Car>를 Optional<Insurance>로 변환한다.
* 세 번째 단계에서는 Optional<Insurance>를 Optional<String>으로 변환한다.
* 세 번재 단계에서 Insurance.getName()은 String을 반환하므로 flatMap을 사용할 필요가 없다.
*
* 호출 체인 중 어떤 메서드가 빈 Optional을 반환한다면 전체 결과로 빈 Optional을 반환하고 아니면 관련 보험회사의 이름을 포함하는
* Optional을 반환한다.
* 호출 체인의 결과로 Optional<String>이 반환되는데 여기에 회사 이름이 저장되어 있을수도 없을수도 있다.
* Optional이 비어있을 때 디폴트 값(default value)을 제공하는 orElse 메서드를 사용했다.
* Optional은 디폴트값을 제공하거나 Optional을 언랩(unwrap)하는 다양한 메서드를 제공한다.
*/
/*
* 10.3.4 디폴트 액션과 Optional 언랩
*
* Optional이 비어있을 때 디폴트값을 제공할 수 있는 orElse 메서드로 값을 읽자.
* Optional 클래스는 Optional 인스턴스에서 값을 읽을 수 있는 다양한 인스턴스 메서드를 제공한다.
*
* - get()은 값을 읽는 가장 간단한 메서드면서 동시에 가장 안전하지 않는 메서드다.
* 래핑된 값이 있으면 값을 반환하고 값이 없으면 NoSuchElementException을 발생시킨다.
* 따라서 Optional에 값이 반드시 있다고 가정할 수 없으면 get 메서드를 사용하지 않는것이 좋다.
* 결국 이 상황은 중첩된 null 확인 코드를 넣는 상황과 다르지 않을수 있다.
*
* - orElse(T other) 메서드를 이용하면 Optional이 값을 포함하지 않았을 때 디폴트값을 제공한다.
*
* - orElseGet(Supplier<? extends T> other)는 orElse 메서드에 대응하는 게으른 버전 메서드다.
* Optional에 값이 없을 때만 Supplier가 실행되기 때문이다.
* 디폴트 메서드를 만드는데 시간이 걸리거나(효율성 때문에) Optional이 비었을 때만 디폴트값을 생성하고 싶다면
* (디플트값이 반드시 필요한 상황) orElseGet(Supplier<? extends T> other)를 사용해야 한다.
*
* - orElseThrow(Supplier<? extends X> exceptionSupplier)는 Optional이 비어있을 때 예외를 발생시킨다.
* get 메서드와 다르게 해당 메서드는 발생시킬 예외의 종류를 선택할 수 있다.
*
* - ifPresent(Consumer<? super T> consumer)를 이용하면 값이 존재할 때 인수로 넘겨준 동작을 실행할 수 있다.
* 값이 없으면 아무 일도 일어나지 않는다.
*/
/*
* 정 map으로 하고 싶다면 아래 메서드처럼 할순 있다.
*/
System.out.println("10.3.3 flatMap으로 Optional 객체 연결 - map : " + getCarInsuranceNameMAP(mapPerson));
/*
* 도메인 모델에 Optional을 사용했을 때 데이터를 직렬화할 수 없는 이유
*
* Optional 클래스는 필드 형식으로 사용할 것을 가정하지 않았으므로 Serializable 인터페이스를 구현하지 않았다.
* 따라서 우리 도메인 모델에 Optional을 사용한다면 직렬화(serializable)모델을 사용하는 도구나 프레임워크에서 문제가 생길 수 있다.
* 이와 같은 단점에도 불구하고 여전히 optional을 사용해서 도메인 모델을 구성하는 것이 바람직하다고 생각한다.
* 특히 객체 그래프에서 일부 또는 전체가 null일 수 있는 상황이라면 더욱 그렇다.
* 직렬화 모델이 필요하다면 아래 코드처럼 Optional로 값을 반환받을 수 있는 메서드를 추가하는 방식을 권장한다.
*
* public class Person {
* private Car car;
*
* public Optional<Car> getCarAsOptional() {
* return Optional.ofNullable(car);
* }
* }
*/
/*
* 10.3.5 두 Optional 합치기
*/
mapPerson = Optional.ofNullable(new Person("김성욱")) ;
mapCar = Optional.ofNullable(new Car("아반테"));
mapPerson.get().setCar(Optional.ofNullable(new Car("아반테")) );
System.out.println("10.3.5 두 Optional 합치기 : " + nullSafefindCheapestInsurance(mapPerson, mapCar).get());
System.out.println("10.3.5 두 Optional 합치기 : " + nullSafefindCheapestInsuranceQuiz(mapPerson, mapCar).get());
/*
* 10.3.6 필터로 특정값 거르기
*
* filter 메서드는 프레디케이트를 인수로 받는다.
* Optional 객체가 값을 가지고 프레디케이트와 일치하면 filter 메서드는 그 값을 반환하고 그렇지 않으면 빈 Optional 객체를 반환한다.
* Optional이 비어있다면 filter 연산은 아무 동작도 하지 않는다. Optional에 값이 있으면 그 값에 프레디케이트를 적용한다.
* 프레디케이트 적용 결과가 true면 Optional에는 아무 변화도 일어나지 않고 false면 값은 사라지고 빈 Optional이 된다.
*/
System.out.println(findInsurance("아반테"));
/*
* Optional 클래스의 메서드
*
* 메서드 : empty
* 설명 : 빈 Optional 인스턴스 반환
*
* 메서드 : filter
* 설명 : 값이 존재하고 프레디케이트와 일치하면 값을 포함하는 Optional 반환하고
* 값이 없거나 프레디케이트와 일치하지 않으면 빈 Optional을 반환.
*
* 메서드 : flatMap
* 설명 : 값이 존재하면 인수로 제공된 함수를 적용한 결과 Optional을 반환하고
* 값이 없으면 빈 Optional을 반환.
*
* 메서드 : get
* 설명 : 값이 존재하면 Optional이 감싸고 있는 값을 반환하고, 값이 없으면
* NoSuchElementException이 발생.
*
* 메서드 : ifPresent
* 설명 : 값이 존재하면 지정된 Consumer를 실행하고 값이 없으면 아무 일도 일어나지 않음.
*
* 메서드 : isPresent
* 설명 : 값이 존재하면 true 반환, 값이 없으면 false 반환.
*
* 메서드 : map
* 설명 : 값이 존재하면 제공된 매핑 함수를 적용함.
*
* 메서드 : of
* 설명 : 값이 존재하면 값을 감싸는 Optional을 반환하고, 값이 null이면 NullPointerException을 발생.
*
* 메서드 : ofNullable
* 설명 : 값이 존재하면 값을 감싸는 Optional을 반환하고, 값이 null이면 빈 Optional을 반환.
*
* 메서드 : orElse
* 설명 : 값이 존재하면 값을 반환, 값이 없으면 디폴트값을 반환.
*
* 메서드 : orElseGet
* 설명 : 값이 존재하면 값을 반환, 값이 없으면 Supplier에서 제공하는 값을 반환.
*
* 메서드 : orElseThrow
* 설명 : 값이 존재하면 값을 반환, 값이 없으면 Supplier에서 생성한 예외를 발쌩.
*/
}
public static Insurance findInsurance(String insuranceName) {
return Optional.ofNullable(new Insurance("신나게", "아반테", 10_000_000))
.filter(i -> i.getCarKind().equals(insuranceName))
.orElseGet(() -> new Insurance("없음", "없음", 0));
//.orElse(new Insurance("없음", "없음", 0));
}
public static Optional<Insurance> nullSafefindCheapestInsuranceQuiz(Optional<Person> person, Optional<Car> car) {
/*
* 첫 번재 Optional에 flatMap을 호출했으므로 첫 번째 Optional이 비어있다면 인수로 전달한 람다 표현식이
* 실행되지 않고 그대로 빈 Optional을 반환한다.
* 반면 persion 값이 있으면 flatMap 메서드에 필요한 Optional<Insurance>를 반환하는 Function의 입력으로
* person을 사용한다.
* 이 함수 바디에서는 두 번째 Optional에 map을 호출하므로 Optional이 car 값을 포함하지 않으면 Function은
* 빈 Optional을 반환하므로 결국 해당 메서드는 빈 Optional을 반환한다.
*/
return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}
public static Optional<Insurance> nullSafefindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
if(person.isPresent() && car.isPresent()) {
return Optional.of(findCheapestInsurance(person.get(), car.get())) ;
}
return Optional.empty();
}
public static Insurance findCheapestInsurance(Person person, Car car) {
List<Insurance> insurancesList = Arrays.asList(
new Insurance("신나게_1", "아반테", 70_000_0)
, new Insurance("신나게_2", "그렌져", 20_000_0)
, new Insurance("신나게_3", "비엠더블유", 30_000_0)
, new Insurance("신나게_4", "벤츠", 40_000_0)
, new Insurance("신나게_4", "벤츠", 20_000_0)
, new Insurance("신나게_5", "아반테", 50_000_0)
, new Insurance("신나게_6", "아반테", 60_000_0)
);
Insurance cheapesInsurace = insurancesList.stream()
.filter((insurances) -> Optional.ofNullable(car)
.map(Car::getName)
.orElse("Unknown")
.equals(insurances.getCarKind() ))
.filter((insurances) -> Optional.ofNullable(person.getCar())
.map(Optional::get)
.map(Car::getName)
.orElse("Unknown")
.equals(insurances.getCarKind() ))
.min(Comparator.comparingInt(Insurance::getPrice))
.orElse(new Insurance("없음", "없음", 0));
return cheapesInsurace;
}
public static String getCarInsuranceName(Optional<Person> person) {
return person
.flatMap((p) -> Optional.ofNullable(p.getCar()).map(c -> c.get()) )
.flatMap((c) -> Optional.ofNullable(c.getInsurance()).map(i -> i.get()) )
.map(Insurance::getName)
.orElse("Unknown");
}
public static String getCarInsuranceNameMAP(Optional<Person> person) {
return person
.map(Person::getCar)
.map(i -> i.get().getInsurance())
.map(s -> s.get().getName())
.orElse("Unknown");
}
}
class Person {
// 사람은 차가 있을수도 없을수도 있으므로 Optional로 정의한다.
private Optional<Car> car;
private String name;
public Person() { }
public Person(String name) {
this.name = name;
}
public Optional<Car> getCar() {
return this.car;
}
public void setCar(Optional<Car> car) {
this.car = car;
}
public String getName() {
return this.name;
}
}
class Car {
// 자동차가 보험에 가입되어 있을수도 없을수도 있으므로 Optional로 정의한다.
private Optional<Insurance> insurance;
private String name;
public Car() { }
public Car(String name) {
this.name = name;
}
public Optional<Insurance> getInsurance() {
return this.insurance;
}
public void setInsurance(Optional<Insurance> insurance) {
this.insurance = insurance;
}
public String getName() {
return this.name;
}
}
class Insurance {
// 보험회사에는 반드시 이름이 있다.
private String name;
private String carKind;
private int price;
public Insurance() { }
public Insurance(String name) {
this.name = name;
}
public Insurance(String name, String carKind, int price) {
this.name = name;
this.carKind = carKind;
this.price = price;
}
public void setName(String name) {
this.name = name;
}
public void setCarKind(String carKind) {
this.carKind = carKind;
}
public void setPrice(int price) {
this.price = price;
}
public String getName() {
return this.name;
}
public String getCarKind() {
return carKind;
}
public int getPrice() {
return price;
}
@Override
public String toString() {
return "{"
+ "price : " + this.price
+ ", name : " + this.name
+ ", carKind : " + this.carKind
+"}";
}
}
10.4 Optional을 사용한 실용 예제
Process
OptionalUtil.java
package Part3.Chapter10.Chapter10_10_4.optional;
import java.util.Optional;
public class OptionalUtil {
public static Optional<Integer> stringToInt(String s) {
/*
* 정수로 변환할 수 없는 문자열 문제를 빈 Optional로 해결할 수 있다.
* 즉, parseInt가 Optional을 반환하도록 모델링할 수 있다.
*/
try {
return Optional.of(Integer.parseInt(s));
} catch(NumberFormatException e) {
return Optional.empty();
}
}
public static Optional<Long> stringToLong(String s) {
try {
return Optional.of(Long.parseLong(s));
} catch(NumberFormatException e) {
return Optional.empty();
}
}
}
Main
package Part3.Chapter10.Chapter10_10_4;
import java.util.HashMap;
import java.util.Optional;
import java.util.Properties;
import Part3.Chapter10.Chapter10_10_4.optional.OptionalUtil;
/*
* 10.4 Optional을 사용한 실용 예제
*
* 자바 8에서 제공하는 Optional 클래스를 효과적으로 사용하려면 값이 없는 상황을 처리하던 기존의 알고리즘과는
* 다른 과점에서 접근해야 한다. 즉, 코드 구현만 바꾸는 것이 아니라 네이티브 자바 API와 상호작용하는 방식도 바꿔야 한다.
*/
public class Main_10_4 {
public static void main(String[] args) {
HashMap<String, String> hashMap = new HashMap<String, String>() {
private static final long serialVersionUID = 1L;
{
put("number", "123456");
put("string", "sinnake");
}
};
/*
* 10.4.1 잠재적으로 null이 될 수 있는 대상으 Optional로 감싸기
*
* 기존 자바 API에서는 null을 반환하면서 요청한 값이 없거나 어떤 문제로 계산에 실패했음을 알린다.
* 예를 들어 Map의 get 메서드는 요청한 키에 대응하는 값을 찾지 못했을 때 null을 반환한다.
* get 메서드의 시그너처는 우리가 고칠 수 없지만 get 메서드의 반환값은 Optional로 감쌀 수 있다.
*/
String value = hashMap.get("key");
System.out.println("[Normal] key - key Value : " + value);
/*
* map에서 반환하는 값을 Optional로 감싸서 개선할 수 있다.
* 코드가 복잡한 기존 if-then-else를 사용하지 않고 Optional.ofNullable를 이용할 수 있다.
*/
value = Optional.ofNullable(hashMap.get("string")).orElse("value");
System.out.println("[Optional] key - key Value : " + value);
/*
* 10.4.2 예외와 Optional
*
* 자바 API는 값을 제공할 수 없을 때 null을 반환하는 대신 예외를 발생시킬 때도 있다.
* 전형적인 예가 Integer.parseInt(String) 정적 메서드다. 이 메서드는 문자열을 정수로 바꾸지 못했을 때
* NumberFormatException을 발생시킨다.
* 기존 값이 null일 수도 있을 때는 null 여부를 확인했지만 예외를 발생시키는 메서드는 try/catch 블록을 사용해야 한다는 점이 다르다.
*
* OptionalUtil 같은 유틸리티 클래스를 만들어서 사용하면 유용하게 사용할 수 있다.
*/
System.out.println("10.4.2 예외와 Optional - 값이 문자열인 경우 : " + OptionalUtil.stringToInt(hashMap.get("string")).orElse(0));
System.out.println("10.4.2 예외와 Optional - 값이 숫자인 경우 : " + OptionalUtil.stringToInt(hashMap.get("number")).orElse(0));
System.out.println("10.4.2 예외와 Optional - null인 경우 : " + OptionalUtil.stringToInt(hashMap.get("key")).orElse(-1));
/*
* 기본형 Optional과 이를 사용하지 말아야 하는 이유
*
* 기본형으로 특화된 OptionalInt, OptionalLong, OptionalDouble 등의 클래스를 제공한다.
* 스트림이 많은 요소를 가질 때는 기본형 특화 스트림을 이용해서 성능을 향상시킬수 있다고 설명했다.
* 하지만 Optional의 최대 요소 수는 한 개이므로 Optional에서는 성능 개선을 할 수 없다.
*
* 그리고 기본 특화형 Optional은 map, flatMap, filter 등을 지원하지 않으므로 권장하지 않는다.
* 게다가 기본 특화형 Optional로 생성한 결과는 다른 일반 Optional과 혼용할 수 없다.
*/
/*
* 10.4.3 응용
*
* Properties를 읽어서 값을 초 단위의 지속시간(duration)으로 해석하는 예제이다.
* 지속시간은 양수여야 하고 문자열이 양의 정수를 가리키면 해당 정수를 반환하고 그 외에는 0을 반환한다.
*/
Properties props = new Properties();
props.put("a", "5");
props.put("b", "true");
props.put("c", "-3");
System.out.println("10.4.3 응용 - readDuration method : " + readDuration(props, "a"));
System.out.println("10.4.3 응용 - readDuration method : " + readDuration(props, "b"));
System.out.println("10.4.3 응용 - readDuration method : " + readDuration(props, "c"));
System.out.println("10.4.3 응용 - readDuration method : " + readDuration(props, "d"));
System.out.println("10.4.3 응용 - readDuration method : " + readDuration(props, null));
System.out.println("10.4.3 응용 - readDurationQuiz method : " + readDurationQuiz(props, "a"));
System.out.println("10.4.3 응용 - readDurationQuiz method : " + readDurationQuiz(props, "b"));
System.out.println("10.4.3 응용 - readDurationQuiz method : " + readDurationQuiz(props, "c"));
System.out.println("10.4.3 응용 - readDurationQuiz method : " + readDurationQuiz(props, "d"));
System.out.println("10.4.3 응용 - readDurationQuiz method : " + readDurationQuiz(props, null));
}
public static int readDurationQuiz(Properties props, String name) {
return Optional.ofNullable(props.getProperty(Optional.ofNullable(name).orElse("")) )
.flatMap(OptionalUtil::stringToInt)
.filter(p -> p > 0)
.orElse(0);
/*
return Optional.ofNullable(props.getProperty(Optional.ofNullable(name).orElse("")) )
.flatMap(p -> {
try {
return Optional.ofNullable(Integer.parseInt(p));
} catch(NumberFormatException e) {
return Optional.empty();
}
})
.filter(p -> p > 0)
.orElse(0);
*/
}
public static int readDuration(Properties props, String name) {
String value = "";
if(name != null ) {
value = props.getProperty(name);
}
if(value != null) {
try {
int i = Integer.parseInt(value);
if(i > 0) {
return i;
}
} catch(NumberFormatException e) { }
}
return 0;
}
}
요약
- 역사적으로 프로그래밍 언어에서는 null 레퍼런스로 값이 없는 상황을 표현해왔다.
- 자바 8에서는 값이 있거나 없음을 표현할 수 있는 클래스 java.util.Optional를 제공한다.
- 팩토리 메서드 Optional.empty, Optional.of, Optional.ofNullable 등을 이용해서 Optional 객체를 만들 수 있다.
- Optional 클래스는 스트림과 비슷한 연산을 수행하는 map, flatMap, filter 등의 메서드를 제공한다.
- Optional로 값이 없는 상황을 적절하게 처리하도록 강제할 수 있다. 즉, Optional로 예상치 못한 null 예외를 방지할 수 있다.
- Optional을 활용하면 더 좋은 API를 설계할 수 있다. 즉, 사용자는 메서드의 시그너처만 보고도 Optional값이 사용되거나 반환되는지 예측할 수 있다.