자바 옵셔널 (Java Optional)
개요
자바 옵셔널Java Optional에 대해 알아보고 자바 8부터 시작해 9, 10까지 추가된 옵셔널 메서드를 소개합니다.
Java 8 Optional
자바 옵셔널은 자바 8에서 최초로 도입 되었습니다. 도입이 된 이유는 너무 명확합니다. 바로 null 때문입니다. null 은 자바 프로그래머들에게 편안함보단 불편함을 더 많이 제공하게 되었습니다. 간단한 코드를 통해 알아볼까요?
class Person { private String name; // constructor, getter, setter method 생략 } class House { private Person onwer; private String address; // constructor, getter, setter method 생략 }
위와 같은 클래스들이 있다고 생각해봅시다. Person 클래스는 한 사람을 표현합니다. House는 주인과 주소를 표현할 수 있어요. 누구든지 이 클래스를 사용해서 표현할 수 있는것이 명확합니다.
어떤 프로그래머가 시스템 콘솔에 집 정보를 노출하기 위해서 다음과 같은 코드를 구현했다고 가정합니다.
void main() { House house = houseService.getRandomHouse(); // 이 메서드는 자신이 가지고 있는 집들 중 아무거나 반환합니다. null 은 나오지 않는다고 가정합니다. System.out.println("onwer: " + house.getOnwer().getName()); System.out.println("address: " + house.getAddress()); }
집에 주인과 주소가 있다면 별 문제 없이 잘 실행 될겁니다. 그런데 만약 주인이 없는 집이 있다면 어떻게 될까요?
void main() { House house = houseService.getRandomHouse(); System.out.println("onwer: " + house.getOnwer().getName()); // java.lang.NullPointerException 발생! System.out.println("address: " + house.getAddress()); }
콘솔에 노출하려고 할 때 NPE 예외가 일어나서 프로그램이 제대로 수행 되지 않을겁니다. 그러면 이러한 상황을 막기 위해서는 어떻게 해야할까요? 다음 코드를 봅시다.
void main() { House house = houseService.getRandomHouse(); if (house.getOnwer() != null) { System.out.println("onwer: " + house.getOnwer().getName()); } System.out.println("address: " + house.getAddress()); }
간단하게 if 분기문을 사용해 해결했습니다. 그러면 새로운 요구 사항을 넣어봅시다. 주인은 있는데 주인의 이름이 없거나, 집 주소가 없는 경우에는 콘솔에 노출하지 않게 해주세요. 라고 하는 요구 사항을 말이죠. 기존 코드를 활용해서 다시 작성해 봅시다.
void main() { House house = houseService.getRandomHouse(); if (house.getOnwer() != null && house.getOnwer().getName() != null) { System.out.println("onwer: " + house.getOnwer().getName()); } if (house.getAddress() != null) { System.out.println("address: " + house.getAddress()); } }
요구 사항대로는 동작하는 코드가 완성 되었습니다. 그런데 해놓고보니 너무 코드가 너저분하네요. 이번에 우리가 알게될 Optional 을 사용하면 다음처럼 코드를 작성할 수 있습니다.
void main() { House house = houseService.getRandomHouse(); Optional.of(house) .map(House::getOnwer) .map(Person::getName) .ifPresent(name -> System.out.println("onwer:" + name)); Optional.of(house) .map(House::getAdress) .ifPresent(address -> System.out.println("address:" + address)); }
이렇게 해놓고 나서 새로운 요구 사항이 왔습니다. 사실은 말이야, 콘솔에 노출하지 않는것보다는 집주인이 없으면 "없음"이라고 나오게 해주고 주소도 없으면 "발급 되지 않음"이라고 나오게 하는게 좋을 것 같아. 라고 말이죠. 이번에도 코드를 다시 작성합니다.
void main() { House house = houseService.getRandomHouse(); String onwerName = Optional.of(house).map(House::getOnwer).map(Person::getName).orElse("없음"); String address = Optional.of(house).map(House::getAdress).orElse("발급 되지 않음"); System.out.println("onwer:" + onwerName); System.out.println("address:" + address); }
이렇게 다시 한번 요구 조건을 만족시켰습니다. 코드가 왜인지 모르겠지만 가독성이 좋아진거 같지 않나요?
간단한 예제를 보았으니 지금부터 옵셔널에서 제공하는 메서드들을 살펴봅니다.
Optional 객체 생성
Optional 객체를 생성하기 위해서는 아래와 같은 메서드들을 사용해야 합니다. 즉, 시작 연산자라고 보시면 됩니다.
Optional.of
value가 null 인 경우 NPE 예외를 던집니다. 반드시 값이 있어야 하는 객체인 경우 해당 메서드를 사용하면 됩니다.
// 메서드 시그니처 public static <T> Optional<T> of(T value); // 예제 Optional<String> opt = Optional.of("result");
Optional.ofNullable
value가 null 인 경우 비어있는 Optional 을 반환합니다. 값이 null 일수도 있는 것은 해당 메서드를 사용하면 됩니다.
// 메서드 시그니처 public static <T> Optional<T> ofNullable(T value); // 예제 Optional<String> opt = Optional.ofNullable(null);
Optional.empty
비어있는 옵셔널 객체를 생성합니다. 조건에 따라 분기를 태워야하고 반환할 값이 없는 경우에도 사용됩니다.
// 메서드 시그니처 public static<T> Optional<T> empty(); // 예제 Optional<String> emptyOpt = Optional.empty();
비어있는 옵셔널 객체
이 포스트를 읽다보면 중간에 옵셔널이 '비어있다'는 표현이 등장합니다. 이것은 옵셔널 객체는 있지만 옵셔널 객체가 가진 유효한 객체가 없는 경우 비어있다라고 표현하겠습니다. 아래의 그림을 보세요.
* 비어있는 옵셔널 객체 ---------- |Optional| 이 옵셔널 객체는 생성은 되었으나 값(객체)을 가지지 않았다. | | 이 상태는 Optional.empty() 라고 볼 수 있다. | | ---------- * 값이 있는 옵셔널 객체 ---------- |Optional| 이 옵셔널 객체는 생성 되고 값(객체)을 가지고 있다. |--------| ||String|| |--------| ----------
Optional 중간 처리
옵셔널 객체를 생성한 후 사용 가능한 메서드입니다. 해당 메서드들은 다시 옵셔널을 반환하므로 메서드 체이닝을 통해 원하는 로직을 반복 삽입할 수 있습니다.
filter
predicate 값이 참이면 해당 필터를 통과시키고 거짓이면 통과 되지 않습니다.
// 메서드 시그니처 public Optional<T> filter(Predicate<? super T> predicate); // 예제 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"
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
flatMap
mapper 함수를 통해 입력값을 다른 값으로 변환하는 메서드입니다. map() 메서드와 다른점은 메서드 시그니처의 매개변수입니다. map() 에서는 제너릭으로 U를 정의했지만 flatMap() 에서는 제너릭으로 Optional(U)를 정의했습니다.
이것이 뜻하는 바는 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'
Optional 종단 처리
종단 처리라는 것은 옵셔널 객체의 체이닝을 끝낸다는 것입니다.
ifPresent
최종적으로 연산을 끝낸 후 값이 비어있지 않다면 입력값으로 주어집니다. 이 값을 가지고 원하는 작업을 수행하면 됩니다. 하지만 중간 연산을 하다 비어있는 옵셔널 객체를 받게 되면 .ifPresent() 메서드의 내용을 수행하지 않습니다.
// 메서드 시그니처 public void ifPresent(Consumer<? super T> consumer); // 예제1 Optional.of("test").ifPresent((value) -> { // something to do }); // 예제2 (ifPresent 미수행) Optional.ofNullable(null).ifPresent((value) -> { // nothing to do });
isPresent
최종적으로 연산을 끝낸 후 객체가 존재하는지 여부를 판별합니다.
// 메서드 시그니처 public boolean isPresent(); // 예제 Optional.ofNullable("test").isPresent(); // true Optional.ofNullable("test").filter((val) -> "result".eqauls(val)).isPresent(); // false
get
최종적으로 연산을 끝낸 후 객체를 꺼냅니다. 이 때, 비어있는 옵셔널 객체였다면 예외가 발생합니다.
// 메서드 시그니처 public T get(); // 예제 Optional.of("test").get(); // 'test' Optional.ofNullable(null).get(); // NoSuchElementException!
orElse
최종적으로 연산을 끝낸 후에도 옵셔널 객체가 비어있다면 기본값으로 제공할 객체를 지정합니다.
// 메서드 시그니처 public T orElse(T other); // 예제 String result = Optional.ofNullable(null).orElse("default"); System.out.println(result); // print 'default'
orElseGet
최종적으로 연산을 끝낸 후에도 옵셔널 객체가 비어있다면 기본값으로 제공할 공급자 함수Supplier를 지정합니다.
// 메서드 시그니처 public T orElseGet(Supplier<? extends T> other); // 예제 String result = Optional.ofNullable("input").filter("test"::equals).orElseGet(() -> "default"); System.out.println(result); // print 'default'
orElseThrow
최종적으로 연산을 끝낸 후에도 옵셔널 객체가 비어있다면 예외 공급자 함수를 통해 예외를 발생시킵니다.
// 메서드 시그니처 public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X; // 예제 Optional.ofNullable("input").filter("test"::equals).orElseThrow(NoSuchElementException::new);
orElse, orElseGet 중 무엇을 사용할까?
orElse 메서드는 옵셔널 객체가 비어있든 비어있지 않든 반드시 실행 합니다. orElseGet 메서드는 옵셔널 객체가 비어있으면 실행합니다. 따라서 기본값을 주고자 할때에 기본값을 구하는 과정이 오래 걸린다면 orElseGet을 사용합시다.
또 한가지의 측면은 orElse 메서드가 받는 매개변수는 객체입니다.
Optional.ofNullable(something).orElse(new Something());
orElseGet 메서드가 받는 매개변수는 공급자 함수Supplier입니다. 이는 기본값으로 제공할 때 사용할 비즈니스 로직을 메서드 안으로 포함 할 수 있다는 것입니다.
Optional.ofNullable(something).orElseGet(() -> { // business logic ... return value; });
이외에도 적절하게 판단해서 사용하시면 됩니다.
Optional<List<T>> vs List<T>
List는 항상 변수를 할당할때마다 초기화해주는 습관이 필요합니다. List를 Optional로 감싸게 되면 Optional로 체크하고 내부의 List 도 체크해야하는 상태가 생겨 코드 가독성이 좋지 않아요. 따라서 항상 아래의 코드처럼 List를 사용할때에 List를 채워주면 좋겠습니다.
List data = Optional.ofNullable(somethingList).orElse(Collections.emptyList()); if (!data.isEmpty()) { // do something... }
Java 9
자바 9에 와서도 몇가지 추가된 옵셔널 메서드들이 있습니다. 자바 8 옵셔널에서 조금 부족했던 부분을 보완하는 메서드들입니다.
or
중간 처리 메서드로 기본값을 제공할 수 있는 공급자 함수를 정의할 수 있습니다. 기존에 제공중인 .orElseGet()과 유사하지만 다른점은 중간에 체이닝을 통해 우선 순위를 결정할 수 있다는 것입니다. 물론 .or() 연산 중에 비어있는 옵셔널이 된다면 다음 .or() 메서드로 진행하게 됩니다.
// 메서드 시그니처 public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier); // 예제 String result = Optional.ofNullable("test") .filter(value -> "filter".equals(value)) .or(Optional::empty) .or(() -> Optional.of("second")) .orElse("final"); System.out.println(result); // print 'second'
.ifPresentOrElse
.ifPresentOrElse()는 종단 처리 메서드입니다. .ifPresent메서드와 유사해보이지만 한가지 매개변수를 더 받을 수 있습니다. 두번째 매개변수인 emptyAction인데요. 첫번째 매개변수인 action은 유효한 객체를 받을 경우 실행하고, 두번째 매개변수인 emptyAction은 유효한 객체를 받지 못한 경우 실행합니다.
// 메서드 시그니처 public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction); // 예제 Optional.ofNullable("test") .ifPresentOrElse(value -> System.out.println(value), () -> System.out.println("null")); // print 'test' Optional.ofNullable(null) .ifPresentOrElse(value -> System.out.println(value), () -> System.out.println("null")); // print 'null'
.stream
.stream() 메서드는 중간 처리 연산자로 기존 자바 8에서 옵셔널 객체가 바로 스트림 객체로 전환 되지 않아 불편했던 부분을 해소시켜줍니다. 아래의 예제는 리스트에서 일부 값만 추출하고 스트림으로 변환한 뒤 다시 리스트로 수집하는 과정을 보여줍니다.
// 메서드 시그니처 public Stream<T> stream(); // 예제 List<String> result = List.of(1, 2, 3, 4) .stream() .map(val -> val % 2 == 0 ? Optional.of(val) : Optional.empty()) .flatMap(Optional::stream) .map(String::valueOf) .collect(Collectors.toList()); System.out.println(result); // print '[2, 4]'
Java 10
자바 10에서도 하나의 메서드가 추가 되었습니다.
orElseThrow
매개변수가 필요없는 예외 메서드를 추가했습니다. 이 메서드를 사용하면 기존에는 아래처럼 호출해야 했던 코드를 간결하게 해줍니다.
// 메서드 시그니처 public T orElseThrow(); // 예제 (자바 8) Optional.ofNullable(something).orElseThrow(NoSuchElementException::new); // 예제 (자바 10) Optional.ofNullable(something).orElseThrow();
그외 Optional 클래스
Optional 클래스는 기본형을 제외하고 OptionalInt, OptionalLong, OptionalDouble 클래스를 별도로 제공하고 있습니다. 사용법은 기본형과 대동소이하니 직접 메서드 시그니처를 보면서 이해하는 것도 좋을 것 같네요.
마무리
지금까지 자바 옵셔널에 대해서 알아봤습니다. 한마디 덧붙이고 싶은것은 옵셔널이 있으니 if 분기문을 전부 Optional로 바꿔야지! 같은 생각을 할 수도 있을것 같아요. 하지만 if 분기문도 적절하게 필요한 곳에는 삽입 하는것이 오히려 코드 가독성이 더 좋아질 수 있다고 생각합니다.
반드시 옵셔널로 해야한다는 생각보다는 무엇이든 적절한 위치에 적절한 코드로 넣어줍시다!
잘못된 점이 있으면 편하게 피드백 부탁드려요. :)