#12. Optional API (Null๊ฐ ์ฒ๋ฆฌ)
by JiwonDev์๋ฐ๋ ์ปดํ์ผ ์์ ์ ํ์ธ๊ฐ๋ฅํ null safe ์ฐ์ฐ์ ๋ฐ๋ก ์ ๊ณตํด์ฃผ์ง ์๋๋ค. ๊ทธ๋์ ๊ธฐ์กด์๋ ์กฐ๊ฑด๋ฌธ์ผ๋ก ํ์ธํ๊ฑฐ๋ ์์ธ๋ฅผ ๋์ ธ ์ฑ ์์ ์ฝ๋ ์ฌ์ฉ์์๊ฒ ๋๊ธฐ๋ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ์๋ค.
* ์ฐธ๊ณ ๋ก Optional์ Stream ์ ์ฉ ๋ฉ์๋๊ฐ ์๋๋ค. Optional.Stream() ํตํด ์คํธ๋ฆผ์ ์์ฑํ์ฌ(Java9๋ถํฐ ์ถ๊ฐ๋จ) ์ฌ์ฉํ๋ ๊ฒ์ด๋ค.
# NPE (NullPointerException)
์์๋ ์ธ๊ธํ์ง๋ง, ์๋ํ๋ ์๋ํ์ง์์๋ ๊ฐ์ฒด์ ๊ฐ์ ํ ๋นํด์ฃผ์ง์์ null์ด ๋ค์ด๊ฐ์ ์๊ธฐ๋ NPE ๋ฌธ์ ๋ ํํ ์ผ์ด๋๋ ์์ธ์ด๋ฉด์ ๊น๋ํ๊ฒ ํด๊ฒฐํ๊ธฐ๋ ์ด๋ ค์ด ์ฃผ์ ์๋ค. (* ์ด๋ฅผ ์กฐ๊ธ์ด๋๋ง ํด๊ฒฐํ๋ ค๊ณ 2020๋ Java14๋ถํฐ๋ instanceof ํค์๋์ NPE์ ๋ํ ์ค๋ฅ๋ฅผ ๋ ์์ธํ๊ฒ ๋ณด์ฌ์ฃผ๋ ๊ธฐ๋ฅ์ด ์ถ๊ฐ๋์๋ค.)
// ํธ๋ํฐ ์ ์กฐ์ฌ์ ์ด๋ฆ์ ๋ฐํํ๋ค. public String getPhoneManufacturerName(Person person) { // `person` ์ด null ์ด๋ฉด? // ์๋๋ฉด `getPhone()` ๋ฉ์๋์ ๊ฒฐ๊ณผ๊ฐ null ์ด๋ฉด? return person.getPhone().getManufacturer().getName(); } // ๋ง์ฝ.. ๋ด๊ฐ ๋ง๋ ๋ฉ์๋๊ฐ ์๋๋ผ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ฒ๋ผ ํธ์ถํ๋ ์
์ฅ์ด๋ผ๋ฉด? String manufacturerName = getPhoneManufacturerName(person); // manufacturer ๊ฐ null ์ ์ฐธ์กฐํ๊ณ ์๋ ๊ฒฝ์ฐ NPE๊ฐ ๋ฐ์ํ ์ ์๋ค. String lowerCaseName = manufacturerName.toLowerCase();
# NPE๋ฅผ ์๋ฐฉํ๋ ๊ณ ์ ์ ์ธ ๋ฐฉ๋ฒ
Java8 ์ด์ ์๋ ๋ค์๊ณผ ๊ฐ์ ๋ฐฉ์์ผ๋ก NPE๋ฅผ ์๋ฐฉํ์๋ค.
// ์ง๊ทธ๋ฌ์ด ์์ธ์ฒ๋ฆฌ ์ฝ๋ public String getPhoneManufacturerName(Person person) { if (person != null) { Phone phone = person.getPhone(); if (phone != null) { Manufacturer manufacturer = phone.getManufacturer(); if (manufacturer != null) { return manufacturer.getName(); } } } return "Samsung"; }
๊ทธ๋ฌ๋ฉด์ ๊ฐ๋ฐ์๋ค์ด ์์ด๋์ด๋ฅผ ๋ธ ๊ฒ์ด, ๋ ๊ฐ์ฒด ํจํด(Null Object Pattern)์ด ์๋ค. ์ด ํจํด์ ํต์ฌ์ null ํค์๋๋ฅผ ์ฌ์ฉํ๋ ๋์ ์ด๋ฅผ ๋์ฒดํ ์ ์๋ null ๋ด๋น ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด ์ฌ์ฉํ๋ ๊ฒ์ด์๋ค.
/* ๋ ๊ฐ์ฒด(Null Object) ํด๋์ค๋ฅผ ๋ฐ๋ก ๊ตฌํ. */ class NullMessenger implements Messenger { @Override public void send(String msg) { // ๊ทธ๋ฅ ๋น ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ค. ์๋ฌด ๋์๋ ํ์ง ์๋๋ค. } } /* null ๊ฐ์ฒด๊ฐ ์๋ ํด๋์ค๋ค. */ interface Messenger { void send(String msg); } class Kakao implements Messenger { @Override public void send(String msg) {...} } class Line implements Messenger { @Override public void send(String msg) {...} }
class MessengerFactory { public static Messenger getMessenger(String name) { if ( ... ){ return ... // ํน์ ์กฐ๊ฑด์ ๋ง๋ Messenger ๊ตฌํ์ฒด ํด๋์ค์ ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค. } // ๊ฒฐ๊ณผ๊ฐ ์๋ ๊ฒฝ์ฐ `null`์ ๋ฐํํ์ง ์๊ณ `NullMessenger`๋ฅผ ๋ฐํํ๋ค. return new NullMessenger(); } } // ์ค์ ์ฌ์ฉํ๋ ์์ Messenger messenger = MessengerFactory.getMessenger("KakaoTalk"); messenger.send("Hello");
์ด๋ ๊ฒ ํจ์ผ๋ก์ NPE ์๋ฌ๋ ํผํ ์ ์์์ง๋ง, ๋ชจ๋ ๊ฐ๋ฐ์๊ฐ ํด๋น ๊ฐ์ฒด์ 'Null ์ ์ฉ ํด๋์ค'๋ฅผ ์๊ณ ์์ด์ผ ์ฌ์ฉํ ์ ์๋ค๋ ๋จ์ ์ด ์์๋ค. ๋ง์ฝ ์ธํฐํ์ด์ค์ ๋ด์ฉ์ด ์ถ๊ฐ๋๊ฑฐ๋ ์ ํํ ํด๋์ค ๊ตฌํ์ ์ ๋ชจ๋ฅธ๋ค๋ฉด ๊ธฐ์กด์ if(..null) ์ค๋ณต์ฒ๋ฆฌ๋ณด๋ค ํจ์ฌ ๋ ๋ณต์กํ ๊ฒ์ฌ ์ฝ๋๋ฅผ ์์ฑํด์ผ ํ ์ง๋ ๋ชจ๋ฅธ๋ค.
# Optional Class์ ๋ฑ์ฅ
์ด๋ฌํ ๋ฌธ์ ์ ๋ค์ ํด๊ฒฐํ๊ธฐ์ํด Java8 ๋ถํฐ ๊ณต์์ ์ผ๋ก ์ ๊ณตํ๋ Null ์ ์ฉ ๊ฐ์ฒด๊ฐ Optional Class์ด๋ค. ์์์ ๋งํ null-object pattern์ ์๋ฐ์์ ๊ณต์์ ์ผ๋ก ๊ตฌํํ๊ณ ํ์คํ ํ ๊ฒ์ด๋ค.
๋ค๋ฅธ ํด๊ฒฐ์ฑ ์ ์์๋์?
Java1.7์ null-safe ์ฒ๋ฆฌ๋ฅผ ์ํ ์ฐ์ฐ์ ๋์ ํ๋ ค๊ณ ์๋ํ์๋ค. ( Project Coin ) ํ์ง๋ง ์๋ก์ด ์์ ์ฅ์น๋ฅผ ์ ์ฉํ๋ ์ด์ ๋ณด๋ค ๋์ ํ์ ๋ JVM์ด๋ ์ฝ๋๋ฅผ ์์ฑํจ์ ์์ด ์ถ๊ฐํด์ผํ๋ ๋น์ฉ์ด๋ ํด๊ฒฐํด์ผํ ๋ฌธ์ ์ ๋ค์ด ์์๊ธฐ์ ๊ฒฐ๊ณผ์ ์ผ๋ก ์น์ธ ๋์ง๋ ์์๋ค.
// Java7์ Project Coin - ์๋น์ค ์ฐ์ฐ์๊ฐ ์ ์ฉ๋์๋ค๋ฉด ์ด๋ฐ ์ฝ๋๊ฐ ๊ฐ๋ฅํ์ง ์์์๊น? public String getPhoneManufacturerName(Person person) { // ์ฒด์ด๋ ์ค์์ null์ด ์๋ค๋ฉด, "Samsung"์ ๋ฐํํ๋ค. return person?.getPhone()?.getManufacturer()?.getName() : "Samsung"; }
# Optional ์์ฑ
์ ๋๋ฆญ ํ์ ์ผ๋ก ์ ์ธํ๊ณ , ์ ์ฉ Static Factory Method๋ฅผ ์ด์ฉํ์ฌ ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค.
Optional<Person> person; // Person ํ์
์ `Optional` ๋ณ์ Optional<Phone> phone; // phone ํ์
์ `Optional` ๋ณ์ // ์ฌ์ฉ์์ phone1 = Optional.empty(); phone2 = Optional.ofNullable(phone);
# Optional.empty()
๋น์ด์๋ Optional ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค. ๋ด๋ถ์ ์ผ๋ก๋ ์ฑ๊ธํด ์ธ์คํด์ค๋ฅผ ๋ฐํํ๊ฒ ๋๋ค.
// ๋ฉ์๋ ์๊ทธ๋์ฒ // `Optional` ํด๋์ค์ ์ ์ ๋ฉค๋ฒ๋ก ์ ์ธ๋์ด ์ฑ๊ธํค์ ๊ตฌํํจ. private static final Optional<?> EMPTY = new Optional<>(); public static<T> Optional<T> empty() { Optional<T> t = (Optional<T>) EMPTY; return t; } // ์์ Optional<String> emptyOpt = Optional.empty();
# Optional.of ( T value )
๊ฐ์ด null ์ด๋ผ๋ฉด NPE ์์ธ๋ฅผ ๋์ง๋ค. ๋ง์ฝ ํด๋น ๊ฐ์ฒด์ ๊ฐ์ด ๋ฐ๋์ ์์ด์ผ ํ๋ ๊ฒฝ์ฐ ์ฌ์ฉํ๋ฉด ๋๋ค.
// ๋ฉ์๋ ์๊ทธ๋์ฒ public static <T> Optional<T> of(T value); // ์์ Optional<String> opt = Optional.of("result");
# Optional.ofNullable( T value )
๊ฐ์ด null์ผ ์๋ ์๋ T ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค. ๋ง์ฝ ๊ฐ์ด null์ด๋ผ๋ฉด ๋น์ด์๋ Optional ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค.
// ๋ฉ์๋ ์๊ทธ๋์ฒ public static <T> Optional<T> ofNullable(T value) { return value == null ? empty() : of(value); } // ์์ Optional<String> opt = Optional.ofNullable(null);
# Optional ์ค๊ฐ ์ฐ์ฐ
# Filter
// ๋ฉ์๋ ์๊ทธ๋์ฒ public Optional<T> filter(Predicate<? super T> predicate); // ๊ฐ์ด ๋น์ด์๋ค๋ฉด(null์ด๋ผ๋ฉด) NPE ์์ธ๋ฅผ ๋ฐ์์ํจ๋ค. Optional.of("True").filter((val) -> "True".eqauls(val)).orElse("NO DATA"); // "True" Optional.of("False").filter((val) -> "True".eqauls(val)).orElse("NO DATA"); // "NO DATA" // ์กฐ๊ฑด์ ๋ง๋ Optional(=person) ๊ฐ์ฒด๋ฅผ ๋ฐ๋๋ค. ๋น์ด์๋ค๋ฉด empty Optional์ ๋ฐํํ๋ค. Optional.ofNullable(person).filter(p -> p.getName().equals("Kimtaeng"));
# map
mapper ํจ์๋ฅผ ํตํด ์ ๋ ฅ ๊ฐ์ ์์ ํ๋ค.
// ๋ฉ์๋ ์๊ทธ๋์ฒ public<U> Optional<U> map(Function<? super T, ? extends U> mapper); // ์์ Integer test = Optional.of("1").map(Integer::valueOf).orElseThrow(NoSuchElementException::new); // string to integer // Optional ๊ฐ์ฒด(=person)๊ฐ ๊ฐ์ด ์๋ค๋ฉด map์ ์คํํ๊ณ , ๋น์ด์๋ค๋ฉด ์๋ฌด๋ฐ ์ฐ์ฐ์ ํ์ง ์๋๋ค. Optional.ofNullable(person).map(p -> p.getName());
# flatMap
์ค์ฒฉ๋ Optional ๊ตฌ์กฐ๋ฅผ ํ ๋จ๊ณ ์์ ๊ณ ๋จ์ผ ์์๋ก ๋ง๋ค์ด์ค๋ค. ์ดํด๊ฐ ๋์ง์๋๋ค๋ฉด ์ด ๋งํฌ๋ฅผ ์ฐธ์กฐํ์.
// ๋ฉ์๋ ์๊ทธ๋์ฒ public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper); // ์์ String result = Optional.of("result") .flatMap((val) -> Optional.of("good")) .get(); System.out.println(result); // print 'good'
class Person { private Optional<Phone> phone; // getter, setter ์๋ต } /* person ๊ฐ์ฒด๋ฅผ ์ฃผ๋ฉด ํด๋น ๊ฐ์ฒด๊ฐ ๊ฐ์ง๊ณ ์๋ phone์ ๋ฐํํจ.*/ // ์๋ํ์ง ์๋ ์ฝ๋. Optional ์ผ๋ก Optional<phone>๋ฅผ ๊ฐ์ธ๊ณ ์๋ค. public Optional<Phone> testMap(Person person) { // Optional<Optional<Phone>> ํ์
์ด๊ธฐ ๋๋ฌธ์ ์ปดํ์ผ ์ค๋ฅ ๋ฐ์ return Optional.ofNullable(person) .map(Person::getPhone); } // ์๋ํ๋ ์ฝ๋. flatMap์ ์ด์ฉํ์ฌ Optional<> ์ค์ฒฉ์ ํ ๋จ๊ณ ์ ๊ฑฐํด์ค๋ค. // Optional<Optional<phone>>์ Optional<Phone>์ผ๋ก ์ ๊ทผํ๋ค. Stream flatmap๊ณผ ๋์๋ฐฉ์์ด ์ ์ฌํ๋ค. public Optional<Phone> testFlatMap(Person person) { return Optional.ofNullable(person) .flatMap(Person::getPhone); }
# or (null ๋์ฒด๊ฐ ์ง์ )
๊ฐ์ด ์กด์ฌํ๋ ๊ฒฝ์ฐ Optional ๊ฐ์ฒด๋ฅผ, ์๋ ๊ฒฝ์ฐ ๋น์ด์๋ Optional์ด ์๋๋ผ or(~)์ ๋ฑ๋กํ Optional ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค.
Optional.ofNullable("Hi") .filter(value -> value.length() > 3) // `filter` ์กฐ๊ฑด์ ๊ฑธ๋ฌ์ง. ๋น Optional ๋ฐํ .or(() -> Optional.of("Hello")) // ๊ฐ์ด ์์ผ๋ฏ๋ก ๋์ฒด Optional[Hello] ๋ฐํ
# Stream์ผ๋ก ๋ณํ
์ค๊ฐ์ ๋น์ด์๋ Optional ๊ฐ์ฒด๊ฐ ๋ฐํ๋๋ ๊ฒฝ์ฐ, Stream.empty()๊ฐ ๋ฐํ๋๋ฉฐ ํจ์ ์ฒด์ด๋์ด ๋ ์ด์ ์งํ๋์ง ์๋์ ์ ์ ์ํ์. ์คํธ๋ฆผ ์ฐ์ฐ์ด ๋ฉ์ถ๋ค.
public void testOptionalWithStream() { // ์ฐธ๊ณ ๋ก List.of๋ ์๋ฐ 9์์ ์ถ๊ฐ๋ ํฉํฐ๋ฆฌ ๋ฉ์๋. ํํ์ ๋ง๋ ๋ค. // Unmodifiable list(์์ ๋ถ๊ฐ๋ฅํ ๋ฆฌ์คํธ)๋ฅผ ๋ฐํํ๋ฏ๋ก ์ฃผ์ List.of(4, 3, 2, 5) .stream() // stream์ผ๋ก ๋ณต์ฌํด์ ์ฌ์ฉ๊ฐ๋ฅํ๋ค. .map(value -> value > 2 ? Optional.of(value) : Optional.empty()) .flatMap(Optional::stream) .forEach(System.out::println); }
๋ฌผ๋ก ๋๋ค์๊ณผ ๋ฉ์๋ ๋ ํผ๋ฐ์ค๋ ์ ํ์ฌํญ์ด๋ค. ๊ผญ ํ์ํ๋ค๋ฉด ์ต๋ช ํด๋์ค๋ฅผ ์ฌ์ฉํด๋ ์๊ด์๋ค.
// ์์ ์์ ํ ๊ฐ์ ์ฝ๋์ด๋ค. ์ดํด๋ฅผ ๋๊ธฐ์ํด ์ค๊ฐ์ค๊ฐ println์ ์ถ๊ฐํ์๋ค. public void testOptionalWithStream() { List.of(4, 3, 2, 5) .stream() .map(value -> { return value > 2 ? Optional.of(value) : Optional.empty(); }) .peek(v -> { // ๋์ด์ค๋ ๊ฐ์ ์ถ๋ ฅํ๊ธฐ ์ํจ System.out.println("peek: " + v); }) .flatMap(new Function<Optional<? extends Object>, Stream<?>>() { @Override public Stream<?> apply(Optional<?> o) { System.out.println("flatMap: " + o); return o.stream(); } }) .forEach(v -> { System.out.println("forEach: " + v); }); }
# Optional ๊ฐ ๊บผ๋ด์ค๊ธฐ (์ข ๋จ ์ฒ๋ฆฌ)
๋ธ๋ก๊ทธ์ ์ ๋ณด
JiwonDev
JiwonDev