이 연재글은 Java 알아보기의 2번째 글입니다.

Java8에서 도입된 Optional에 대하여 살펴보겠습니다. Optional class의 목적은 null 참조 대신 Optional value로 표현되는 형식의 솔루션을 제공하는 것입니다. 즉 이전 버전까지의 Java의 고질적인 문제였던 NullPointerException을 해결하기 위한 방책으로 Optional이 등장했다고 보면 됩니다.

Optional Object 생성

Optional 객체를 생성하는 방법에는 여러 가지가 있습니다. 빈 Optional 객체를 만들려면 단순히 empty () 정적 메서드를 사용하면 됩니다.

Optional<String> empty = Optional.empty();
assertFalse(empty.isPresent());

Optional 객체 내에 값이 있는지 확인하기 위해서 isPresent() 메서드를 사용할 수 있습니다. null이 아닌 값으로 Optional을 만든 경우에만 값이 존재합니다.

정적 메서드 of()를 사용하여 Optional 객체를 만들 수도 있습니다. 그러나 of() 메서드에 전달된 인수는 null 일 수 없습니다. 그렇지 않으면 NullPointerException이 발생합니다. 따라서 반드시 값이 있어야 하는 객체인 경우에 해당 메서드를 사용하면 됩니다.

String name = "happydaddy";
Optional<String> optOf = Optional.of(name);
assertTrue(optOf.isPresent());

null 값이 필요한 경우 ofNullable() 메서드를 사용할 수 있습니다. 이렇게 하면 인수가 null값이어도 예외가 발생하지 않고 빈 Optional 객체가 반환됩니다.

String name = "happydaddy";
Optional<String> optOf = Optional.ofNullable(name);
assertTrue(optOf.isPresent());

isPresent() 및 isEmpty()로 값 유무 체크

메서드에서 반환된 Optional 객체가 있거나 직접 생성한 경우 isPresent () 메서드를 사용하여 값이 있는지 확인할 수 있습니다.

Optional<String> optOf = Optional.of("happydaddy");
assertTrue(optOf.isPresent());
Optional<String> optOfNullable = Optional.ofNullable(null);
assertFalse(optOfNullable.isPresent());

Java 11부터는 isPresent의 반대인 isEmpty 메서드를 사용할 수 있습니다.

Optional<String> optOfNullable = Optional.ofNullable(null);
assertTrue(optOfNullable.isEmpty());

ifPresent()를 사용한 조건부 동작처리

ifPresent() 메서드를 사용하면 null이 아닌 경우의 코드를 실행할 수 있습니다.

아래와 같은 코드는 내부 로직을 수행하기 전에 name변수가 null인지 확인합니다. 이 방법은 오류가 발생하기 쉬운데 변수를 선언한 다음에 변수에 대한 null 검사를 수행하는 것을 잊어버릴 수 있기 때문입니다. 따라서 아래와 같은 코드는 입력 데이터 체크에 대한 프로그래밍 적인 문제로 NullPointerException을 발생시킬 수 있습니다.

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

Optional은 좋은 프로그래밍 관행을 유지하면서 nullable 값을 명시 적으로 처리할 수 있습니다. Java8에서 위 코드를 리팩터링하는 방법을 살펴 보겠습니다. 일반적인 함수형 프로그래밍에서는 실제로 존재하는 객체에 대한 작업을 수행할 수 있습니다.

Optional<String> optOf = Optional.of("happydaddy");
optOf.ifPresent(name -> System.out.println(name.length()));

orElse()로 기본값 처리하기

orElse() 메서드는 Optional 인스턴스에 랩핑 된 값이 존재하면 그 값을 리턴하고 그렇지 않으면 기본값으로 메서드에 전달된 매개변수 값을 리턴합니다.

String name = null;
Optional<String> ofNullable = Optional.ofNullable(name);
name = ofNullable.orElse("happyMommy");
assertEquals("happyMommy", name);

orElseGet()으로 기본값 처리하기

orElseGet() 메서드는 orElse()와 유사합니다. 그러나 Optional 값이 존재하지 않으면 인수로 전달된 공급자 함수(Supplier)의 결과 값을 반환합니다.

public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}
String name = null;
Optional<String> ofNullable = Optional.ofNullable(name);
name = ofNullable.orElseGet(() -> "happyMommy");
assertEquals("happyMommy", name);

