CHAP11 - Modern-Java-in-Action/Online-Study GitHub Wiki
- Optional Class doc
- Optional ์ฅ์ : ์ข์ API๋ฅผ ์ค๊ณ + null ํฌ์ธํฐ ์์ธ ๊ฐ์
์ด ์ฅ์ ๋ด์ฉ
- null ์ฐธ์กฐ์ ๋ฌธ์ ์ ๊ณผ null์ ๋ฉ๋ฆฌํด์ผ ํ๋ ์ด์
- null ๋์ Optional : nuII๋ก ๋ถํฐ ์์ ํ ๋๋ฉ์ธ ๋ชจ๋ธ ์ฌ๊ตฌํํ๊ธฐ
- Optional ํ์ฉ : null ํ์ธ ์ฝ๋ ์ ๊ฑฐํ๊ธฐ
- Optional์ ์ ์ฅ๋ ๊ฐ์ ํ์ธํ๋ ๋ฐฉ๋ฒ
- ๊ฐ์ด ์์ ์๋ ์๋ ์ํฉ์ ๊ณ ๋ คํ๋ ํ๋ก๊ทธ๋๋ฐ
ํฉํธ
- ๋ํฌ์ธํฐ ์๋ฌ ๋๋ฌด ๋ง์ด ๊ฒช์๋ค.
- null์ ๋ง๋ ์ฌ๋๋ ์๋ฐ์์ null ํฌ์ธํฐ๋ฅผ ๋ง๋ ๊ฒ์ ์ค์๋ผ๊ณ ๋งํ๋ค.
null ๋๋ฌธ์ ์ด๋ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋์ง ๊ฐ๋จํ ์์ ๋ก ์ดํด๋ณด์.
// Person -> Car -> Insurance
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();
}
- deep deoubt (๊น์ ์์ฌ): ์๋ if๋ฌธ ๋ฐ๋ณต๋ ํจํด
// ์๋ 1
// ํ๋๋ผ๋ null ์ฐธ์กฐ๊ฐ ์์ผ๋ฉด "Unknown" ๋ฆฌํด
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";
}
// ---------------------------------------
// ์๋ 2
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();
}
์งง๊ฒ๋งํด, null๋ก ๊ฐ์ด ์๋ค๋ ์ฌ์ค์ ํํํ๋ ๊ฒ์ ์ข์ ๋ฐฉ๋ฒ์ด ์๋๋ค.
- ๋ค๋ฅธ ์ธ์ด null ๋ฌธ์ ํด๊ฒฐ ์์
- ํ์ค์ผ: Maybe
- ์ค์นผ๋ผ: Option[T]
- ๊ทธ๋ฃจ๋น: ์์ ๋ด๋น๊ฒ์ด์ ์ฐ์ฐ์(?.)
// ๊ทธ๋ฃจ๋น ์์
def carInsuranceName = person?.car?.insurance?.name
- ์๋ฐ์์ , java.util.Optional
- Optional ํด๋์ค๊ฐ ๊ฐ์ฒด๊ฐ ์๋ null์ด๋ ๊ฐ์ธ์ค๋ค.
// BEFORE
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; }
}
// ---------------------------------------------------------
// AFTER
public class Person2 {
private Optional<Car> car;
public Optional<Car> getCar() { return car; }
}
public class Car2 {
private Optional<Insurance> insurance;
public Optional<Insurance> getInsurance() { return insurance; }
}
public class Insurance2 {
private String name;
public String getName() { return name; } // ๋ณดํ ๊ฐ์ฒด ์กด์ฌ์ ์ด๋ฆ์ด ๋ฐ๋์ ์์ด์ผํจ์ผ๋ก, ๋ฆฌํดํ์ด Optional์ด ์๋๋ค.
}
๋ชจ๋ null ์ฐธ์กฐ๋ฅผ Optional๋ก ๋์นํ๋ ๊ฒ ์ ๋ฐ๋์งํ์ง ์๋ค. Optional์ ์ญํ ์ ๋ ์ดํดํ๊ธฐ ์ฌ์ด API๋ฅผ ์ค๊ณํ๋๋ก ๋๋ ๊ฒ์ด๋ค. ์ฆ, ๋ฉ์๋์ ์๊ทธ๋์ฒ๋ง ๋ณด๊ณ ๋ ์ ํํ๊ฐ์ธ์ง ์ฌ๋ถ๋ฅผ ๊ตฌ๋ณํ ์ ์๋ค.
Optional๋ก ๊ฐ์ผ ๊ฐ์ ์ค์ ๋ก ์ด๋ป๊ฒ ์ฌ์ฉํ ์ ์์๊น? ์ผ๋จ, Optional ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด๋ณด์.
// 1 ๋น Optional ๊ฐ์ฒด ๋ง๋ค๊ธฐ
Optional<Car2> optCar1 = Optional.empty();
// 2 null์ด ์๋ ๊ฐ์ผ๋ก Optional ๋ง๋ค๊ธฐ
// null ์ด ์๋์ ํ์คํ ๊ฒฝ์ฐ
Optional<Car2> optCar2 = Optional.of(car);
// 3 null๊ฐ์ผ๋ก Optional ๋ง๋ค๊ธฐ
Optional<Ca2r> optCar3 = Optional.ofNullable(car);
์ฃผ์: get() ์ ์ด์ฉํ์ฌ Optional์ ๊ฐ์ ๊ฐ์ ธ์ฌ ์ ์๋๋ฐ, Optional์ด ๋น์ด์์ผ๋ฉด ํธ์ถ์ ์์ธ๊ฐ ๋ฐ์ํ๋ค. ์ฆ, Optional ์ฌ์ฉ์ ๊ฒฐ๊ตญ null์ ์ฌ์ฉํ์ ๋ ์ ๊ฐ์ ๋ฌธ์ ๋ฅผ ๊ฒช์ ์ ์๋ค.
// BEFORE
String name = null;
if(insurance != null){
name = insurance.getName();
}
// AFTER
// ๋ณดํ ๊ฐ์ฒด๋ฅผ Optional๋ก ๋ง๋ค๊ณ ,
// Optional๋ก ๊ฐ์ธ์ง ๊ฐ์ฒด์ getName์ ์คํํ๋ค.
// ๋ณดํ ๊ฐ์ฒด๊ฐ ๋์ด๋ฉด name์๋ Optional empty๊ฐ ๋ค์ด๊ฐ๊ณ
// ๊ฐ์ด ์์ ๊ฒฝ์ฐ name์๋ ๊ฐ์ด ๋ค์ด๊ฐ๋ค.
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
// ------------------------------------
// ๋ณดํ ์ด๋ฆ: aaaa๋ก
Insurance insurance1 = new Insurance("aaaa");
Optional<Insurance> optInsurance1 = Optional.ofNullable(insurance1);
Optional<String> ๋ณดํ1 = optInsurance1.map(Insurance::getName);
// // ๋ณดํ ์ด๋ฆ: ์ง์ x
Insurance insurance2 = new Insurance();
Optional<Insurance> optInsurance2 = Optional.ofNullable(insurance2);
Optional<String> ๋ณดํ2 = optInsurance2.map(Insurance::getName);
System.out.println(๋ณดํ1); // ๊ฒฐ๊ณผ: Optional[aaaa]
System.out.println(๋ณดํ2); // ๊ฒฐ๊ณผ: Optional.empty
- ์คํธ๋ฆผ์ flatMap ๋ฉ์๋์ Optional์ flatMap ๋ฉ์๋๋ ์ ์ฌํ๋ค.
- map๊ณผ flatmap ์๊ทธ๋์ฒ ์ฐจ์ด


