개요

널처리 방법의 일부인 자바 Optional 에 대해서 간단히 메모.

 

 

Chapter10:null 대신 Optional

Null Pointer

컴퓨터에서 널포인트 또는 널 레퍼런스는 유효한 객체를 가리키지 않은 저장된 값이 있음을 의미한다. 흔히 자바에서 아래와 같이 표현할 수 있다.

// person 레퍼런스는 특정한 Person 오브젝트를 가리키고 있다.
Person person = new Person("PARK");

// personOfNull 은 null 레퍼런스이다.
Person personOfNull = null;

 

1965년 토니호어라는 영국 컴퓨터과학자가 힙에 할당되는 레코드를 사용하여 형식을 갖는 최초의 프로그래밍 언어 중 하나인 알골을 설계하면서 처음 null 레퍼런스가 등장했다. 그는 이후에 null 레퍼런스를 만들게 된 것을 Billion Dollar Mistake 라는 표현을 하였다.

 

 

has a 관계로 참조값을 지니고 있는 어떤 객체가 있다고 가정한다.

public class Person {
	private Car car;
    public Car getCar() { return this.car; }
}

public class Car {
	private Insurance insurance;
    public Insurance getInsurance() { return this.insurance; }
}

public class Insurance {
	private String name;
    public String getName() { return this.name; }
}
  • Car currentCar = person.getCar().getInsurance().getName() 를 하였을 시에
    • Person 이 널일 수 있다.
    • Car 가 널일 수 있다.
    • Insurance 가 널일 수 있다.
    • String name 이 널일 수 있다.
  • 위의 경우에 반복패턴을 통해서 null 여부를 검사할 수 있다. 하지만 그렇게 진행하다보면 코드의 가독성이 떨어지고 들여쓰기의 수준이 높아진다. (지속적인 if 문이 계속해서 들어갈 수 있기 때문이다.)

자바8에서는 이러한 문제 대안으로 옵셔널 Optional 클래스를 제공하고 있다.

 

Optional

선택형 값의 여부이다. 어떠한 특정한 값이 있을수도 또는 없을수도 있음을 말할 수 있다. 다시 말해서 선택형 값의 여부이다. 어떠한 객체가 있다고 한다면, 아니 어떠한 오브젝트가 있다고 가정을 한다면 그걸 한번 Optional 객체로 래핑(wrapping) 하는 것이다. ! 

 

옵셔널 클래스에서는 인스턴스 값을 읽을 수 있도록 몇가지 메소드를 제공하고 있다. 

  • get()
    • 값을 읽는 가장 간단한 메소드이다. 하지만 가장 안전하지 않은 메소드이다. get() 을 통해서 래핑된 값이 있으면 반환을 수행하지만, 값이 없다면 NoSuchElementException 을 일으킨다. 결국 중첩되게 null 검사를 하는 것과 별반 다르지 않다.
  • orElse(T other)
    • Optional 이 값을 포함하지 않고 있는 empty 인 경우에 디폴트 값을 제공할 수 있다.
  • orElseGet(Supplier<? extends t> other)
    • orElse() 메소드에 대응되는 lazy 한 버전이다. orElse 와 orElseGet 의 차이점은 여기링크에서 확인하자.

 

Optional 을 이용하면 앞선 코드를 아래와 같이 변경할 수 있다.

public class Person {
	private Optional<Car> car;
    
    // 사람이 차량을 소유 또는 소유하지 않았을수도 있으므로 Optional 로 정의
    public Optional<Car> getCar() { return this.car; }
}

public class Car {
	private Optional<Insurance> insurance;
    
    // 자동차가 보험에 가입 또는 미가입일수 있으므로 Optional 로 정의
    public Optional<Insurance> getInsurance() { return this.insurance; }
}

public class Insurance {
	private String name;
    
    // 보험회사에는 반드시 이름이 있다.
    public String getName() { return this.name; }
}

위의 부분에서 확인할 것은 

 

모든 null 레퍼런스를 Optional 로 대치하는 것은 바람직하지 않다. Optional 의 역할은 더 이해하기 쉬운 API 를 만드는데 주안점을 두고 있다. 즉 우리가 "메소드 시그니처" 만을 보고도 선택형 값의 여부를 구별할 수 있는 것이다.

 

Optional 의 등장으로 인해 wrapping 된 데이터를 unwrap 하여 값을 얻을 수 없는 상황에 대해서 적절하게 대응할 수 있도록 강제하는 효과가 있다.

 