orElse()orElseGet() 차이점

Optional 또는 Java 8을 처음 사용하는 많은 프로그래머에게 orElse()와 orElseGet()의 차이점은 명확하지 않습니다. 실제로 이 두 가지 방법은 기능면에서 서로 겹친다는 인상을 줍니다. 그러나 잘 이해되지 않으면 코드 성능에 큰 영향을 줄 수 있는 두 가지 미묘하지만 매우 중요한 차이점이 있습니다.

@Test
void whenOrElseGetAndOrElse_1() {
        String name = null;
        String defaultText = Optional.ofNullable(name).orElseGet(this::getDefault);
        assertEquals("HappyMommy", defaultText);
        defaultText = Optional.ofNullable(name).orElse(getDefault());
        assertEquals("HappyMommy", defaultText);
}

@Test
    void whenOrElseGetAndOrElse_2() {
        String name = "HappyDaddy";
        String defaultText = Optional.ofNullable(name).orElseGet(this::getDefault);
        assertEquals("HappyDaddy", defaultText);
        defaultText = Optional.ofNullable(name).orElse(getDefault());
        assertEquals("HappyDaddy", defaultText);
}

public String getDefault() {
        System.out.println("getDefault");
        return "HappyMommy";
}

위의 내용을 실행해보면 orElseGet() 메서드는 Optional 객체가 비어있을 때만 실행됩니다. 그러나 orElse() 메서드는 Optional 객체가 비어있든 비어있지 않든 반드시 실행합니다. 즉 orElse()의 경우 Optional 객체의 존재 유무와 상관없이 사용하지 않든 사용하든 getDefault 객체를 생성함을 알 수 있습니다.

이 간단한 예에서는 JVM이 이러한 객체를 처리하는 방법을 알고 있으므로 기본 객체를 만드는 데 큰 비용이 들지 않습니다. 그러나 getDefault()와 같은 메서드가 웹 서비스 호출을 수행하거나 데이터베이스를 쿼리 해야 할 경우 비용이 매우 분명해지므로 주의해서 사용해야 합니다.

orElseThrow()로 예외 던지기

orElseThrow() 메서드는 orElse() 및 orElseGet()에 이어 빈 값을 처리하기 위한 새로운 접근법을 추가합니다. Optional에 랩핑 된 값이 없을 때 기본값을 리턴하는 대신 예외를 발생시킵니다.

String name = null;    Optional.ofNullable(name).orElseThrow(IllegalArgumentException::new);

get()을 사용하여 값 반환

Optional에 랩핑된 객체의 연산이 끝나고 최종적으로 값을 가져옵니다.

Optional<String> opt = Optional.of("HappyDaddy");
String name = opt.get();
assertEquals("HappyDaddy", name);

그러나 get()은 래핑 된 객체가 null이 아닌 경우에만 값을 반환할 수 있습니다. 그렇지 않으면 NoSuchElementException이 발생합니다.

String nullName = null;
Optional<String> opt = Optional.ofNullable(nullName);
String name = opt.get();

이것이 get() 메서드의 주요 결함입니다. 이상적으로, Optional은 예기치 않은 예외를 피하는 데 도움이 됩니다. 따라서 이 방법은 Optional의 목표에 맞지 않으며 향후 릴리스에서 더 이상 사용되지 않을 것입니다.

filter()를 사용한 조건부 리턴

filter 방식으로 래핑 된 값에 대해 인라인 테스트를 실행할 수 있습니다. 술어(predicate)를 인수로 사용하고 선택적 오브젝트를 리턴합니다. 즉 predicate 값이 참이면 해당 필터를 통과시키고 거짓이면 빈 optional을 반환합니다.

Integer year = 2019;
Optional<Integer> yearOptional = Optional.of(year);
boolean is2019 = yearOptional.filter(y -> y == 2019).isPresent();
assertTrue(is2019);
boolean is2020 = yearOptional.filter(y -> y == 2020).isPresent();
assertFalse(is2020);

filter는 일반적으로 사전 정의된 규칙에 따라 Optional에 랩핑 된 값을 거부하기 사용됩니다. 아래의 예는 특정 가격대의 단말기인지 확인하는 메서드입니다. Optional과 filter를 사용하면 불필요한 if 문을 제거하여 가독성을 높이고 Nullpointer 오류에 대한 위험성을 제거할 수 있습니다.

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

