본문 바로가기
학습/Java

Java 8에 등장한 Optional이란?

코동이 2022. 10. 29.

개요


Java 8에 Optional이 새로 등장했습니다. 어떤 문법인지 어떤 경우에 사용하는지 정리합니다.

 

 

 

코드 가독성을 높이고 널 포인트 예외를 방어해보자


널(Null)은 값이 없음을 가리키며 널 참조는 많은 문제의 원인이 됩니다. 따라서 Java 8에서는 이런 문제점을 일부 해결하기 위해서 java.util.Optional을 만들었습니다.

다음과 같은 코드가 있다고 가정해보겠습니다.

String version = computer.getSoundcard().getUSB().getVersion();


코드에는 문제가 없지만 많은 컴퓨터는 사운드 카드를 가지고 있지 않습니다. 그렇다면 getSoundcard()는 어떻게 될까요? 

대게의 경우 null을 리턴해서 사운드 카드가 없다는 것을 가리켰습니다. 불행히도, getUSB() 호출은 널을 리턴하기 때문에 런타임에 NullPointerException을 던지고 프로그램은 더 이상 실행되지 못하고 멈춥니다. 

컴퓨터 사이언스의 권위자 중에 한 명인 Tony Hoare는 다음과 같이 말했습니다.

"나는 수십억달러의 실수를 저질렀다. 그것은 1965년 null의 발명이다. 널 참조 사용 유혹을 뿌리 칠 수 없었다, 왜냐하면 사용하기 너무 쉬웠기 때문이다"

 

그렇다면 예상치 못한 널 포인트 예외를 예방하려면 어떻게 해야 할까요? 

 

 

널을 검사해서 예방하라


첫 번째 방법으로 널을 검사 해서 예방하는 것입니다.

String version = "UNKNOWN";
if(computer != null){
  Soundcard soundcard = computer.getSoundcard();
  if(soundcard != null){
    USB usb = soundcard.getUSB();
    if(usb != null){
      version = usb.getVersion();
    }
  }
}

 

하지만, 중첩 검사 때문에 코드가 굉장히 지저분합니다. NullPointerException을 예방하기 위해 너무 많은 부가 코드를 사용합니다. 가독성을 저해시킵니다

만약에 널 검사를 하지 않으면 문제가 발생합니다. 결론적으로 널 사용 자체가 잘못된 접근입니다.

 

 

Groovy 같은 언어들은 "?." 같은 연산자를 통해 널 참조가 예상되는 값들을 안전하게 조회합니다.

String version = computer?.getSoundcard()?.getUSB()?.getVersion();



만약 computer가 getSoundcard(0가 혹은 getUSB()가 null이면 version도 널이 할당됩니다. 널을 검사하기 위해 복잡한 내부 조건을 적을 필요가 없습니다.
또한 "?:"을 사용해 기본값을 할당할 수도 있습니다.

String version = computer?.getSoundcard()?.getUSB()?.getVersion() ?: "UNKNOWN";



하지만 이는 Groovy의 문법입니다. Java에서는 어떻게 해결할까요?

 

 

Optional 사용


Java 8에서 java.util.Optional<T>가 Haskell과 Scala의 개념에 영향을 받아 등장했습니다. 이 클래스는 옵셔널 값을 캡슐 화하는 클래스입니다. Optional은 값을 가지고 있거나 가지고 있지 않은 하나의 값 컨테이너입니다.

 

An optional sound card

 

 

이제 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(){ ... }
}

 

위의 정의에 따르면, 컴퓨터는 사운드 카드를 optional로 가지고 있기 때문에, 사운드 카드가 존재할수도 존재하지 않을 수도 있습니다. 또한 사운드 카드 입장에서 USB도 마찬가지입니다. Optional은 주어진 값이 없을 수도 있다는 것을 암시합니다. Optional은 값이 존재하는지 안 하는지에 따라서 다양한 경우를 다룰 수 있는 메서드들이 있습니다. 무엇보다, Optional이 널 참조보다 장점은 값이 없는 경우를 고민하도록 강요하는 것입니다. 별도의 처리를 통해서 예상치 못한 널 포인트 예외를 예방할 수 있습니다.

Optional의 의도는 모든 널 참조를 변경해야 한다는 것이 아닙니다. 대신에, 메서드의 명세서만 읽어도 optional 값을 기대할지 안할지 알 수 있어서 더 응집력 있는 API를 설계하는 것입니다.

 

 

Optional을 사용해서 코드 개선하기


