자바 8의 Optional 등장
- Coding/Java
- 2020. 11. 6.
NullPointerException
NullPointerException은 개발자가 한번이라도 만나봤을 에러이다. 그정도로 흔하게 일어나는 에러로, 이는 “자바의 모든 객체는 NULL일 수 있다.”” 라는 말을 확인시켜준다. NullPointerExcpetion이 발생하는 경우를 예시로 보자.
public static getCarInsurancename(Person pserson) {
return person.getCar().getInsurance().getName();
}
위 코드에서 getCar()를 실행한 후의 값이 NULL이라면? 차가 없는 사람은 존재할 수 있다. person.getCar()의 값이 NULL인데 getInsurance()가 실행되면 여기서 NullPointerException이 발생하게된다. 또한 만약 person이 NULL이더라도 같은 방식으로 getCar()이 실행되었을때 NullPointerException이 발생한다. 이를 해결하기 위해선 어떤 방법이 있을까?
if 문을 통한 NULL값 체크
if문의 NULL값 체크를 통해 NullPointerException을 발생하지 않도록 코딩해보자.
if (AA != null) {
if (BB != null) {
if (CC != null) {
}
}
}
위 코드에서는 중첩 if문을 사용하였다. 이를 반복패턴이라고 하는데, 이것은 깊은의심(Deep Doubt)와 같다. 깊은 의심이란, 변수가 null인지 의심되어, 코드 들여쓰기 수준이 증가하는 것이다. 이는 코드의 구조를 엉망으로 만들고, 코드의 가독성을 떨어트린다.
if(A == null) {
return ...
}
if(B == null) {
return ...
}
if(C == null) {
return ...
}
중첩 if문을 제거하고 이번에는 총 3개의 출구를 만들었다. null일 경우 if문을 통해 빠져나올 수 있는 출구를 만들었는데, 이는 개발자가 충분히 실수를 일으키게 될만한 요소를 지녔고 이는 잠재적인 버그를 가지고있다고 말할 수 있다. 누군가가 해당 객체가 NULL일 수 있다는 사실을 잊어버린다면? 이는 곧 버그로 발견될 것이다.
NULL의 근본적인 문제
1) 에러의 근원이다.
2) 코드 가독성을 떨어트린다.
3) 아무런 의미가 없다.
4) 자바의 철학에 위배된다.
5) 버그 발생 가능성을 증가시킨다.
Optional 클래스의 등장
자바 8에서는 NullPointerException 에러를 방지하기 위해 Optional 클래스를 등장시켰다. Optional 클래스란, 선택형 값을 캡슐화하는 클래스이다.
Car car = null;
위 코드에서는 car 객체가 null이다.
Optional<Car> car;
Optional로 감싼 Car 객체는 null일수도 있고, null이 아닐수도 있다. 이는 값이 존재하지 않을 수 있다.라는 의미이다. 만약 값이 존재한다면, Optional이 값을 감쌀것이고 값이 존재하지 않는다면, Optional<Car>은 빈 값을 지니게된다. 값이 없을 경우 Optional.empty() 메서드를 사용하여 Optional을 반환할 수 있다.
empty() : Optional의 특별한 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드이다.
Optional에 대한 개념을 간단히 알아보았는데, 항상 사용해서는 안된다. 반드시 사용해야할 경우와 사용하면 안될 경우를 구분하는 고민을 해야한다. 만약 필수 값을 가져야할 객체를 Optional<T>로 선언한다면 필수값이 빈 값을 가질수도 있다는 잘못된 사실을 명시하는 상황이 되어버리기 때문이다.
Optional 선언 방법
Optional을 사용하는 방법에는 여러가지가 있다. 실제로 Optional을 어떻게 사용할까? 우선, Optional을 사용하려면 Optional 객체를 만들어야한다.
1) 빈 Optional
Optional<Car> optCar = Optional.empty();
2) NULL이 아닌 값으로 Optional 만들기
Optional<Car> optCar = Optional.of(car);
of 메서드를 사용하여 null이 아닌 값을 포함시키고 있다. 여기서는 car이 NULL일때 NullPointerException이 발생한다.
3) NULL 값으로 Optional 만들기
Optional<Car> optCar = Optional.ofNullable(car);
ofNullAble 메서드를 사용하여 NULL 값이 저장할 수 있게 되었다. car 객체가 NULL이라면 빈 Optional 객체가 반환된다.
4) GET 메서드
Optional 에는 get 메서드가 존재한다. Optional 안의 값을 가져오는데, 이는 Optional이 NULL일 경우 NullPointerException이 발생한다. 이는 객체가 NULL일때에 발생하는 문제점과 같다.
기본 코드 vs Optional 코드
1) 기본 코드
String name = null;
if (insurance != null) {
name = insurance.getName();
}
2) Optional 사용 코드
Optional<Insurance> optInsurance = Optional.ofNullAble(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
1)번의 코드를 2)번의 코드로 변환할 수 있게되었다. 여기서 map 함수를 사용하였는데 map 함수는 만약 Optional이 값을 포함하면 map의 인수로 제공된 함수(Insurance::getName)를 실행하여 값을 바꾼다. 반대로 Optional의 값이 NULL이라면, 아무일도 일어나지 않는다.
FlatMap을 사용한 Optional 객체 연결
우선 코드를 보자.
Optional<Person> optPerson = Optional.of(person); // null 불가능
Optional<String> name = optPerson.map(Person::getCar) // (1) Optional<Car> 반환
.map(Car::getInsurance) // (2)
.map(Insurance::getName)
of 메서드를 사용하여 null이 불가능한 Optional을 만들었다. (1)의 map 메서드를 사용하여 Optional<Car>을 반환받았다. (2)의 map 메서드를 사용하면 문제가 발생한다. 따라서 위 코드는 컴파일 에러가 발생한다. (2)의 map 메서드는 또다른 Optional 객체를 반환한다. 따라서 Optional<Optional<Car>> 이라는 이차원 Optional을 반환하게 되어 에러가 발생한다. 이를 해결하기 위해서 나온게 flatMap 메서드이다.
flatMap()
flatMap 메서드는 이차원 Optional을 일차원 Optional로 평준화해준다. 그리고 두개의 Optional 중 하나라도 NULL이면 빈 Optional을 생성한다. Optional 내부에 다른 Optional을 만들고 이 Optional 안에 값을 저장하는게 Map 메서드라면 flatMap 메서드는 하나의 Optional로 저장한다.
flatMap 의 사용
public Set<String> getCarInsuranceNames(List<Poerson> peresons) {
return persons.stream()
.map(Person::getCar) // (1)
.map(optCar -> optCar.flatMap(Car::getInsurance)) // (2)
.map(optIns -> optIns.map(Insurance::getName)) // (3)
.flatMap(Optional::stream) // (4)
.collect(toSet()); // (5)
}
(1) : 사람 목록을 각 사람이 보유한 자동차의 Optional<Car> 스트림으로 변환한다.
(2) : FlatMap 연산을 이용해 Optional<Car> 을 Optional<Insurance>로 변환한다.
(3) : Optional<Insurance>를 해당 이름의 Optional<String>으로 매핑한다.
(4) : Stream<Optional<String>>을 현재 이름을 포함하는 Stream<String> 으로 변환한다.
Stream<Optional<String>> stream = ...
Set<String> result = stream.filter(Optional::isPresent)
.map(Optional::get)
.collect(toSet());
위 코드를 (4)의 한줄의 코드로 같은 결과를 얻을 수 있다.
(5) : 결과 문자열을 중복되지 않은 값을 갖도록 집합으로 수집한다.
Optional 다시 정의
Optional에 대한 기초적인 개념을 알아보았다. 결국 Optional은 도메인 모델과 관련한 암묵적인 지식에 의존하지 않고 명시적으로 시스템을 정의하는 것이다. 이는 정확한 정보의 전달이 가능하다. 또한 문서화의 제공과 같다. 예를 들어, Optional 반환 메서드가 있다는 것은 모든 사람에게 이 메서득 빈 값을 받거나 빈 결과를 반환할 수 있다는 사실을 알려주는 것과 같다. 이는 문서화를 제공한다고 볼 수 있다.
ofElse 메서드
한가지 의문이 들 수 있다. Optional이 비어있을때 우리는 default인 기본 값을 지정할 수 있으면 좋지 않을까? 이는 ofElse라는 메서드로 제공된다.
Optional 디폴트 액션과 언랩
1) get()
값을 읽는 메서드로 안전성이 없다. 값이 없으면 “NoSuchElementException”을 발생시킨다. 따라서 get 메서드는 값이 있다는 확신이 있을때 사용해야한다.
2) orElse()
Optional의 값이 없을때 default Value 설정이 가능하다.
3) ofElseGet(Supplier<? extends T> other)
Optional 값이 없을때 Supplier 을 실행한다. 이는 디폴트 메서드 생성 시간이 오래걸릴 경우와 Optional이 비어있을때에만 값을 생성해야할 경우(기본값이 반드시 필요할 경우)에 사용한다.
4) orElseThrow(Supplier<? extends X> exceptionSupplier)
Optional이 비어있을 때 예외가 발생한다. 여기서 발생시킬 에러를 선택할 수 있다.
5) ifPresent(Consumer<Consumer<? Super T> consumer)
값이 존재할 때 인수로 넘겨준 동작을 실행한다. 만약 인수가 없다면 아무일도 발생하지 않는다.
6) ifPresentOrElse(Consumer<? Super T> action, Runnable emptyAction)
Optional이 비었을때 실행할 수 있는 Runnable을 인수로 받는다. 이외에는 위 5)의 ifPresent와 같다.
두 Optional 합치기
예시를 보자. Person과 Car 정보를 이용해서 가장 저렴한 보험료를 제공하는 보험회사를 찾는 로직을 구현한 외부 서비스가 있다고 가정하자.
public Insurance FindCheapestInsurance(Person person, Car car) {
// 다양한 보험회사가 제공하는 서비스 조회
// 모든 결과 데이터 비교
return cheapestCompany;
}
이제 두 Optional을 인수로 받아서 Optional<Insurance>를 반환하는 null 안전 버전의 메서드를 구현해야한다. 인수로 전달한 값 중 하나라도 비어있으면 빈 Optional<Insurance>를 반환한다. Optional 클래스는 Optional 값을 포함하는지 여부를 알려주는 isPresent라는 메서드를 제공한다.
public Optional<Insurance> nullSafeFindChepestInsuracne (Optional<Person> person, Optional<Car> car) {
if (person.isPresent() && car.isPresent()) {
return Optional.of(findChepestInsurance(person.get(), car.get()));
} else {
return Optional.empty();
}
}
nullSafeFindChepestInsuracne 메서드는 person, car의 시그니처만으로 둘다 아무 값도 반환하지 않을 수 잇다는 정보를 명시적으로 보여주고있다. 이는 null확인 코드와 다른 점이 없다. Optional 클래스에서 제공하는 기능을 이용해서 더 자연스럽게 개선시켜 줄 필요가 있다.
Filter 메서드
객체의 메서드를 호출해서 어떤 프로퍼티를 확인해야할 경우가 있다. 예를 들어, 보험 회사 이름이 CambridgeInsurance 인지 확인해야 한다고 가정하자. 이 작업을 안전하게 수행하려면 다음 코드에서 보여주는 것처럼 Insurance 객체가 null 인지 여부를 확인한 다음 getName() 메서드를 호출해야한다.
Insurance insurance = ...;
if (insurance != null && "CambridgeInsurance".equals(insurance.getName())) {
System.out.println("ok");
}
위 코드를 filter 메서드를 사용한 코드로 변환해보자.
Optional<Insurance> optInsurance = ...
optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName()))
.isPresent(x -> System.out.println("ok"));
filter 메서드는 프레디케이트(boolean)를 인수로 받는다. Optional 객체가값을 가지고 프레디케이트와 일치하면 filter 메서드는 그 값을 반환하고 그렇지 않으면 빈 Optional 객체를 반환한다. Optional이 비어있다면 filter 연산은 아무 동작도 하지 않고, 값이 있으면 그 값에 프레디케이트를 적용한다. 프레디케이트 적용 결과가 true이면 Optional에는 아무 변화도 일어나지 않는다. 하지만 결과가 false이면, 값은 사라져버리고 Optional은 빈 상태가 된다.
Optional로 null이 될수 있는 대상을 감싸기
자바 8에서 등장한 Optional 클래스로 인해 우리는 null 일때의 경우를 제어할 수 있게 되었다. 객체가 null일 경우 null을 반환하는 것보다는 Optional을 반환하는 것이 더 바람직하다. get 메서드의 시그니처는 우리가 변경할 수 없지만 get 메서드의 반환값은 Optional로 감쌀 수 있다. Map<string, Object> 형식의 Map에 key 로 접근한다고 가정해보자.
Object value = map.get("key");
문자열 ‘key’에 해당하는 값이 없으면 null을 반환한다. map에서 반환하는 값을 Optional로 감싸보자.
Optional<Object> value = Optional.ofNullable(map.get("key"));
위 코드로 변환하면 우리는 null일 수 있는 값을 Optional로 안전하게 처리할 수 있다.
예외처리
자바에서는 문자열을 정수로 변환하는 정적 메서드 parseInt() 메서드를 제공한다.
Integer.parseInt(String)
이 메서드는 문자열을 정수로 바꾸지 못할 때 NumberFormatException을 발생시킨다. 이는 문자열이 숫자가 아니라는 사실을 예외로 알리는 것이다. 정수로 변환할 수 없는 문자열 문제 또한 빈 Optional로 해결할 수 있다. parseInt가 Optional을 반환하도록 모델링 할 수 있다.
public static Optional<Integer> stringToInt(String s) {
try {
return Optional.of(Integer.parseInt(s)); // 정수로 변환된 값을 포함하는 Optional을 반환한다.
} catch(NumberFormatException e) {
return Optional.empty(); // 빈 Optional을 반환한다.
}
}
기본형 Optional을 사용하지 말아야할 이유
스트림처럼 Optional도 기본형으로 특화된 OptionalInt, OptionalLong, OptionalDouble 등의 클래스를 제공한다. 예를 들어 Optional<Integer> 대신에 OptionalInt를 반환할 수 있다. 하지만 기본형 Optional을 사용하지 말아야할 이유가 존재한다. 그것은 지금까지 배워온 map, flatMap, filter 등의 유용한 메서드들을 기본형 특화 Optional은 지원하지 않기 때문이다. 게다가 스트림과 마찬가지로 기본형 특화 Optional로 생성한 결과는 다른 일반 Optional과 혼용할 수 없다. 이는 OptionalInt를 반환했을때 이를 다른 Optional의 flatMap의 메서드 참조로 전달할 수 없다는 말이다. 이는 Optional의 장점을 살리지못하는 치명적인 단점이다.
'Coding > Java' 카테고리의 다른 글
람다표현식에 대해 알아보기 (0) | 2020.11.06 |
---|---|
java8의 LocalDateTime 클래스 날짜시간 (0) | 2020.11.06 |
java에서 get() NullPointerException 일때 (0) | 2019.10.22 |
[LocalDateTime] java8 의 날짜시간 (0) | 2019.10.22 |
[JAVA] 날짜변환 String-Date-XMLGregorianCalendar-GMT+09:00 (0) | 2019.10.21 |