본문 바로가기
Java

[Java] Optional

by Kloong 2022. 10. 19.

Optional

0. 시작하기에 앞서 - 출처

이 글은 책 "자바의 정석" 과 다음 3개의 링크를 참고해서 요약해 쓰는 글이다. 더 자세한 정보를 얻고 싶다면 "자바의 정석"과 다음 링크를 읽어보는 것을 추천한다(내 글보다 훨씬 좋다!).

자바8 Optional 1부: 빠져나올 수 없는 null 처리의 늪
자바8 Optional 2부: null을 대하는 새로운 방법
자바8 Optional 3부: Optional을 Optional답게

또 람다식과 Stream 에 대한 기본 지식이 없으면 이 글을 읽는 의미가 반감된다. 람다식과 스트림에 대해 잘 모른다면 아래 링크를 읽어본 뒤 이 글을 읽기 바란다.

람다식(Lambda Expression)
Stream

1. java.lang.Optional<T>

Optional<T> 클래스는 "null 일 수도 있는 객체" 를 감싸는 일종의 래퍼 클래스이다.

Java의 고질적인 문제 중 하나는 바로 null 처리이다. Java를 잘 몰라도 NullPointerException 의 악명은 익히 들어봤을 것이다. NPE의 가장 큰 문제는 컴파일 타임에 잡아내기 쉽지 않아서, 런타임에 예상치 못한 곳에서 갑자기 튀어나온다는 것이다.

다음 코드를 살펴보자.

/* 출처: https://www.daleseo.com/java8-optional-before */
@Getter
public class Order {
    private Long id;
    private Member member;
}

@Getter
public class Member {
    private Long id;
    private Address address;
}

@Getter
public class Address {
    private String street;
    private String city;
}

주어진 데이터 모델에서 "주문"한 "회원"의 "도시" 를 알아내는 메소드인 getCityOfMemberOfOrder() 가 필요하다고 해보자.

/* 출처: https://www.daleseo.com/java8-optional-before */
public String getCityOfMemberOfOrder(Order order) {
    return order.getMember().getAddress().getCity();
}

Java를 써 본 사람이라면 위 코드가 얼마나 위험한 코드인지 쉽게 알 수 있을 것이다. Order 객체, getMember() 로 얻은 Member 객체, getAddress() 로 얻은 Address 객체, 이렇게 세 객체 중 하나라도 null 이라면 바로 NullPointerException 이 발생한다.

문제는 단순히 Order 객체를 넘겨받은 입장인 getCityOfMemberOfOrder() 메소드는 위 객체들이 null 인지 아닌지 알 방법이 없다. 따라서 NullPointerException 을 예방하려면 다음과 같이 null 체크를 해야한다.

/* 출처: https://www.daleseo.com/java8-optional-before */
public String getCityOfMemberOfOrder(Order order) {
    if (order != null) {
        Member member = order.getMember();
        if (member != null) {
            Address address = member.getAddress();
            if (address != null) {
                return address.getCity();
            }
        }
    }
    return null;
}

그 놈의 null 이 도대체 뭐길래, 아주 단순한 로직이 null 체크 코드 때문에 이렇게 끔찍하게 변해버렸다. if 문이 너무 많아져서 코드를 읽기가 어렵고, 핵심 로직을 한 눈에 파악하기도 힘들다.

if 문을 모두 통과하지 못하면 위 메소드는 또 다시 null 을 반환해 버린다. 그러면 이 메소드를 호출하는 쪽 역시 같은 방식으로 null 체크를 해야만 한다. 만약 null 체크를 잊어버린다면? 런타임에서 NPE 가 발생할 확률이 높아지는 것이다.

따라서 완벽한 NPE 예방을 위해서는 내가 호출하려는 메소드가 null 을 반환하는지의 여부도 하나 하나 확인을 해야만 한다. 정말 끔찍한 일이 아닐 수 없다.

하지만 Java 8 부터 Optional 이 등장하게 되면서 고질적인 null 처리 문제가 어느정도 해소되었다. Optional 사용법을 알아보면서 Optional 이 이 문제를 어떻게 해결할 수 있는지 확인해보자.

2. Optional 기본 사용법 - Optional이 제공하는 메소드

1) Optional 객체 생성하기

i) Optional.empty()

