[번역] Java - Optional (예제 코드 포함)

hanblueblue·2020년 5월 8일
0

Document 번역

목록 보기
9/22
post-thumbnail

ref.
Guide To Java 8 Optional
Oracle-Java Optional
Tired of Null Pointer Exceptions? Consider Using Java SE 8's "Optional"!
예제로 사용한 모든 코드는 github에서 확인 할 수 있습니다.

Java Docs

java.util
Class Optional<T>
java.lang.Object
java.util.Optional<T>


public final class Optional<T>
extends Object

null인 값도 포함할 수 있는 컨테이너 오브젝트. 값이 있으면(즉, null이 아니면) isPresent()true를 반환하고 get()은 값을 반환한다. orElse() (값이 없으면 디폴트 값을 반환함) 및 ifPresent() (값이 있으면 코드 블록을 실행함)과 같이 포함된 값의 존재 여부에 의존하는 추가 메소드가 제공된다.

이는 값 기반의 클래스로서(This is a value-based class;), Optional 인스턴스에서 식별에 민감한(identity-sensitive) 연산(참조 등식 (==), 식별 해시 코드, 동기화 등을 포함)을 사용하면 예기치 않은 결과가 발생할 수 있으므로 피해야한다.

Since:
1.8

Tired of Null Pointer Exceptions? Consider Using Java SE 8's "Optional"!

코드의 가독성을 높이고 NullPoiterException으로부터 보호하게 만들 수 있다.

누군가는 npe를 처리하기 전까지는 진짜 자바 프로그래머가 아니라고 말했다. null 참조는 종종 값이 존재하지 않음을 나타내는데 사용되기 때문에 농담으로써도 꽤 많은 문제의 원인이 된다. Java SE 8에는 이러한 문제 중 일부를 완화 할 수 있는 java.util.Optionaal이라는 새로운 클래스가 도입되었다.

null이 가져오는 위험을 보기 위한 예제를 들어보겠다. 아래와 같은 컴퓨터의 중첩된 객체 구조를 생각해보자.
다음 코드에서 무엇이 문제라고 생각하는가?
String version = computer.getSoundcard().getUSB().getVersion();
이 코드는 꽤 합리적으로 보인다. 그러나 많은 컴퓨터(e.g. Raspberry Pi)에는 실제로 사운드 카드가 제공되지 않는다. getSoundcard()의 결과가 무엇일까?

일반적인 (나쁜) 방법은 사운드 카드가 없음을 나타내기 위해서 null 참조를 반환하는 것이다. 그리고 이것은 getUSB()에 대한 호출을 시도하는 것과 동시에 런타임시 NullPointerException을 발생시키고 프로그램은 실행을 중단한다.

_Tony Hoare_는 다음과 같이 말하였다.
"저는 이것을 10억 달러의 실수라고 부릅니다. 이는 1965년에 null 참조가 가져온 발명품입니다. 나는 단순히 구현이 쉽기때문에 null 참조를 넣는 유혹을 이길 수가 없었습니다."

의도하지 않은 npe를 방지하기 위해 우리는 무엇을 할 수 있을까? 다음과 같이 방어적인 로직을 추가할 수 있다.

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

그러나 중첩된 검사로 인해 위의 코드가 더러워짐을 알 수 있다. 불행히도 npe를 발생시키지 않기 위한 많은 상용구 코드가 필요해진다. 또한 이러한 검사가 비즈니스 로직 내에서 사용되는 것은 굉장히 성가신 일이다. 실제로, 저런 것들은 프로그램의 전반적인 가독성을 감소시키고 있다.

또한 이는 오류가 발생하기 쉬운 프로세스이다. 어떤 하나의 속성이 null인지를 확인하는 것을 잊어버린 경우는 어떻게 해야하는가? 우리는 null을 사용해 값이 없음을 나타내는 것(value == null과 같은 것들..)을 잘못된 접근법이라고 주장한다. 우리가 필요로 하는 것은 값의 유무를 모델링하는 더 좋은 방법이다.

컨텍스트를 제공하기 위해 다른 방식 언어가 사용하는 프로그래밍 기법을 간단히 살펴보자.

What Alternatives to Null Are There?

