본문 바로가기

Java

Java Optional 사용법

원문 : https://www.baeldung.com/java-optional, https://www.baeldung.com/java-9-optional

 

1. 개요

Java SE 8 에 소개된 java.util.Optional<T> 클래스는 Haskell과 Scala 로부터 영감을 받아 개발 되었고, 아무 값도 없는(null) 값을 표현하는데 사용된다.

NPE(NullPointException)를 방지하기 위한 null 체크와 같은 역할을 하는데 편리한 기능을 제공하기때문에 객체타입을 반환하는 함수를 작성할때 null 반환될 여지가 있는 경우 반환타입을 Optional로 해주면 좋다.

 

2. Optional 객체 생성하기

값이 비어 있는 Optional 객체를 생성할때는 간단히 Optional.empty() 정적(static) 메소드를 이용하면 된다.

void whenCreatesEmptyOptional_thenCorrect() {
    Optional<String> emtpy = Optional.empty();
    // isPresent() 메소드는 Optional 객체가 값을 가지고 있는지 여부를 boolean 값으로 반환한다.
    assertFalse(emtpy.isPresent());
}

또다른 정적 메소드인 Optional.of(value) 메소드를 이용해 Optional 객체를 생성 할 수도 있다.

메소드의 인자로 null 을 전달하면 에러가 발생하기때문에 null 이 올 가능성 있다면 Optiona.ofNullable(value) 함수를 이용하도록 하자.

@Test
void givenNonNull_whenCreatesNonNullable_thenCorrect() {
    Optional<String> opt = Optional.of("String value");
    assertTrue(opt.isPresent());
}

@Test
void givenNull_whenThrowsErrorOnCreate_thenCorrect() {
    // of() 메소드는 null 값을 인자로 받으면 NullPointerException을 발생시킨다.
    assertThrows(NullPointerException.class, () -> Optional.of(null));
}

@Test
void givenNoneNull_whenCreateNullable_thenCorrect() {
    Optional<String> opt = Optional.ofNullable("String value");
    assertTrue(opt.isPresent());

    // ofNullable() 메소드는 null 값을 인자로 받아도 NullPointerException을 발생시키지 않는다.
    String nullValue = null;
    Optional<String> opt2 = Optional.ofNullable(nullValue);
    assertFalse(opt2.isPresent());
}

 

3. 값이 있는지 확인 하기: isPresent(), isEmpty()

값이 있는지는 isPresent() 로, 값이 없는지는 isEmpty() (Java11 에 추가)로 확인 할 수 있다.

@Test
void givenOptional_whenIsPresentWorks_thenCorrect() {
    Optional<String> opt = Optional.of("String value");
    assertTrue(opt.isPresent());

    opt = Optional.ofNullable(null);
    assertFalse(opt.isPresent());
}

@Test
void givenAnEmptyOptional_thenIsEmptyBehavesAsExpected() {
    Optional<String> opt = Optional.of("String value");
    assertFalse(opt.isEmpty());

    opt = Optional.ofNullable(null);
    assertTrue(opt.isEmpty());
}


4. ifPresent()를 이용한 조건부 동작

Optional 이전에 nullable 한 값을 사용하기 위해서는 아래와 같은 null 확인을 꼭 해줘야 했다. 값을 사용하기 전에 매번 확인해야 하고, NPE의 주요한 원인이 된다.

if (name != null) {
	System.out.println(name.length());
}

Optional 객체는 ifPresent(Consumer<? super T> action) 함수를 이용해서 이를 간단히 해결할 수 있다. ifPresent() 함수는 Consumer를 인자로 받아서 Optional 객체가 값이 있는 경우 인자로 받은 Consumer를 실행한다.

@Test
void givenOptional_whenIfPresentWorks_thenCorrect() {
    Optional<String> opt = Optional.of("String value");
    opt.ifPresent(val -> System.out.println(val));
}

 

5. orElse()를 이용한 기본값 설정

orElse(T other) 함수는 Optional 객체가 값이 없는 경우 반환할 기본값을 인자로 받아 값이 있는 경우 해당 값을 반환하고, 그렇지 않은경우 함수의 인자로 전달한 값을 반환한다.

@Test
void whenOrElseWorks_thenCorrect() {
    String value = "String value";
    String valueReturned = Optional.ofNullable(value).orElse("default");
    assertEquals(value, valueReturned);

    String nullValue = null;
    String nullValueReturned = Optional.ofNullable(nullValue).orElse("default");
    assertEquals("default", nullValueReturned);
}

 

6. orElseGet()을 이용한 기본값 설정