비어있는 Optional 객체를 반환한다. 이 객체는 Optional 내부적으로 미리 생성해놓은 싱글톤 인스턴스이다.

public final class Optional<T> {

    private static final Optional<?> EMPTY =
                                    new Optional<>(null);

    private final T value;

    private Optional(T value) {
        this.value = value;
    }

    public static<T> Optional<T> empty() {
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }

    //생략...
}
Optional<Member> emptyMember = Optional.empty();

ii) Optional.of()

null 이 아닌 객체를 담고 있는 Optional 객체를 생성한다. 메소드의 인자로 null 이 넘어올 경우 NPE를 던지기 때문에 주의해서 사용해야 한다.

public static <T> Optional<T> of(T value) {
    return new Optional<>(Objects.requireNonNull(value));
}
//Objects.requireNonNull(value) 에서 value == null 이면 NPE 발생
Optional<Member> member = Optional.of(aMember);

iii) Optional.ofNullable()

null 인지 아닌지 확신할 수 없는 객체를 담고 있는 Optional 객체를 생성한다. null 이 넘어올 경우 Optional.empty() 와 동일하게 비어 있는 싱글톤 Optional 객체를 반환한다.

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? (Optional<T>) EMPTY
                         : new Optional<>(value);
Map<Long, Member> memberMap = new HashMap<>();
Optional<Member> member = 
                    Optional.ofNullable(memberMap.get(10L));

2) Optional 객체에서 값 꺼내오기

맨 처음에 언급했듯이 Optional<T> 클래스는 "null 일 수도 있는 객체" 를 감싸는 일종의 래퍼 클래스이다.

따라서 메소드의 반환형이 Optional<T> 라면 메소드를 호출하는 쪽에서는 "이 메소드가 반환하는 객체가 null 일 수도 있다는" 사실을 바로 알 수 있게 되고, null 체크를 한 뒤 객체에 접근을 시도할 것이다.

그렇다면 Optional 객체가 담고 있는 객체를 어떻게 꺼내올 수 있을까?

Optionalnull 일 수도 있는 객체를 담고 있기 때문에, 담고 있는 객체가 null 일 경우를 대비해서 해당 객체에 접근하는 다양한 방식의 메소드를 제공한다.

i) get()

객체를 반환한다. 객체가 null 이면 NoSuchElementException 을 던진다. 따라서 객체가 null 이 아니라고 확신할 때만 이 메소드를 사용해야 한다.

public T get() {
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}

ii) orElse()

객체가 null 이 아니면 반환하고, null 이면 인자로 받은 객체를 반환한다.

public T orElse(T other) {
    return value != null ? value : other;
}

iii) orElseGet()

orElse() 의 람다식 버전. 객체가 null 일 경우 인자로 넘겨받은 람다식의 실행 결과값을 반환한다.

public T orElseGet(Supplier<? extends T> supplier) {
    return value != null ? value : supplier.get();
}

iV) orElseThrow()

객체가 null 인 경우 인자로 넘겨받은 Exception 객체를 반환하는 람다식을 실행해서 예외를 던진다.

public <X extends Throwable> T orElseThrow(
            Supplier<? extends X> exceptionSupplier) throws X {
    if (value != null) {
        return value;
    } else {
        throw exceptionSupplier.get();
    }
}

3. Optional 의 잘못된 사용

이제 Optional 이 제공하는 다양한 메소드들도 알았으니 한 번 앞에서 예시로 들었던 getCityOfMemberOfOrder() 메소드의 null 체크 문제에 Optional 을 적용해보자.

Optionalget() 메소드를 쓰려면 Optional 객체가 담고 있는 객체가 null 인지 확인을 해야하는 데, 이 때 isPresent() 메소드를 사용할 수 있다.

public boolean isPresent() {
    return value != null;
}
if (optionalMember.isPresent()) {
    return optionalMember.get();
}

이제 getCityOfMemberOfOrder()Optional 을 적용해보자.

