Optional 이란?
Optional 클래스는 Integer나 Double 클래스처럼 'T'타입의 객체를 포장해 주는 래퍼 클래스다. 모든 타입의 참조 변수를 지정할 수 있다. 또한, Optional을 사용함으로서 일부의 NullPointException의 발생을 피할 수 있다.
아래로 이어지는 설명은 모두 오라클의 기술 문서를 번역해서 작성하였다.
www.oracle.com/technical-resources/articles/java/java8-optional.html
Computer의 중첩된 구조의 객체가 있다.
이에 대해 다음과 같은 코드를 작성한다고 생각해 보자.
String version = computer.getSoundcard().getUSB().getVersion();
컴퓨터에 있는 USB의 버전 정보를 가져오는 코드이다. 어울리는 코드로 보이지만, 문제점이 있다. 사운드 카드가 없는 컴퓨터가 존재한다는 것이다. getSoundcard()에서 null을 return한다면, 컴퓨터에 USB가 있어도 버전 정보를 획득할 수 없고, NullPointException이 발생하게 된다.
이를 방지하기 위한, 간단한 Null check code는 아래와 같다.
String version = "UNKNOWN";
if(computer != null){
Soundcard soundcard = computer.getSoundcard();
if(soundcard != null){
USB usb = soundcard.getUSB();
if(usb != null){
version = usb.getVersion();
}
}
}
위 코드의 경우, 가독성이 떨어지고, 개발자가 null 값을 체크해야 한다는 사실을 잊었을 경우에 논리 오류가 일어날 가능성이 있다.
여러 언어에서 Null check를 위한 다양한 시도를 해왔다. 그루비의 경우는 "?." 연산자를 사용해서 잠재적인 null 참조를 안전하게 검사한다. 하스켈은 null이 될 수 있는 객체에 Maybe 타입을 사용한다. 스칼라는 Option[T] 생성자를 사용한다.
Java 8에서도 하스켈과 스칼라에서 아이디어를 받아 java.util.Optional<T> 패키지를 추가하면서 null 값 검사를 가능하게 하였다. Optional은 예상할 수 없는 값을 캡슐화한다. 아래 그림에서 단일 값이 있는 컨테이너와 빈 컨테이너를 확인할 수 있다.
위의 코드도 아래와 같이 수정할 수 있다.
public class Computer {
private Optional<Soundcard> soundcard;
public Optional<Soundcard> getSoundcard() { ... }
...
}
public class Soundcard {
private Optional<USB> usb;
public Optional<USB> getUSB() { ... }
}
public class USB{
public String getVersion(){ ... }
}
사운드 카드, USB가 Null 값이 될 수도 있는 객체라는 사실을 쉽게 알 수 있다.
Optional 사용하기
1. Optional 객체 생성
// empty Optional
Optional<Soundcard> sc = Optional.empty();
// null이 아닌 값을 가지는 Optional
SoundCard soundcard = new Soundcard();
Optional<Soundcard> sc = Optional.of(soundcard);
만약 soundcard 객체의 값이 null이면, sc에서 바로 NullPointException을 던진다. ofNullable 메소드를 사용하면 Optional에 null 값을 저장할 수도 있다.
Optional<Soundcard> sc = Optional.ofNullable(soundcard);
2. 값이 존재할 때 동작
ifPresent() 메소드를 사용해서 null 값을 체크할 수 있다.
Optional<Soundcard> soundcard = ...;
soundcard.ifPresent(System.out::println);
null을 체크해야 한다는 사실을 일일이 기억하는 대신, 작업을 type system에 강제로 수행시킬 수 있다.
값이 존재한다면 get() 메소드를 사용해서 객체에 저장된 값을 반환할 수 있다. 그러나, 값이 존재하지 않는다면 NoSuchElementException이 발생한다. get()과 ifPresent()를 함께 사용해서 예외를 방지할 수 있다.
if(soundcard.isPresent()){
System.out.println(soundcard.get());
}
그러나, 이는 Optional 사용 시 권장되는 방법은 아니다. 이보다는 아래의 방법들을 선호한다.
3. Default Value
전형적인 방법으로는 연산의 결과 값이 null이면 디폴트 값을 반환하는 방법이 있다. 삼항 연산자를 통해서 구현할 수 있다.
Soundcard soundcard =
maybeSoundcard != null ? maybeSoundcard
: new Soundcard("basic_sound_card");
Optional을 사용하면, Optional이 비어있을 때 디폴트 값을 제공하는 orElse() 메소드를 사용할 수 있다. 유사하게, orElseThrow()를 사용하면 Optional이 비어있을 때 디폴트 값을 제공하는 대신 예외를 던질 수 있다.
Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("default"));
Soundcard soundcard = maybeSoundCard.orElseThrow(IllegalStateException::new);
4. filter 메소드 사용
우리는 자주 메소드 호출을 통해서 객체의 속성을 검사한다. 예를 들어서, USB 포트가 특정 버전인지 아닌지 알고 싶을 수도 있다. 전형적인 안전한 방법으로는 아래와 같이, USB 객체가 null인지의 여부를 먼저 검사한 뒤 getVersion() 메소드를 호출할 수 있다.
USB usb = ...;
if(usb != null && "3.0".equals(usb.getVersion())){
System.out.println("ok");
}
위 코드는 Optional 클래스에 있는 filter 메소드를 사용해서 재작성할 수 있다.
Optional<USB> maybeUSB = ...;
maybeUSB.filter(usb -> "3.0".equals(usb.getVersion())
.ifPresent(() -> System.out.println("ok"));
filter 메소드는 stream 클래스에 있는 filter 메소드처럼 매개변수에 대한 조건문을 수행한다. Optional 객체에 있는 값이 null이 아니고, 조건문의 결과가 true라면, filter는 값을 반환한다. 그렇지 않다면 빈 Optional 메소드를 반환한다.
5. Extracting and Transforming Values Using the map
전형적인 패턴 중 객체에서 정보를 추출하는 경우가 있다. 예를 들어서, Soundcard 객체에서 USB 객체를 추출해서 USB의 버전 정보를 검사하고 싶을 수도 있다. 전형적인 코드는 다음과 같이 작성한다.
if(soundcard != null){
USB usb = soundcard.getUSB();
if(usb != null && "3.0".equals(usb.getVersion()){
System.out.println("ok");
}
}
이 패턴은 map 메소드를 사용해서 재작성 할 수 있다. Optional 클래스의 map 메소드는 stream 클래스의 map 매소드와도 유사하다. Optional 안에 있는 값은 인수의 형태로 함수를 거치면서 가공된다. map과 filter를 아래처럼 함께 사용할 수도 있다.
// map 사용
Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB);
// map + filter 사용
maybeSoundcard.map(Soundcard::getUSB)
.filter(usb -> "3.0".equals(usb.getVersion())
.ifPresent(() -> System.out.println("ok"));
6. Cascading Optional Object Using the flatMap Method
위에서 여러 패턴이 Optional을 통해서 리팩토링 되는 걸 볼 수 있었다. 이를 기반으로 맨 처음의 코드를 고치면 아래와 같다. map을 통해서 원하는 객체를 추출할 수 있다.
// 리팩톨이 전
String version = computer.getSoundcard().getUSB().getVersion();
// 리팩토링 후
String version = computer.map(Computer::getSoundcard)
.map(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
그러나 이 코드는 컴파일되지 않는다. computer 변수의 type은 Optional<Computer>이며, map 메소드를 호출할 수 있다. 그러나 getSoundcard()는 Optional<Soundcard> 타입을 반환한다. 그 결과 map이 최종적으로 반환하는 타입은 Optional<Optional<Soundcard>>가 된다. 따라서 getUSB() 메소드 또한 호출할 수 없게 된다. Optional 객체를 맨 바깥에 있는 Optional 객체가 거듭해서 감싸고 있는 모양이 되기 때문이다. 구조를 아래 그림을 통해 쉽게 이해할 수 있다.
이 문제는 stream에서도 지원하는 flatMap 메소드를 사용해서 해결할 수 있다. stream에서의 flatMap 메소드는 인수로 다른 stream을 반환하는 함수를 받는다. 이 함수는 각각의 요소에 적용되며, 결과적으로 중첩된 stream을 반환한다. 하지만, flatMap은 생성 된 스트림을 해당 스트림의 내용으로 대체하는 효과를 가지고 있다. 즉, 함수에서 생성된 모든 분리된 스트림은 하나의 단일 스트림으로 통합되거나 '평탄화'된다.
Optional 또한 flatMap 메소드를 가지고 있다. flatMap은 map이 그렇듯이 Optional 값에 함수를 적용하고, 결과가 2개로 중첩된 Optional 객체라면 하나로 평탄화한다. 아래 그림은 Optional의 map을 썼을 때와 flatMap을 썼을 때의 결과를 나타내며 비교하고 있다.
이를 적용해서 재작성한 코드는 아래와 같다.
String version = computer.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
첫 번째 flatMap은 Optional<Optional<Soundcard>> 대신 Optional<Soundcard>를 반환하고, 두 번째 flatMap도 같은 원리로 Optional<USB>를 반환한다. getVersion은 String을 반환하기 때문에 map을 사용한 것을 알 수 있다.
7. 결론
자바 8에서 추가된 java.util.Optional<T>에 대해서 알아볼 수 있었다. Optional의 의도는 코드에 있는 모든 단일 null 참조를 대체할 뿐만 아니라, 개발자가 메소드를 읽는 것 만으로도 null값을 예상할 수 있는, 더 나은 API를 설계하는 데 도움을 준다. 게다가, Optional은 존재하지 않는 값을 다루기 위해서 Optional을 unwrap하도록 하며, 결과적으로, 의도치 않은 null point exception이 발생하는 것을 방지할 수 있다.
- Reference
www.oracle.com/technical-resources/articles/java/java8-optional.html
www.tcpschool.com/java/java_stream_optional
'java' 카테고리의 다른 글
[210127] java Deque (0) | 2021.01.27 |
---|---|
[210124] java Character.digit() & Character.isDigit() (0) | 2021.01.24 |
[210118] java Stream (1) - stream 정의와 관련 메소드 (0) | 2021.01.18 |
[210117] JAVA 대소문자 관련 String 메소드 (0) | 2021.01.17 |
[210116] java comparable & comparator (0) | 2021.01.16 |