orElse() 함수와 비슷하지만 orElse() 함수가 값을 인자로 받는데 반해 orElseGet(Supplier<? extends T> supplier) 함수는 supplier 함수형 인터페이스를 인자로 받는다. 그래서 Optional 객체가 값을 가지고 있으면 값을 반환하고, 그렇지 않은경우 supplier를 통해 생성된 값을 반환한다.

@Test
void whenOrElseGetWorks_thenCorrect() {
    String value = "String value";
    String valueReturned = Optional.ofNullable(value).orElseGet(() ->"default");
    assertEquals(value, valueReturned);

    String nullValue = null;
    String nullValueReturned = Optional.ofNullable(nullValue).orElseGet(() -> "default");
    assertEquals("default", nullValueReturned);
}

 

7. orElse()와 orElseGet()의 차이점

orElse()와 orElseGet()은 똑같은 역할을 하고, 파라미터의 타입만 다르다. 그래서 아래와 같은 함수가 있을때

public String getMyDefault() {
    System.out.println("Getting Default Value");
    return "Default Value";
}

아래처럼 orElse() 의 인자에 함수를 호출해서 결과를 넘기는 식으로 코드를 작성하면 결국 똑같은거 아니냐 하는 생각을 할 수 있다. 실제로 아래 예는 두 함수고 똑같이 동작한다.

@Test
void whenOrElseGetAndOrElseOverlap_thenCorrect() {
    String text = null;

    System.out.println("Using orElseGet:");
    String defaultText = Optional.ofNullable(text).orElseGet(this::getMyDefault);
    assertEquals("Default Value", defaultText);

    System.out.println("Using orElse:");
    defaultText = Optional.ofNullable(text).orElse(getMyDefault());
    assertEquals("Default Value", defaultText);
}

하지만 아래 예제는 다르다.

@Test
void whenOrElseGetAndOrElseDiffer_thenCorrect() {
    String text = "Text present";

    System.out.println("Using orElseGet:");
    String defaultText = Optional.ofNullable(text).orElseGet(this::getMyDefault);
    assertEquals("Text present", defaultText);

    System.out.println("Using orElse:");
    defaultText = Optional.ofNullable(text).orElse(getMyDefault());
    assertEquals("Text present", defaultText);
}

// 결과 ===========
// Using orElseGet:
// Using orElse:
// Getting Default Value

orElseGet() 함수의 경우 Optional 객체에 값이 없을때만 인자로 전달 받은 함수가 실행되어 기본 값이 반환 되지만, orElse() 함수는 Optional 객체의 값 존재 여부와 관계 없이 함수가 실행된다. 위 예제에서는 getMyDefault() 함수가 간단한 함수여서 크게 문제가 되지 않지만 해당 함수가 부하를 주거나 실행에 시간이 오래 걸리는 함수라면 전체 성능에 영향을 줄 수 있다.

 

8. orElseThrow()를 이용한 예외 발생

orElseThrow() 함수는 Optional 객체에 값이 없는 경우 예외를 던진다. 파라미터로는 값이 없을때 던질 예외를 제공하는 Supplier를 받는다. Java 10 부터는 파라미터 없는 함수가 추가 되었으며, 파라미터 없이 함수를 호출 하면, NoSuchElementException 이 던져진다.

@Test
void whenOrElseThrowWorks_thenCorect() {
    String nullValue = null;
    assertThrows(
            IllegalArgumentException.class,
            () -> Optional.ofNullable(nullValue).orElseThrow(IllegalArgumentException::new)
    );

    assertThrows(
            NoSuchElementException.class,
            () -> Optional.ofNullable(nullValue).orElseThrow()
    );
}

 

9. get()를 이용한 값 반환

get() 함수는 위 orElse... 함수들과 마찬가지로 Optional 객체에 값이 있으면 해당 값을 반환한다. 하지만 값이 없다면 위 함수들과 다르게 NoSuchElementException 이 던져진다. 

값이 없다고 예외가 발생한다면 Optional 을 굳이 사용할 필요가 없다. 혹시 null 인경우 예외를 발생시키고 싶다면 좀더 명확한 이름의 orElseThrow() 함수를 사용하는 편이 훨씬 더 좋기 때문에 get() 함수를 사용하는것은 피하는것이 좋다.

 

10. filter()를 이용한 조건부 값 반환

filter(Predicate<? super T> predicate) 함수는 predicate 를 인자로 받고 이 predicate는 Optional 의 값을 인자로 test를 수행한다. 그래서 test 결과가 true이면 원래의 Optional 객체가 반환되지만, fales 인 경우에는 값이 비어 있는 Optional 객체를 반환한다.

