Java 8 옵셔널 Optional

🗓 ⏱ 소요시간 4 분

개발할 때 개발자를 괴롭히는 예외 중 하나는 NullPointerException 입니다. NullPointerException 을 피하고 null 체크하는 로직을 줄이기 위해 빈 값일 때 null 대신 초기값을 사용하길 권장하곤 합니다.

1
2
List<String> names = getNames();
names.sort(); // names 가 null 이라면 NullPointerException 이 일어난다.

자바 8에서는 Optional<T> 클래스를 이용해서 NullPointerException 을 방지할 수 있습니다. Optional<T> 클래스는 한 마디로 null 이 올 수 있는 값을 감싸는 래퍼 클래스로 참조하더라도 null 이 일어나지 않도록 해주는 클래스입니다.

1
2
3
4
5
6
7
public final class Optional<T> {

// If non-null, the value; if null, indicates no value is present
private final T value;

...
}

위 코드의 value 에 값을 저장하기 때문에 값이 null 이더라도 바로 참조 시 NPE 가 나지 않고, 클래스이기 때문에 각종 메소드를 제공해줍니다.

생성하기

먼저 다음과 같이 빈 객체를 생성할 수 있습니다.

1
2
3
4
Optional<String> optional = Optional.empty();
System.out.println(optional); // Optional.empty

System.out.println(optional.isPresent()); // false

혹은 null 이 올 수 있는 값을 Optional<T> 로 감싸서 생성할 수 있습니다. orElse 또는 orElseGet 메소드를 이용해서 값이 없는 경우라도 안전하게 값을 가져올 수 있습니다.

다음 예제에서는 getString 메소드가 리턴하는 값이 null 일 수도 있습니다. 하지만 Optional<T> 로 감싸면 됩니다.

1
2
3
4
// Optional 안에는 값이 있을 수도 있고 빈 객체일 수도 있다.
Optional<String> optional = Optional.ofNullable(getString());

String result = optional.orElse("other"); // 값이 없다면 "other" 를 리턴

사용하기

사용법은 간단합니다. 먼저 자바 8 이전에는 다음과 같이 null 체크가 필요했습니다.

1
2
3
// 자바 8 이전
List<String> list = getList();
List<String> listOpt = list != null ? list : new ArrayList<>();

Optional<T> 과 Lambda 를 이용하면 좀 더 간단하게 표현할 수 있습니다.

1
List<String> listOpt = Optional.ofNullable(getList()).orElseGet(() -> new ArrayList<>());

다음 코드는 null 체크 때문에 지저분해진 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
User user = getUser();
if (user != null) {
Address address = user.getAddress();
if (address != null) {
String street = address.getStreet();
if (street != null) {
return street;
}
}
}
return "주소 없음";

map 메소드는 해당 값이 null 이 아니면 mapper 를 이용해 계산한 값을 저장하는 Optional 객체를 리턴합니다. 만약 값이 null 이라면 빈 Optional 객체를 리턴합니다.

1
2
public<U> Optional<U> map(Function<? super T, ? extends U> mapper)
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper)

map 메소드를 이용해서 간단하게 표현해보겠습니다.

1
2
3
4
5
6
7
8
9
Optional<User> user = Optional.ofNullable(getUser());
Optional<Address> address = user.map(User::getAddress);
Optional<String> street = address.map(Address::getStreet);
String result = street.orElse("주소 없음");

// 다음과 같이 축약해서 쓸 수 있다.
user.map(User::getAddress)
.map(Address::getStreet)
.orElse("주소 없음");

만약 위 예제에서 getAddressgetStreetOptional<T> 객체를 리턴한다면 flatMap 메소드를 사용할 수도 있습니다.

1
2
3
4
5
Optional<User> user = Optional.ofNullable(getUser());
String result = user
.flatMap(User::getAddress)
.flatMap(Address::getStreet)
.orElse("주소 없음");

다음 예제는 NullPointerException 을 핸들링하는 예제입니다.

1
2
3
4
5
6
7
String value = null;
String result = "";
try {
result = value.toUpperCase();
} catch (NullPointerException e) {
throw new CustomExcpetion();
}

위와 같은 코드는 Optional<T>을 이용하면 다음과 같이 표현할 수 있습니다.

1
2
3
String value = null;
Optional<String> valueOpt = Optional.ofNullable(value);
String result = valueOpt.orElseThrow(CustomExcpetion::new).toUpperCase();

논란거리

옵셔널은 논란이 있는 클래스입니다. NPE 를 더 쉽게 다룰 수 있고, 다른 포스트에서 살펴볼 스트림과 함께 사용하면 간결하게 코딩할 수 있는 장점이 있습니다. 하지만 처음부터 Option 을 지원한 Scala 와 다르게 이미 많은 코드가 옵셔널 없이 null 체크를 하고 있기 때문에 개발자를 혼란스럽게 하고 기존 소스와 다른 코드 스타일을 만들 수 있어 부정적으로 보는 개발자들도 많습니다.

모든 도구가 그렇듯 장점과 단점이 있고, 쓰기 나름일 겁니다. 기존 프로젝트는 기존 코딩 스타일을 유지하되, 새롭게 시작하는 프로젝트에서는 적용해보는 것도 좋을 것 같습니다.

참고