Groovy와 같은 언어어는 ?로 표시되는 잠재적인 null 참조를 안전하게 탐색하도록 하는 연산자가 존재한다. (Note. 이는 곧 C#에도 포함될 예정이며 Java SE 7 용으로도 제안되었지만 해당 릴리즈에는 포함되지 않았다.) 이는 다음과 같이 작동한다.

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

이 경우, computer가 null이거나 getSoundcard()가 null을 반환하거나 getUSB()가 null을 반환하면 변수 version에는 null이 할당된다. null을 확인하기 위에 (이전 섹션에서 보여준 것과 같은) 복잡한 중첩 조건을 사용할 필요가 없다.

또한 Groovy는 Elvis 연산자라 불리는 ?:를 포함하는데 기본값이 필요한 간단한 경우에 사용될 수 있다. 다음에서 ? 연산자로 인해 null이 리턴되면 대신하여 기본값인 **"UNKNOWN"**이 리턴된다. null이 리턴되지 않는다면 최종적으로 사용가능한 getVersion()의 반환값이 할당 될 것이다.

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

HaskellScala와 같은 다른 기능적 언어에서는 다른 관점을 가지고 있다. Haskell에는 Maybe라는 타입이 초함되는데, 이는 기본적으로 선택적인 값을 캡슐화한다. Maybe 타입의 값은 주어진 유형의 값을 포함할 수도, 포함하지 않을 수도 있다. 따라서 null을 참조한다는 개념은 없다. ScalaOption[T]라 불리는 유사한 구성을 통해 T 타입의 값에 대한 존재 유무를 캡슐화한다. 그런 다음 "null check"라는 개념을 강제하는 Option 타입에서 사용가능한 연산을 통해 값이 존재하는지 아닌지에 대한 판단을 명시적으로 수행해야한다. 이는 type system에 의해 강제적으로 시행되므로 더 이상 "이를 수행하는 것을 잊어버릴" 필요가 없다.

그렇다면, Java SE 8은 어떻게 수행될까? 🤔

Optional in a Nutshell

Java SE 8에는 HaskellScala의 아이디어에서 영감을 얻은 java.util.Optional<T>라는 새로운 클래스가 도입되었다. 아래의 코드와 이미지에 나와있는 것처럼 옵션 값을 캡슐화 하는 클래스이다. Optional은 값을 포함하거나 포함하지 않는("비어있다"라고 불린다.) 단일 값 컨테이너로 볼 수 있다.

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

위의 코드는 컴퓨터는 Soundcard를 가질 수도 있고, 그렇지 않을 수도 있음을 보여준다. (즉, 사운드 카드는 optional이다.) 또한 사운드 카드에도 선택적으로 USB가 있을 수 있다. 이 새 모델은 이제 주어진 값이 누락 가능한지 여부를 명확하게 반영할 수 있다. (누락됐다면 npe가 발생할 것이다.) Guava와 같은 라이브러리에서도 비슷한 아이디어를 얻을 수 있다.

그러나 Optional<Soundcard> 오브젝트를 이용해 실제로 하고자 하는 것은 무엇일까? 결국 원하는 것은 USB 포트 번호를 얻는 것이다. (getUSB()) 요컨태, Optional 클래스에는 값이 있거나 없는 경우에 명시적으로 이를 처리하게 하는 메서드가 포함되어있다. null 참조와 비교했을 때 Optional클래스는 값이 없는 경우에 대해 어떻게 처리할 것인지 생각할 거리를 준단 것이 이점이라고 할 수 있다. 즉, 결과적으로 의도하지 않은 npe을 방지 할 수 있다.

Optional 클래스의 의도가 모든 단일 null 참조를 대체하는 것이 아니란 것은 매우 중요하다. 대신, 단지 메소드의 signature를 읽음으로써 사용자가 해당 값이 선택적임을 기대하고 있는지 파악할 수 있는 API로 디자인되었다. 이는 그다음에 값이 없는 경우에 처리를 위해 Optional을 풀도록 강제한다. (get() 등)

Patterns for Adopting Optional

몇 가지 코드를 보자. 먼저 Optional을 사용하여 일반적인 null-check 패턴을 작성하는 방법을 살펴 볼 것이다. 이 글이 끝날 때쯤, 당신은 아래에 쓰인 것처럼 Optionl을 사용하여 몇 가지 중첨된 널 검사(value != null이 반복되는..)를 수행하는 코드를 다시 작성하는 방법을 이해할 것이다.

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

❗️NOTE
Java SE 8 람다 및 메소드 참조 구문 (Java 8 : Lambdas)과 stream pipelining 개념 (Java SE 8 스트림으로 데이터 처리를 참고하라

💻 실습 📌

이제부터 해당 도큐먼트에서 알려주는대로 코드를 작성하며 익혀볼 것이다. 우선, Computer, soundcard, USB 클래스를 생성하자.

/** Computer */
@Getter
public class Computer {

    @Setter
    private Optional<SoundCard> soundCard;

    @Builder
    public Computer (SoundCard soundCard){
        this.soundCard = Optional.ofNullable(soundCard);
    }
    
    public Optional<Computer> toOptional () {
        return Optional.ofNullable(this);
    }
}

/** SoundCard */
@Getter
@NoArgsConstructor
public class SoundCard {

    @Setter
    private Optional<USB> usb;

    @Builder
    public SoundCard (USB usb){
        this.usb = Optional.ofNullable(usb);
    }
}

/** USB */
@Getter
@NoArgsConstructor
public class USB {
    private String serialNumber;
    
    public USB(String serialNumber){
        this.serialNumber = serialNumber;
    }
}

그리고 ComputerTest라는 Test 클래스도 만들어 주도록 하자. 여기서 이 글에서 관련된 모든 실습을 수행할 것이다. 테스트를 위한 의존성은 다음과 같다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>groupId</groupId>
    <artifactId>javaProject</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
            <scope>provided</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>3.1.0</version>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <version>3.1.0</version>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.4.2</version>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.hamcrest/hamcrest-all -->
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-all</artifactId>
            <version>1.3</version>
            <scope>test</scope>
        </dependency>
      
        <dependency>
            <groupId>com.github.npathai</groupId>
            <artifactId>hamcrest-optional</artifactId>
            <version>2.0.0</version>
            <scope>test</scope>
        </dependency>
      

        <!-- https://mvnrepository.com/artifact/org.assertj/assertj-core -->
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.15.0</version>
            <scope>test</scope>
        </dependency>


    </dependencies>

</project>

hamcrest-optional 이라는 종속성은 이번에 처음 추가해보았는데 Optional 클래스에 대한 Matcher의 일종이다.

Creating Optional objects

가장 먼저, 비어있는 Optional을 만들어 볼 것이다.

    @Test
    @DisplayName("빈 옵셔널 값을 만든다.")
    public void emptyOptional(){
        Optional<SoundCard> sc = Optional.empty();
        assertThat(sc, OptionalMatchers.isEmpty());
    }

다음은 오브젝트를 만든 뒤 Optional로 캡슐화하는 코드이다.

    @Test
    @DisplayName("값이 존재하는 옵셔널을 만든다.")
    public void scOptional1(){
        SoundCard soundCard = new SoundCard();
        Optional<SoundCard> sc = Optional.of(soundCard);
        assertThat(sc, OptionalMatchers.isPresent());
    }

도큐먼트에 보면 만약 soundCard가 null인 경우에는 npe가 발생한다고 되어있어서 시도해보았다.
soundCard가 Optional.of()에 파라미터로 들어가는 행에서 npe가 발생하는 것을 확인할 수 있다.

대신 다음과 같이 ofNullable() 메소드를 사용하면 객체가 null인 경우에도 Optional이 만들어진다. 또한 이는 empty()로 만들어 진 것과 동일하게 빈 Optional 객체이다.

    @Test
    @DisplayName("null을 Optional로 캡슐화 시도한다.")
    public void scOptional3(){
        SoundCard soundCard = null;
        Optional<SoundCard> sc = Optional.ofNullable(soundCard);
        assertThat(sc, OptionalMatchers.isEmpty());
    }

Do Something If a Value Is Present

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

위와 같이 작성할 것을 isPresent()를 if문에 사용하여 다음과 같이 표현할 수 있다.
ifPresent()를 사용하고 함수를 넘기면 이를 더 함축적으로 쓸 수 있다.

    @Test
    @DisplayName("값이 존재하는 옵셔널일 경우 print한다.")
    public void scOptional4(){
        Optional<SoundCard> sc = Optional.of(new SoundCard());
        if(sc.isPresent()){
            System.out.println(sc.get());
        }
        sc.ifPresent(System.out::println);
    }

Default Values and Actions

Optional로 캡슐화된 객체에 orElse구문을 이용하여 null일 경우 새로운 디폴트 값을 할당할 수 있다.
orElseThrow를 사용하면 null일 경우 특정한 Exception을 발생시키는 것도 가능하다.

    @Test
    @DisplayName("빈 옵셔널 객체라면 디폴트 값을 할당하도록 한다.")
    public void emptyOptional2(){
        //null인 객체
        Optional<SoundCard> emptySc = Optional.empty();
        
        //default 값으로 사용할 객체
        SoundCard defaultSc = new SoundCard();
        
        //emptySc가 null일 경우에는 defaultSc를 soundCard에 할당한다.
        SoundCard soundCard = emptySc.orElse(defaultSc);
        
        //최종적으로 할당된 값이 defaultSc인지 확인한다.
        assertThat(soundCard, is(defaultSc));
    }

위 이미지는 다른 프로젝트에서 사용한 것을 가져온 것이다.
Optional.of( .. ).orElseThrow(() -> new Exception());으로 처리하고 있다.

Rejecting Certain Values Using the filter Method

filter() 메소드는 Stream 인터페이스와도 자주 이용되는데 말그대로 "필터링"을 위한 메소드이다. 도큐먼트에 쓰인 글을 잠시 다시 보도록 하겠다.

filter 메소드는 predicate(술어)를 argument로 사용한다. 만약 값이 Optional 객체에 존재하고 이것이 argument로 넘어온 predicate아 일치하면 filter 메소드는 "해당 값"을 반환한다. 그렇지 않으면 빈 옵션 객체를 반환한다. Stream 인터페이스 값이 옵션 객체에 존재하고 술어와 일치하면 필터 메서드는 해당 값을 반환합니다. 그렇지 않으면 빈 옵션 객체를 반환합니다. 스트림 인터페이스를 사용하여 filter 메소드를 사용했더라면 이미 비슷한 패턴을 보았을 수도 있을 것이다.

다음과 같이 테스트 코드를 작성해보았다.

    @Test
    @DisplayName("usb 시리얼 넘버가 AAA123일 경우에만 이를 반환하도록 한다.")
    public void usbOptional1(){
        //타겟인 문자열과 타겟이 아닌 문자열이다.
        String target = "AAA123";
        String notTarget = "BBB999";

        //각 시리얼 넘버를 가진 usb을 옵션 객체에 넣는다.
        Optional<USB> usb1 = Optional.of(new USB(target));
        Optional<USB> usb2 = Optional.of(new USB(notTarget));

        //옵션 객체를 담을 리스트
        List<Optional<USB>> usbList = new ArrayList<>();

        //각 usb의 시리얼 넘버가 타겟과 일치한 경우에만 비어있지 않은 옵션 객체가 들어간다.
        //즉, usb2는 타겟 넘버를 가지지 않았으므로 비어있는 옵션 객체가 들어갈 것이다.
        usbList.add(usb1.filter(usb -> usb.getSerialNumber().equals(target)));
        usbList.add(usb2.filter(usb -> usb.getSerialNumber().equals(target)));

        //리스트를 직렬화하여 usb가 존재하는 옵션 경우로만 필터링하고 배열로 바꿔 그 길이를 재었다. -> usb1만 존재할 것이므로 1
        assertThat(usbList.stream().filter(usb -> usb.isPresent()).toArray().length, equalTo(1));
        //리스트의 첫 번째 값은 usb1과 같을 것이다.
        //리스트의 두 번째 값은 Optional.empty() 즉, 비어있는 값 == null 일 것이다.
        assertThat(usbList.get(0).get(), equalTo(usb1.get()));
        assertThat(usbList.get(1).isEmpty(), equalTo(true));
    }

디버깅을 하면 usb1은 리스트에 들어가지만 usb2는 필터링되어 빈 값이 들어가는 것을 확인 할 수 있다.

다음은 filterifPresent를 사용해 if문과 동일한 동작을 수행하는 구문을 구현한 것이다.

    @Test
    @DisplayName("필터링 테스트.")
    public void usbOptional2(){
        String target = "AAA123";
        USB usb = new USB(target);
        if(usb != null && usb.getSerialNumber().equals(target)){
            System.out.println("기존의 if문을 이용한 방식");
        }

        Optional<USB> optionalUSB = Optional.of(new USB(target));
        optionalUSB.filter(optionalUsb -> optionalUsb.getSerialNumber().equals(target)).ifPresent(none->System.out.println("optional을 통한 필터링"));

    }

❗️NOTE
마지막 ifPresent()를 보면 의아함을 느낄 수 있는데, none이 대체 뭐고 왜 들어갔냐는 것이다. none은 정말 아무의미 없는 글자일 뿐인데, 전혀 오류가 발생하지 않는다. (선언적으로 값이 포함된 객체나 원시타입도 아니다.) 이를 빼고 ()로 작성하면 다음과 같은 경고 문구가 뜬다.
cannot infer functional interface type
직역하자면 기능적 인터페이스 타입을 추론/해석 할 수 없다는 뜻인데, 구글링을 통해 다음과 같은 답변을 찾았다.

  • ifPresent ()는 Consumer <String>, 즉 String을 인수로 취하는 함수를 예상합니다. 그러나 당신은 아무런 Argument없이 람다를 전달합니다.

Oracle Java 공식 문서에서는 Consumer를 다음과 같이 설명한다.

  • This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference. 이것은 기능 인터페이스이므로 람다식이나 메소드 참조를 위한 할당 대상으로 사용할 수 있다.

ref. http://quabr.com:8182/59146040/java-cannot-infer-functional-interface-type
ref. https://docs.oracle.com/javase/8/docs/api/java/util/function/Consumer.html

Extracting and Transforming Values Using the map Method

아마 우리가 사운드카드가 가지고 있는 usb가 올바른 시리얼 넘버를 가지고 있는지 판단하기 위해서는 다음과 같이 코드를 작성할 것이다.

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

이를 map 메소드를 이용하여 널과 원하는 값인지를 동시에 확인하는 패턴으로 작성할 수 있다.
Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB); 이렇게 말이다. 🤔

⭐️ 이는 Stream과 함께 사용되는 map 메소드와 직접적으로 유사하다. 거기에서도 map 메소드에 함수를 전달해 이를 스트림 요소에 적용하는데 만약 스트림이 비어있으면 아무일도 일어나지 않는다.

Optional 클래스의 map 메소드 또한 위에서 설명한 것과 정확히 동일하다. Optional에 포함된 값은 argument로 전달된 함수에 의해 "변환"되며, Optional이 비어있으면 아무일도 일어나지 않는다.

이 부분은 도큐먼트에서는 다음처럼 작성하고 있다.
파란색으로 표시한 부분을 보면 usb를 바로 가져다가 getVersion()을 사용하고 있는데, 처음 SoundCard 클래스에서 usb를 Optional 객체를 사용했기 때문에 반드시 usb.get()이라는 구문이 필요해지게 된다.

이 때 usb는 Optional<USB>이므로 비어있는 옵션 객체인 경우에는 get()을 수행하려하면 exception이 발생한다. (위 예제에서는 havenUsbSoundCard 객체 때문에 발생한다.)

따라서 filter를 이용해 usb가 존재하는지(usb.isPresent()) 판단할 필요가 있었다. (map(USB::getSerialNumber)를 시도해보았는데 안됐음.)

2020.05.09 추가

Cascading Optional Objects Using the flatMap Method

바로 위에서 map(USB::getSerialNumber)이 수행되지 않는다고 이야기 했는데 이에 대해서 도큐먼트에선 다음처럼 설명하고 있다.

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

안타깝게도 위의 코드는 컴파일되지 않는다.
Optional<Computer> 타입은 가변적인 컴퓨터 타입이므로 map 메소드를 호출하는 것은 가능하다. getSoundcard()는 Optional<Soundcard> 유형의 객체를 반환하는데, 이는 맵 조작의 결과가 Optional<Optional<Soundcard>> 유형의 오브젝트임을 의미한다. 따라서 결과적으로 getUSB() 호출은 유효할 수 없다.


👉 이전 코드에서 map(Soundcard::getUSB)는 유효했으나 (반환값이 Optional<Optional<USB>>이다.) 그 다음의 map(USB::getSerialNumber)이 컴파일 에러를 일으킨 것과 같은 맥락이다.


가장 바깥쪽 Optional에는 다른 Optional 값이 포함돼 있으므로 유효하지 않으므로 당연히 getUSB 메소드는 지원되지 않는다. 아래 그림은 중첩된 Optional 구조를 보여준다.

Stream에서 사용하는 flatMap 메소드는 함수를 argument로 사용하여 다른 Stream을 반환한다. 이 함수는 Stream의 각 요소에 적용되며 단일 스트림으로 결과를 반환한다. 그러나, flatMap은 생성된 각 스트림을 해당 스트림의 내용으로 대체하는 효과가 있다. 즉, 함수에 의해 생성된 모든 개별 스트림은 하나의 단일 스트림으로 통합되거나 "flatten"(평탄)하게 된다. 이와 유사하게 두 단계의 Optional을 하나로 "flatten"하게 만들려고 한다.

Optional은 flatMap 메서드도 지원한다. 그 목적은 (맵 연산과 마찬가지로) Optional 값에 변환 함수를 적용한 다음 결과적으로 두 단계의 Optional을 하나로 병합하는 것이다. 아래 그림은 변환 함수가 Optional 객체를 반환 할 때 map과 flatMap의 차이점을 보여준다.

다음과 같은 테스트 코드를 작성하여 연속적으로 Optional을 처리하는 것을 확인해보자

@Test
    @DisplayName("flatmap을 이용하여 연속적인 Optional 객체를 처리한다.")
    public void computerOptional1(){
        String target = "ABC123";

        SoundCard soundCard = new SoundCard();
        soundCard.setUsb(Optional.of(new USB(target)));
        Computer computer = Computer.builder().soundCard(soundCard).build();

        computer.toOptional().flatMap(Computer::getSoundCard)
                                .flatMap(SoundCard::getUsb)
                                .filter(usb -> usb.getSerialNumber().equals(target))
                                .ifPresent(none->System.out.println("is ok, target : "+target));

    }

그 다음은 1.사운드 카드가 존재하지 않는 컴퓨터 / 2.USB가 존재하지 않는 사운드 카드를 가진 컴퓨터 / 3. 조건을 만족하는 컴퓨터 세가지를 가진 리스트를 flatmap을 이용해 판별하도록 하였다.

    @Test
    @DisplayName("flatmap을 이용하여 연속적인 Optional 객체를 처리한다.")
    public void computerOptional2() {
        String target = "ABC123";

        SoundCard soundCard2 = SoundCard.builder().usb(null).build();
        SoundCard soundCard3 = SoundCard.builder().usb(new USB(target)).build();

        Computer computer1 = Computer.builder().soundCard(null).build();
        Computer computer2 = Computer.builder().soundCard(soundCard2).build();
        Computer computer3 = Computer.builder().soundCard(soundCard3).build();

        List<Computer> computerList = Arrays.asList(computer1, computer2, computer3);

        for (int index = 0; index < computerList.size(); ++index) {
            findTarget(computerList.get(index), index, target);
        }
    }
    
    
    private void findTarget(Computer computer, int index, String target) {
        computer.toOptional().flatMap(Computer::getSoundCard)
                .flatMap(SoundCard::getUsb)
                .filter(usb -> usb.getSerialNumber().equals(target))
                .ifPresent(none -> System.out.println("is ok, index : " + index + ", target : " + target));
    }

결과는 예상한대로 3번째 컴퓨터(index 2)만 출력되었다.

위의 모든 예제 코드는 github에서 확인 할 수 있습니다.
추가적인 api에 관련된 내용은 https://www.baeldung.com/java-optional에서 학습할 수 있습니다.

profile
🌱 😈💻 🌱

0개의 댓글