@Test
void whenOptionalFilterWorks_thenCorrect() {
    Integer year = 2016;
    Optional<Integer> yearOptional = Optional.of(year);
    boolean is2016 = yearOptional.filter(y -> y == 2016).isPresent();
    assertTrue(is2016);

    boolean is2017 = yearOptional.filter(y -> y == 2017).isPresent();
    assertFalse(is2017);
}

filter() 함수는 이런식으로 우리가 원치 않는 값이 들어왔을때 이를 걸러내는 기능을 구현 할 수 있다.

다른 예제를 살펴보자, Modem 을 사려고 하는데 가격이 적절한지를 검사하는 로직을 구현한다고 가정해보자.

일반적인 경우라면 아래 priceIsInRange1() 함수처럼 인자로 받은 modem 에 대한 null 체크, modem의 가격에 대한 null 체크 를 수행한 이후 가격이 적절한 범위인지 if 문의 조건으로 검사한다.

private class Modem() {
    private Double price;

    public Double getPrice() {
        return price;
    }

    public Modem(Double price) {
        this.price = price;
    }
}

public boolean priceIsInRange1(Modem modem) {
    if (modem != null && modem.getPrice() != null
            && (modem.getPrice() >= 10
                && modem.getPrice() <= 15)) {
        return true;
    }

    return false;
}

하지만 Optional 을 활용한다면, 아래 priceIsInRange2() 함수처럼 null 체크도 필요없이 필요한 로직만 작성하면 되는 편리함을 얻을 수있다.

public boolean priceIsInRange2(Modem modem) {
    return Optional.ofNullable(modem)
            .map(Modem::getPrice)
            .filter(p -> p >= 10)
            .filter(p -> p <= 15)
            .isPresent();
}

 

11. map()을 이용한 값 변환

map(Function<? super T, ? extends U> mapper) 함수는 인자로 받는 Function을 이용해서 Optional 객체의 값으로 어떤 연산을 수행하여 다른 값으로 변경하고 이 변경된 값을 포함하는 Optional 객체를 반환한다.

map() 함수와 filter() 함수를 결합하여 강력한 기능을 구현할 수 있다. 아래 예제는 패스워드를 입력받아 이 패스워드가 올바른 패스워드인지 검사하는 로직을 구현한 것이다.

@Test
void givenOptional_whenMapWorksWithFilter_thenCorrect() {
    String password = " password ";
    Optional<String> passOpt = Optional.of(password);
    boolean correctPassword = passOpt
            .map(String::trim) //map() 을 이용해 입력받은 문자열을 정리(trim) 한다.
            .filter(pass -> pass.equals("password")) //패스워드가 정확한지 확인한다.
            .isPresent();

    assertTrue(correctPassword);
}

 

12. flatMap()을 이용한 값 변환

flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) 함수는 map() 함수와 마찬가지를 값을 변경하는 기능을 수행한다.

다른점은 map() 함수의 경우 Function<? super T, ? extends U> 타입을 인자로 받아, Optional 객체의 값(T 타입)을 U타입으로 변경한 후 Optional 객체로 감싸서 결과를 반환하는 반면,

flatMap() 함수는 Function<? super T, ? extends Optional<? extends U>> 타입을 인자로 받아, Optional 객체의 값(T 타입)을 변경하여 Optional<U> 로 변경하여 결과를 반환한다.

설명이 복잡한데 아래 예를 보고 이해하자

// 아래와 같이 속성들이 nullable 해서 get 함수가 Optional 형태를 반환하는 객체가 있을때
private class Person {
    private String name;
    private int age;

    public Optional<String> getName() {
        return Optional.ofNullable(name);
    }