Optional과 filter를 이용하여 Refactoring 합니다. map 연산은 단순히 값을 다른 값으로 변환하는 데 사용됩니다.

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

map()으로 값 변환

이전 섹션에서는 filter를 기반으로 값을 거부하거나 수락하는 방법을 살펴보았습니다. 비슷한 구문을 사용하여 map() 메서드로 Optional 값을 변환할 수 있습니다.

List<String> companyNames = Arrays.asList("paypal", "oracle", "", "microsoft", "", "apple");
Optional<List<String>> listOptional = Optional.of(companyNames);
int size = listOptional.map(List::size).orElse(0);
assertEquals(6, size);

이 예제에서는 Optional 객체 안에 문자열 목록을 래핑하고 map 메서드를 사용하여 포함된 목록에 대한 작업을 수행합니다. 우리가 수행하는 작업은 목록의 크기를 검색하는 것입니다.

map 메서드는 Optional 안에 래핑 된 계산 결과를 반환합니다. 그런 다음 반환된 Optional에서 적절한 메서드를 호출하여 값을 검색해야 합니다.

filter 메서드는 단순히 값을 확인하고 boolean을 리턴합니다. 반면에 map 메서드는 기존 값을 가져와서 이 값을 사용하여 계산을 수행하고 Optional 객체에 래핑 된 계산 결과를 반환합니다.

String name = "HappyDaddy";
Optional<String> nameOptional = Optional.of(name);
int len = nameOptional.map(String::length).orElse(0);
assertEquals(10, len);

더 강력한 기능을 수행하기 위해 맵과 필터링을 함께 연결할 수 있습니다.

사용자가 입력 한 비밀번호의 정확성을 확인하려 한다고 가정해 봅시다. 맵 변환을 사용하여 비밀번호를 정리하고 필터를 사용하여 비밀번호가 올바른지 확인할 수 있습니다.

String password = " password ";
Optional<String> passOpt = Optional.of(password);
boolean correctPassword = passOpt.filter(pass -> pass.equals("password")).isPresent();
assertFalse(correctPassword);
correctPassword = passOpt.map(String::trim).filter(pass -> pass.equals("password")).isPresent();
assertTrue(correctPassword);

flatMap()으로 값 변환

map() 메서드와 마찬가지로 값을 변환하는 대안으로 flatMap() 메서드도 있습니다. 차이점은 map은 래핑 되지 않은 경우에만 값을 변환하는 반면 flatMap은 래핑 된 값을 가져와서 변환하기 전에 래핑 해제한다는 것입니다.

차이점을 보다 명확하게 파악하기 위해 이름, 나이 및 비밀번호와 같은 사람의 세부 정보를 취하는 Person 객체를 살펴보겠습니다.

public class Person {
    private String name;
    private int age;
    private String password;

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

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

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

    public Optional<String> getPassword() {
        return Optional.ofNullable(password);
    }
}

우리는 일반적으로 위와 같은 객체를 만들고 내부 메서드들을 Optional 객체로 포장합니다. 그런데 Person 객체를 사용하기 위해 Optional로 감싸면 중첩된 Optional 인스턴스가 만들어집니다.

Person person = new Person("john", 26);
Optional<Person> personOptional = Optional.of(person);

아래의 예제에서는 Person객체에서 name을 조회하는 방법을 map, flatmap으로 구현한 것입니다. map을 이용하는 경우 중첩된 Optional로 인해 name을 조회하는데 복잡한 반면 flatmap을 이용하면 변환하기 전에 래핑을 해제하므로 map으로 연산하는 것보다 간단하게 처리할 수 있습니다.

Person person = new Person("john", 26);
Optional<Person> personOptional = Optional.of(person);
Optional<Optional<String>> nameOptionalWrapper = personOptional.map(Person::getName);
Optional<String> nameOptional = nameOptionalWrapper.orElseThrow(IllegalArgumentException::new);
String name1 = nameOptional.orElse("");
assertEquals("john", name1);

String name = personOptional.flatMap(Person::getName).orElse("");
assertEquals("john", name);
연재글 이동[이전글] Java RoadMap 그리고 JDK9 ~ 11 변경사항(update jdk12,13)
[다음글] Java Stream 실습 코드 정리