이제 널 체크를 모두 제거하고 Optional을 사용해서 개선해보겠습니다. flatMap, map, orElse 등 람다와 스트림을 활용해서 간단하게 짤 수 있습니다.

 

String name = computer.flatMap(Computer::getSoundcard)
                          .flatMap(Soundcard::getUSB)
                          .map(USB::getVersion)
                          .orElse("UNKNOWN");

 

Optional을 사용하지 않았을 때와 사용했을 때를 비교해보겠습니다.

 

SoundCard soundcard = new Soundcard();
Optional<Soundcard> sc = Optional.of(soundcard);

 

Optional을 사용하지 않은 soundcard의 경우 널을 참조하면 NullPointerException을 던집니다.

Optional을 사용한 sc의 경우 널을 참조하더라도 Optional의 empty가 할당됩니다.

 

 

널 검사하고 다음 동작


SoundCard soundcard = ...;
if(soundcard != null){
  System.out.println(soundcard);
}

Optional<Soundcard> soundcard = ...;
soundcard.ifPresent(System.out::println);

 

Optional을 사용하지 않은 soundcard의 경우 null 검사를 해야 합니다.

Optional을 사용한 soundcard의 경우 ifPresent()를 사용하면 됩니다.

 

 

기본값 할당과 예외처리


Soundcard soundcard = 
  maybeSoundcard != null ? maybeSoundcard 
            : new Soundcard("basic_sound_card");
            
Soundcard soundcard = 
  maybeSoundCard.orElseThrow(IllegalStateException::new);

 

Optional을 사용하지 않은 soundcard의 경우 기본 값을 넣기 위해 삼항 연산자로 null 검사를 해야 합니다.

Optional을 사용한 soundcard의 경우 orElseThrow()를 사용해 null인 경우 예외를 던집니다.

 

 

filter를 활용하여 조건 처리


USB usb = ...;
if(usb != null && "3.0".equals(usb.getVersion())){
  System.out.println("ok");
}

Optional<USB> maybeUSB = ...;
maybeUSB.filter(usb -> "3.0".equals(usb.getVersion())
                    .ifPresent(() -> System.out.println("ok"));

 

Optional을 사용하지 않은 usb의 경우 조건을 처리하기 위해 if문에서 검사해야 합니다.

Optional을 사용한 maybeUSB의 경우 매개변수 predicate를 가지는 filter()로 true, false를 검사해 true인 값들을 반환합니다. 그 이후 ifPresent()로 출력합니다.  

 

 

map을 활용하여 추출과 변형


if(soundcard != null){
  USB usb = soundcard.getUSB();
  if(usb != null && "3.0".equals(usb.getVersion()){
    System.out.println("ok");
  }
}

maybeSoundcard.map(Soundcard::getUSB)
      .filter(usb -> "3.0".equals(usb.getVersion())
      .ifPresent(() -> System.out.println("ok"));

 

map(Soundcard::getUSB)를 활용해 maybeSoundcard 내에 모든 값을 getUSB()로 변환합니다.

 

 

flatMap을 활용하여 Optional 형변환


//컴파일 실패
String version = computer.map(Computer::getSoundcard)
                  .map(Soundcard::getUSB)
                  .map(USB::getVersion)
                  .orElse("UNKNOWN");

//컴파일 성공
String version = computer.flatMap(Computer::getSoundcard)
                   .flatMap(Soundcard::getUSB)
                   .map(USB::getVersion)
                   .orElse("UNKNOWN");

 

computer.map(Computer::getSoundcard)Optional<Optional<SoundCard>>가 되어 Optional 중첩이 됩니다. 하지만 computer.flatMap(Computer::getSoundcard)Opional<SoundCard>로 변환합니다. 왜냐하면 flatMap은 map처럼 기존에 각각의 스트림을 변환하는 대신에, 모든 스트림을 하나의 스트림으로 "평평하게" 새롭게 만들기 때문입니다.

 

따라서 각 클래스가 Optional 변수를 활용하므로 flatMap을 2번 사용하고, String을 반환형으로 가지는 USB의 getVersion()만 map을 사용합니다.

 

Using map versus flatMap with Optional

 

 

참고

https://www.oracle.com/technical-resources/articles/java/java8-optional.html

반응형

'학습 > Java' 카테고리의 다른 글

mutable vs immutable  (0) 2022.11.01
StringBuffer vs StringBuilder  (0) 2022.11.01
자바 8에 추가된 Date Time API  (0) 2022.10.28
Java 8에서 개선된 가비지 컬렉터는?  (0) 2022.10.19
jar war 차이점  (0) 2022.04.26