    public Optional<Integer> getAge() {
        return Optional.ofNullable(age);
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

@Test
void givenOptional_whenFlatMapWorks_thenCorrect() {
    Person person = new Person("john", 26);
    Optional<Person> personOptional = Optional.of(person);
	
    // map을 사용하면 getName() 함수의 결과가 Optional<String> 이기때문에
    // 이 값이 Optional로 래핑되어 Optional<Optional<String>> 결과가 반환된다.
    Optional<Optional<String>> nameOptionalWrapper = personOptional.map(Person::getName);
    
    // 그래서 실제 값을 꺼내 쓰려면 값을 2번 꺼내야(.orElseThrow(), .orElse()) 한다.
    Optional<String> nameOptional = nameOptionalWrapper.orElseThrow(IllegalArgumentException::new);
    String name1 = nameOptional.orElse("");
    assertEquals("john", name1);
	
    // 하지만 flatMap을 사용한다면 getName() 함수 결과 그대로인 Optional<String> 이 반환되어
    // 한번만 값을 꺼내면된다.
    String name = personOptional
            .flatMap(Person::getName)
            .orElse("");
    assertEquals("john", name);
}

 

13. Chaining 

"복수개의 Optional 객체에서 값이 존재하는 첫번째 Optional 을 반환하는 기능" 등의 기능을 구현하기 위해서는 아래와 같이 Optional 객체를 엮어서(chain) Stream API를 활용하면 가능하다.

private Optional<String> getEmpty() {
    System.out.println("get empty optional");
    return Optional.empty();
}

private Optional<String> getHello() {
    System.out.println("get hello optional");
    return Optional.of("hello");
}

private Optional<String> getBye() {
    System.out.println("get bye optional");
    return Optional.of("bye");
}

@Test
void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturned() {
    Optional<String> found = Stream.of(getEmpty(), getHello(), getBye())
            .filter(Optional::isPresent)
            .map(Optional::get)
            .findFirst();

    assertEquals(getHello(), found);
}

// 실행 결과
// get empty optional
// get hello optional
// get bye optional
// get hello optional

근데 위와 같이 작성하면 get 함수 (getEmpty(), getHello(), getBye()) 들이 Optional 값이 있던 없던 무조건 실행된다 (실행 결과 참고).

이를 방지 해가 위해서는 Stream.of() 함수의 인자로 메소드 참조가 담긴 Supplier 인터페이스를 넘기면 된다.

void givenThreeOptionals_whenChaining_thenFirstNonEmptyIsReturnedAndRestNotEvaluated() {
    Optional<String> found = Stream.<Supplier<Optional<String>>>of(this::getEmpty, this::getHello, this::getBye)
            .map(Supplier::get)
            .filter(Optional::isPresent)
            .map(Optional::get)
            .findFirst();

    assertEquals(getHello(), found);
}

 

14. or() 함수 (Java9 이후)

orElse() 함수나 orElseGet() 함수가 Optional 객체가 가지고 있는 값을 반환하는 함수임에 비해 or(Supplier<? extends Optional<? extends T>> supplier) 함수는 Optional을  반환하는 Supplier 를 인자로 받아, Optional 객체의 값이 존재하면 Optional 객체를 반환하고 Optional 객체의 값이 비어 있으면 인자로 받은 Supplier 를 통해 생성된 Optional 객체를 반환한다. (Supplier 는 필요한 순간에만 실행된다.)

@Test
void givenOptional_whenPresentAndEmpty_thenShouldTakeAValueFromItAndOr() {
    String expected = "properValue";
    Optional<String> value = Optional.of(expected);
    Optional<String> emptyValue = Optional.ofNullable(null);
    Optional<String> defaultValue = Optional.of("default");

    Optional<String> result = value.or(() -> defaultValue);
    assertEquals(expected, result.get());

    Optional<String> result2 = emptyValue.or(() -> defaultValue);
    assertEquals("default", result2.get());
}

 

15. ifPresentOrElse() 함수 (Java9 이후)

ifPresent(Consumer<? super T> action) 함수가 Optional 객체에 값이 있을때 action 이 실행되는 반면, ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) 함수는 Consumer 와 Runnable을 인자로 받아서 값이 있다면, Consumer를, 값이 없다면 Runnable을 실행한다.

@Test
void givenOptional_whenPresentAndNotPresent_thenShouldExecuteProperCallback() {
    Optional<String> value = Optional.of("properValue");
    Optional<String> emptyValue = Optional.ofNullable(null);

    value.ifPresentOrElse(
            v -> System.out.println("properValue = " + v),
            () -> System.out.println("properValue is absent")
    );
    // properValue = properValue


    emptyValue.ifPresentOrElse(
            v -> System.out.println("emptyValue = " + v),
            () -> System.out.println("emptyValue is absent")
    );
    // emptyValue is absent
}

 

16. stream() 함수 (Java9 이후)

stream() 함수는 Optional 객채에 값이 있는 경우 순서가 있는 Stream 을 반환한다(Stream 에는 당연히 값이 1개).  만약 Optional 객채에 값이 없으면 빈 Stream 을 반환한다. 간단히 Optional을 Stream 을 변경하는 함수라고 생각하면 된다.

@Test
void givenOptionalOfSome_whenToStream_thenShouldTreatItAsOneElementStream() {
    Optional<String> value = Optional.of("a");

    List<String> collect = value.stream().map(String::toUpperCase).collect(Collectors.toList());

    assertThat(collect).hasSameElementsAs(List.of("A"));
}

 

17. 잘못사용하는 경우

Optional 은 파라미터로 사용하지말고 빈 값을 반환할 가능성이 있는 함수의 리턴타입으로만 사용 하도록 하자.

 

18. Optional 과 직렬화

Optional을 필드로 사용하는것도 추천하지 않는다. 그리고 serializable 클래스에서 Optional을 사용하면 NotSerializableException이 발생한다.

 

 

반응형