Optional 을 이용한 패턴 일부

아래의 생각을 가정해보자.

  • 사람이 있다. 
  • 사람은 지갑을 가지고 있다.
  • 지갑에는 돈이 있을수도 또는 없을수도 있다.

코드로 나타내면 아래와 같다.

public class Person {

    private final String name;
    private final Wallet wallet;

    public Person(String name, Wallet wallet) {
        this.name = name;
        this.wallet = wallet;
    }

    public String getName() {
        return name;
    }

    public Wallet getWallet() {
        return wallet;
    }
}

public class Wallet {

    private Money money = null;

    public Wallet(final Money money) {
        this.money = money;
    }

    public Wallet() {}

    public Money getMoney() {
        return money;
    }
}

public class Money {
    private final Long money;

    public Money(Long money) {
        this.money = money;
    }

    public Long getMoney() {
        return money;
    }
}

그리고 위의 코드에서 지갑 내에 돈이 있는지 없는지 여부를 판단하려고 한다면, 내 지갑의 상태를 살펴봐야 한다. 바로 돈을 살피는게 아니라 그 이전의 행동은 내 지갑을 열어 돈이 있는지 없는지 여부를 확인이 필요하기 때문이다.

 

패턴일부 : Optional Map() 이용하기

Optional 은 Map() 메소드를 제공한다. map 은 스트림처럼 비슷하게 쓰일 수 있는데, Optional 안의 래핑된 객체를 다른 객체로 변환시킬 수 있다.

/**
 * richPerson 은 지갑안에 50000원이 있다.
 */
Person richPerson = new Person("PARK", new Wallet(new Money(50000L)));
Optional<Wallet> w1 = Optional.of(richPerson.getWallet());
Optional<Money> m1 = w1.map(Wallet::getMoney);
System.out.println("result1 : " + ((m1.isPresent()) ? "지갑에 돈이 있습니다." : "지갑에 돈이 한 푼도 없습니다."));

/**
 * poorPerson 은 지갑안에 아무것도 없다.
 */
Person poorPerson = new Person("PARK", new Wallet());
Optional<Wallet> w2 = Optional.of(poorPerson.getWallet());
Optional<Money> m2 = w2.map(Wallet::getMoney);
System.out.println("result2 : " + ((m2.isPresent()) ? "지갑에 돈이 있습니다." : "지갑에 돈이 한 푼도 없습니다."));

위의 코드에서 살피면,

  • getWallet() 을 통해서 Wallet 을 획득하고 해당 값을 Optional<Wallet> 으로 래핑해주었다.
  • 이후에 map(Wallet::getMoney) 를 통해서, Optional<Wallet> 을 Optional<Money> 로 변경해주었다.

만약에 해당 money 에 값이 있으면 ? 혹은 money 의 값이 null 이라면 어떻게 결과가 나올까? 결과는 아래와 같이 나온다. 

result1 : 지갑에 돈이 있습니다.
result2 : 지갑에 돈이 한 푼도 없습니다.

 

Optional<Money> 를 하였을 때, 해당 값이 존재하지 않으면 Optional 은 빈 Optional 을 반환하고 아닐경우에만 래핑된 Optional 이 반환되는 것이다.

 

응용해보기.

/**
 * map 의 간결화
 * 계속에서 Optional 을 중첩해서 get() 으로 획득하고 다시 of() 로 감싸주어야 한다.
 */
Optional<Person> optionalPerson = Optional.of(new Person("PARK", new Wallet(new Money(1000L))));
Optional<Wallet> optionalWallet = Optional.of(optionalPerson.get().getWallet());
Money optionalMoney = Optional.of(optionalWallet.get().getMoney()).orElse(new Money(0L));
Optional<Person> optionalPerson = Optional.of(new Person("PARK", new Wallet(new Money(1000L))));
Money currentMoney = optionalPerson
        .map(Person::getWallet)
        .map(Wallet::getMoney)
        .orElse(new Money(0L));

두번째 코드처럼 메소드 체이닝 형태로 간결하게 변경할 수 있다.

 

 

추가적으로 중첩된 Optional 에 대해서 flatMap() 메소드가 있다. 이 부분은 get() 메소드로 획득한 값이 Optional 로 래핑된 상태에서 flat 으로 한번 벗겨주고, 진행하는건데 자세한 내용은 여기서 살펴보자.

 

참고자료

en.wikipedia.org/wiki/Null_pointer#cite_note-11

자바 8 인 액션

Posted by doubler
,