// ์ปดํ์ผ ์๋ฌ ์์
// map -> map -> map ์ ํธ์ถํ๋๋ฐ, Optional<Optional<Optional<Car>>> ์ด๋ ๊ฒ ์ค์ฒฉ๋๋ค
Optional<Person2> optPerson = Optional.of(person2);
Optional<String> name =
optPerson.map(Person2::getCar)
.map(Car2::getInsurance)
.map(Insurance2::getName);
// ์คํธ๋ฆผ์ flatMap๊ณผ ๋น์ทํ๊ฒ, ์ต์
๋์์ flatMap ์ฌ์ฉํ๋ฉด ์ค์ฒฉ์ด ํ๋ฆฌ๋ฉด์ ์ผ์ฐจ์์ผ๋ก ๋ง๋ค์ด์ง๋ค.
// null ์ฒดํฌํ๋ if๋ฌธ ์์ด ์ฝ๋ ๊ฐ๊ฒฐ
public String getCarInsuranceName(Optional<Person> person) {
return person.flatMap(Person::getCar) // Optional์ ์ด์ฉํ ์ฐธ์กฐ ์ฒด์ธ
.flatMap(Car::getInsurance)
.map(Insurance::getName)
.orElse("Unknown"); // ๊ฐ์ด ์๋ค๋ฉด
}
// ------------------------------------------
// ์คํ ์์
// ์ฐจ ์๋ ์ฌ๋ ์์ฑ
Person2 person = new Person2(Optional.empty());
Optional<Person2> optPerson = Optional.of(person);
System.out.println(getCarInsuranceName3(optPerson));
// ๊ฒฐ๊ณผ: Unknown
- ํธ์ถ์ ๋
ผ๋ฆฌ์ ๊ณผ์
- 1๋จ๊ณ: Person์ Function์ ์ ์ฉ (getCar ๋ฉ์๋๊ฐ ํ์
)
- flatmap ์ฌ์ฉ ์ด์ : Optional์ด ๊ณ์ ์ค์ฒฉ๋จ์ผ๋ก ํ์คํ ํ๋ค.
- ํ์คํ ๊ณผ์ : ๋ Optional์ ํฉ์น๋ ๊ธฐ๋ฅ์ ์ํํ๋ฉด์ ๋ ์ค ํ๋๋ผ๋ null์ด๋ฉด ๋น Optional์ ์์ฑ
- flatmap ์ฌ์ฉ ์ด์ : Optional์ด ๊ณ์ ์ค์ฒฉ๋จ์ผ๋ก ํ์คํ ํ๋ค.
- 2๋จ๊ณ: 1๋จ๊ณ์ ๋์ผํ๊ฒ ์๋ํ๊ณ , Optional ๋ฆฌํด
- 3๋จ๊ณ: ์คํธ๋ง ๋ฐํํ๋ฏ๋ก, flatmap ์ฌ์ฉ ํ์ x
- 1๋จ๊ณ: Person์ Function์ ์ ์ฉ (getCar ๋ฉ์๋๊ฐ ํ์
)
๋๋ฉ์ธ ๋ชจ๋ธ์ Optional์ ์ฌ์ฉํ์ ๋ ๋ฐ์ดํฐ๋ฅผ ์ง๋ ฌํํ ์ ์์ง๋ง, ์ด๋ฐ ๋จ์ ์๋ ์ ์ Optional์ ์ฌ์ฉํด์ ๋๋ฉ์ธ ๋ชจ๋ธ์ ๊ตฌ์ฑํ๋ ๊ฒ์ด ๋ฐ๋์งํ๋ค๊ณ ๋งํ๋ค.
- ์๋ฐ9, Optional์ stream() ๋ฉ์๋๋ฅผ ์ถ๊ฐ
- flatMap(Optional::stream): Stream<Optional>์ ํ์ฌ ์ด๋ฆ์ ํฌํจํ๋ Stream์ผ๋ก ๋ณํ
// ์ฌ๋๋ค์ด ๊ฐ์
ํ ๋ณดํํ์ฌ ์ด๋ฆ๋ค์ ๋ฆฌํด
public static Set<String> getCarInsuranceNames(List<Person2> persons) {
return persons.stream()
.map(Person2::getCar) // ์ด๋ค ์ฌ๋์ ์๋์ฐจ๋ฅผ ๊ฐ์ง์ง ์์ ์๋ ์๋ค.
.map(optCar -> optCar.flatMap(Car2::getInsurance))
.map(optIns -> optIns.map(Insurance2::getName))
.flatMap(Optional::stream)
.collect(Collectors.toSet()); // ์ค๋ณต ์ ๊ฑฐ ๊ธฐ๋ฅ๋
}
// ------------------------------------------------
System.out.println(getCarInsuranceNames(personList)); // [a, b, c]
// ์ด 7๋ช
์ค์, ์ฐจ ์๋ 4๋ช
์ค 1๋ช
๋ง ๋นผ๊ณ ๋ณดํ์ ๋ค์๊ณ (๋ณดํ๋ช
: a,b,c), ๋ค๋ฅธ 3๋ช
์ ์ฐจ๊ฐ ์๋ค.
static List<Person2> personList = List.of(
new Person2(Optional.of(new Car2(Optional.of(new Insurance2("a"))))),
new Person2(Optional.of(new Car2(Optional.of(new Insurance2("b"))))),
new Person2(Optional.of(new Car2(Optional.of(new Insurance2("c"))))),
new Person2(Optional.of(new Car2(Optional.empty())))),
new Person2(Optional.empty()),
new Person2(Optional.empty()),
new Person2(Optional.empty())
);
Optional์์ stream ๋ฉ์๋๋ฅผ ์ ๊ณตํ์ง ์์๋ค๋ฉด, Optional์ isPresent ๋ฉ์๋๋ฅผ ์ฌ์ฉํด ๊ฐ๋ฉฐ, ์ง์ ์ฒ๋ฆฌํด์ค์ผํ๋ ๋ฒ๊ฑฐ๋ก์์ด ์์์๊ฒ์ด๋ค. (์ต์ ๋์ ์คํธ๋ฆผ ์๊ฒจ์ ์ข๋ค!)
- Optional unwrap ๋ฐฉ๋ฒ๋ค
- orElse("๋ํดํธ๊ฐ"): ๊ฐ์ด ์์๋ ๊ธฐ๋ณธ๊ฐ ์ ๊ณต.
- get(): ๊ฐ ๋ฐ๋ก ์ ๊ณต. ๊ฐ์ด ์๋ค๋ฉด, NoSuchElementException. ๊ทธ๋ฌ๋ ์กฐ์ฌ ์ฌ์ฉ.
- orElseGet(): ๊ฐ์ด ์์๋๋ง, Supplier๊ฐ ์คํ. Optional์ด ๋น์ด์์ ๋๋ง ๊ธฐ๋ณธ๊ฐ์ ์์ฑํ๊ณ ์ถ์๋ ์ฌ์ฉ.
- orElseThrow(): get๊ณผ ๋น์ท
- ifPresent(): ๊ฐ์ด ์กด์ฌํ ๋ ์ธ์๋ก ๋๊ฒจ์ค ๋์์ ์คํ. ์์ผ๋ฉด ์คํ x.
- ifPresentOrElse(): Optional์ด ๋น์์ ๋ ์คํํ ์ ์๋ Runnable์ ์ธ์๋ก ๋ฐ๋๋ค๋ ์ ๋ง ifPresent์ ๋ค๋ฅด๋ค.
// ifPresentOrElse ์์
Optional<Integer> op = Optional.empty();
try {
op.ifPresentOrElse(
(value) -> {
System.out.println( "Value is present, its: " + value);
},
() -> {
System.out.println("Value is empty");
});
} catch (Exception e) {
System.out.println(e);
}
// ๊ฒฐ๊ณผ: Value is empty
๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง ๊ฐ์ : ๊ฐ์ฅ ๋ณดํ๋ฃ ์ ๋ ดํ ํ์ฌ ์ฐพ๊ธฐ
// ์ฌ๋์ ๋ฐ๋ผ ๋ณดํ๋ฃ ๋ค๋ฅผํ
๋,
// ๊ฐ์ฅ ์ ๋ ดํ ๋ณดํ๋ฃ ์ฐพ๊ธฐ
public Insurance findCheapestInsurance(Person person, Car car) {
// ๋ก์ง: ๋ค์ํ ๋ณดํํ์ฌ๊ฐ ์ ๊ณตํ๋ ์๋น์ค ์กฐํ ํ ๋ชจ๋ ๊ฒฐ๊ณผ ๋ฐ์ดํฐ ๋น๊ต
return cheapestCompany;
}
Optional๋ฅผ ๋ฐํํ๋ ๋ฒ์ ์ ๊ตฌํํด์ผ ํ ๋
// ์๋ ๊ตฌํ ์ฝ๋๋ null ํ์ธ ์ฝ๋์ ํฌ๊ฒ ๋ค๋ฅธ ์ ์ด ์๋ค.
// ํด์ฆ: ๊ฐ์ ํด ๋ณด์
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();
}
}
Optional ์ธ๋ฉํ์ง ์๊ณ ๋ Optional ํฉ์น๊ธฐ
์ ๋ต
public Optional<Insurance> nullSafeFindCheapestInsurance(
Optional<Person> person, Optional<Car> car) {
return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}
๊ฐ์ฒด์ ๋ฉ์๋ ํธ์ถํด์ ์ด๋ค ํ๋กํผํฐ๋ฅผ ํ์ธํด์ผ ํ ๋๊ฐ ์๋ค.
// ๋ณดํํ์ฌ ์ด๋ฆ CambridgeInsurance์ธ์ง ํ์ธ
Insurance insurance = ...;
if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){
System.out.println("ok");
}
filter ์ฌ์ฉ: ์กฐ๊ฑด์ ๋ง์ผ๋ฉด ๊ฐ์ ๋ฐํํ๊ณ ๊ทธ๋ ์ง ์์ผ๋ฉด ๋น Optional ๊ฐ์ฒด๋ฅผ ๋ฐํ
// Optional์ผ๋ก ์ฌ๊ตฌํ
Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance ->
"CambridgeInsurance".equals(insurance.getName()))
.ifPresent(x -> System.out.println("ok"));]
Optional filtering
ํน์ ๋์ด ์ด์ ๋๋ ์ฌ๋๋ค์ด ๊ฐ์ ํ ๋ณดํ์ ์ฐพ์์ ๋ณด์. (minAge ์ด์ ์ผ๋๋ง)
public String getCarInsuranceName(Optional<Person> person, int minAge)
์ ๋ต
public String getCarInsuranceName(Optional<Person> person, int minAge) {
return person.filter(p -> p.getAge() >= minAge)
.flatMap(Person::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getName)
.orElse("Unknown");
}
๊ธฐ์กด์ ์ฝ๋๋ฅผ ์ฌ๊ตฌํํ๋๋ฐ ๋์์ด ๋๋ ๋ค์ํ ๊ธฐ๋ฒ์ ํ์ธํด๋ณด์.
Object value = map.get("key");
// ------------------------------------
// get์ ์๊ทธ๋์ฒ๋ฅผ ๊ณ ์น ์ ์์ง๋ง, ๋ฐ์๊ฐ์ ๊ฐ์ ์ ์๋ค.
Optional<Object> value = Optional.ofNullable(map.get("key"));
- Integer.parseInt()๋ ๊ฐ์ ์ ๊ณตํ ์ ์์ ๋ null์ ๋ฐํํ๋ ๋์ ์์ธ๋ฅผ ๋ฐ์์ํจ๋ค.
- e.g. Integer.parseInt("hello") -> NumberFormatException ์๋ฌ
// ์ด์ ๊ฐ์ ์ ํธ๋ฆฌํฐ ํด๋์ค๋ฅด ๋ง์ด ๋ง๋ค์!
public static Optional<Integer> stringToInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
// stringToInt("hello") -> ์๋ฌ ๋์ง ์๋๋ค. ์ต์
๋์ empty๋ฅผ ๋ฐ๋๋ค.
- Optionallnt, OptionalLong, OptionalDouble์ ์ฌ์ฉ ์์ ํ๊ธฐ.
- map, flatMap,filter ๋ฑ์ ์ง์ํ์ง ์๋๋ค.
- ๊ธฐ๋ณธํ์ ๊ธฐ์กด Optional ํผ์ฉ ์ฌ์ฉ ๋ถ๊ฐ
- ์, OptionalInt๋ฅผ ๋ฐํํ๋ค๋ฉด ์ด๋ฅผ ๋ค๋ฅธ Optional ์ flatMap์ ๋ฉ์๋ ์ฐธ์กฐ๋ก ์ ๋ฌํ ์ ์๋ค.
์ค์ ์ ๋ฌด์์ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ๋ณด์.
// ํ๋ก๊ทธ๋จ ์ค์ ์ธ์๋ก ํ๋กํผํฐ ์ ๋ฌ ์์
// ํค๋ฅผ ์ฃผ๊ณ ์์ ๊ฐ์ด ์์์ธ๊ฒ๋ง ๊ฐ์ ๊ฐ์ ธ์ค์. otherwise 0
@Test
public void testMap() {
Properties props = new Properties();
props.setProperty("a", "5");
props.setProperty("b", "true");
props.setProperty("c", "-3");
assertEquals(5, readDurationImperative(props, "a"));
assertEquals(0, readDurationImperative(props, "b"));
assertEquals(0, readDurationImperative(props, "c"));
assertEquals(0, readDurationImperative(props, "d"));
}
public static int readDurationImperative(Properties props, String name) {
String value = props.getProperty(name);
if (value != null) {
try {
int i = Integer.parseInt(value);
if (i > 0) { // ์์ ์ธ์ง ํ์ธ
return i;
}
} catch (NumberFormatException nfe) { // ์ซ์ ์๋ ๊ฒฝ์ฐ
}
}
return 0;
}
์ ํธ๋ฆฌํฐ ๋ฉ์๋๋ฅผ ์ด์ฉํด, ์ ์ฝ๋๋ฅผ ๊ฐ์ ํด๋ณด์.
Optional๋ก ํ๋กํผํฐ์์ ์ง์ ์๊ฐ ์ฝ๊ธฐ
@Test
public void testMap() {
Properties props = new Properties();
props.setProperty("a", "5");
props.setProperty("b", "true");
props.setProperty("c", "-3");
assertEquals(5, readDurationWithOptional(props, "a"));
assertEquals(0, readDurationWithOptional(props, "b"));
assertEquals(0, readDurationWithOptional(props, "c"));
assertEquals(0, readDurationWithOptional(props, "d"));
}
public static int readDurationWithOptional(Properties props, String name) {
return Optional.ofNullable(props.getProperty(name))
.flatMap(OptionalUtility::stringToInt)
.filter(i -> i > 0)
.orElse(0);
}