개요
널처리 방법의 일부인 자바 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 인 액션
'jvm lang' 카테고리의 다른 글
2022-06-23 [kotlin] nullable 쓰다가 겪은 일. (0) | 2022.06.23 |
---|---|
20220125 [kotlin] objectMapper readValue to List (수정 : 24-05-26) (0) | 2022.01.25 |
20201006 [java] annotation 작동 살피기 (0) | 2020.10.07 |
20200914 [java8/java11] windows 환경에서 자바버전 두 개 관리. (0) | 2020.09.14 |
20200616 [java8] Java8InAction 읽기 & 기록 [ch07 ~ ch08] (0) | 2020.06.18 |