/* 출처: https://www.daleseo.com/java8-optional-after */
public Optional<String> getCityOfMemberFromOrder(Order order) {
    Optional<Order> optionalOrder = Optional.ofNullable(order);
    if (optionalOrder.isPresent()) {
        Optional<Member> optionalMember =
                        Optional.ofNullable(maybeOrder.get());
        if (optionalMember.isPresent()) {
            Optional<Address> optionalAddress =
                        Optional.ofNullable(maybeMember.get());
            if (optionalAddress.isPresent()) {
                Address address = optionalAddress.get();
                Optinal<String> optionalCity =
                        Optional.ofNullable(address.getCity());
                if (optionalCity.isPresent()) {
                    return optionalCity.get();
                }
            }
        }
    }
    return Optional.empty();
}

와! 이런 끔찍한 코드가 탄생해버렸다! 분명 Optional 을 사용하면 null 체크 문제를 해결할 수 있다고 했는데 이게 무슨 일일까?

바로 Optional 을 제대로 사용하지 못한 결과다. Optional 을 제대로 사용하기 위해서는 함수형 프로그래밍의 관점을 가지고, Optional 을 마치 한 개의 원소를 가진 Stream 처럼 바라봐야 한다.

4. Optional 제대로 사용하기 - 함수형 프로그래밍

Optional 은 "이 값은 null 일 수도 있다"는 사실을 알려주는 래퍼 클래스의 역할만 하는 것이 아니다. Optional 은 개발자에게 함수형 프로그래밍을 가능하게 해준다.

Optional 은 인자로 람다식을 받는 map(), filter() 등의 메소드들을 지원한다. 어디서 많이 보지 않았나?

맞다. 바로 Stream 에서 본 그 친구들과 동일하다.

위에서 언급했듯이 Optional 을 제대로 사용하려면 한 개의 원소를 가진 Stream 취급을 해야 한다.

null 체크는 Optional 내부에서 처리하도록 맡기고 (마치 Stream 에서 반복은 내부에서 알아서 하도록 맡긴 것 처럼), 개발자는 Optional 객체가 갖고 있는 객체로 "무엇을 해야 할 지" 에만 집중해서 코드를 작성하면 된다.

예시 코드를 보면 바로 이해가 갈 것이다. Optional 을 "제대로" 사용해서 getCityOfMemberOfOrder() 메소드의 문제를 해결해보자.

/* 출처: https://www.daleseo.com/java8-optional-effective */
public Optional<String> getCityOfMemberOfOrder(Order order) {
    return Optional.ofNullable(order)
                    .map(Order::getMember)
                    .map(Member::getAddress)
                    .map(Address::getCity)
                    .orElse(Optional.empty());
}

코드가 엄청나게 간결해졌다. 그리고 핵심 로직을 한 눈에 파악할 수 있게 되었다.

Stream 처럼 이런 코드가 가능한 이유는 Optionalmap() 을 살펴보면 알 수 있다.

public <U> Optional<U> map(
                    Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent()) {
        return empty();
    } else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

반환형이 Optional 이기 때문에 map() 을 연속적으로 적용할 수 있다.

그리고 가장 마지막에 orElse(), orElseGet(), orElseThrow() 등의 메소드를 사용해서 값이 존재 하면 그 값을 반환하고 없으면 다른 동작을 하면 된다.

이렇게 함수형 프로그래밍의 관점으로 Optional 을 사용해야 Optional 을 제대로 사용할 수 있다. 그래서 Lambda Expression, Stream 에 이어서 마지막으로 이 글을 작성하는 것이다.

더 자세한 내용과 예제 코드는 아래 링크를 확인하길 바란다. 사실상 저 링크 첨부한 게 이 글의 본체이기 때문에 왠만하면 저 글을 읽는 것이 좋다 ㅎㅎ....

출처 & 참고 - 링크 3개 꼭 읽어보시길!

자바8 Optional 1부: 빠져나올 수 없는 null 처리의 늪
자바8 Optional 2부: null을 대하는 새로운 방법
자바8 Optional 3부: Optional을 Optional답게

남궁 성, 『자바의 정석』, 도우출판(2016)
자바의 정석

'Java' 카테고리의 다른 글

[Java] Stream  (0) 2022.10.13
[Java] 람다식(Lambda Expression)  (0) 2022.10.12
Exception 관련 클래스의 계층 구조 (Unchecked Exception/Checked Exception)  (0) 2022.03.31
변수의 초기화  (0) 2022.01.20
[소소한 정보] this와 this()  (0) 2022.01.20

댓글