모던자바인액션 - 9

이건희·2024년 1월 19일
0

모던자바인액션

목록 보기
9/9

챕터 11인 'null 대신 Optional 클래스'이다. 챕터 10은 여러번 읽었지만 내가 이해할 내용이 아니라고 판단했다..(너무 어려웠음) 그래서 일단 다음 챕터로 넘어갔다.

Optional은 실제 개발 시에 많이 사용했었다. ofNullable이나 get, orElseThrow 정도만 사용했지 나머지 메서드들이나 원리는 잘 모르고 사용했던 것 같다. 이번 챕터를 읽으면서 어느정도 정리가 되었다.

Optional

Optional은 이전 글들에서도 다뤘듯이, 선택형 값을 캡슐화하는 클래스다.

  • 값이 있으면 Optional 클래스는 값을 감싸고, 값이 없으면 Optional.empty 메서드로 Optional을 반환한다.

  • Optional.empty는 Optional의 특별한 싱글턴 인스턴스르 반환하는 정적 팩토리 메서드이다.

  • Optional을 쓰지 않고, null을 참조 하려고 하면 NullPointException이 발생하지만, Optional.empty는 Optional 객체이므로 다양한 방식으로 활용 가능하다.

Optional<Car>와 같이 감싸면, 부연 설명을 붙이지 않아도 Car는 값이 있을수도 있고, 없을 수도 있다는 의미를 자연스럽게 내포하게 된다.


Optional 객체 만들기

빈 Optional

위에 설명했듯이, Optional.empty()로 빈 Optional 객체를 얻는다.

Optional<Car> optCar = Optional.empty();

null이 아닌 값으로 Optional 만들기

정적 펙토리 메서드 Optional.of로 null이 아닌 값을 포함하는 Optional을 만들 수 있다.

Optional<Car> optCar = Optional.of(car);

하지만 car가 null이라면 즉시 NullPointException이 발생한다.

null값으로 Optional 만들기

정적 펙토리 메서드 Optional.ofNullable로 null값을 저장할 수 있는 Optional을 만들 수 있다.

Optional<Car> optCar = Optional.ofNullable(car);

car가 null이면 빈 Optional 객체가 반환된다.


map으로 Optional 값을 추출하고 변환하기

이번 챕터를 읽으면서 처음 안 사실인데, Optional은 stream처럼 map을 지원한다.

Insurance가 name을 가지고 있다고 할때,
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);

스트림의 map은 스트림 각 요소에 제공된 함수를 적용하는 연산이다. 여기서 Optional은 최대 요소의 개수가 한 개 이하인 데이터 컬렉션으로 생각할 수 있다.

  • Optional이 값을 포함하면 map의 인수로 제공된 함수가 값을 바꾼다.

  • Optional이 비어 있으면 아무 일도 일어나지 않는다.

예를 들면 아래 코드와 같다.

class Main {
    public static void main(String[] args) {
        Car a = null;
        Optional<Car> aCar = Optional.ofNullable(a);
        Optional<Integer> in = aCar.map(Car::getA);
        System.out.println(in);
    }
}

class Car {
    public int a;
    public int getA(){
        return this.a;
    }
}

위 코드 결과는 Optional.empty이다. 따로 Exception이 발생하지도 않고 정상적으로 작동한다.


flatMap으로 Optional 객체 연결

Person이 Car 객체를 필드로 가지고 있고, Car는 Insurance 객체를, Insurance 객체는 name을 가지고 있다고 치자. 그리고 name 필드는 무조건 값이 있어야 하고, Car, Insurance 객체는 null이 될 수도 있다고 치자.

아래 코드를 한번 보자

Optional<Person> optPerson = Optional.of(person);
Optional<String> name = 
	optPerson.map(Person::getCar)
   			 .map(Car::getInsurance)
             .map(Insurance::getName);

위 코드는 컴파일 되지 않는다.

  • optPerson은 Optional<Person>이라서 map을 호출 가능하다.

  • 하지만, getCar는 Optional<Car> 형식의 객체를 반환한다.

  • 즉, map 연산의 결과는 Optional<Optional<Car>> 형식으로 중첩 Optional 객체 구조이다.

이 문제는 flatMap이라는 메서드를 이용하여 해결할 수 있다. flatMap은 함수를 인수로 받아서 다른 스트림을 반환하는 메서드다. 보통 스트림의 스트림이 만들어질 때 flatMap은 인수로 받은 함수를 적용해서 생성된 각각의 스트림에서 콘텐츠만 남긴다. 즉, 하나의 스트림으로 병합된다.

따라서 아래와 같이 바꿀 수 있다.

Optional<Person> optPerson = Optional.of(person);
Optional<String> name = 
	optPerson.flatMap(Person::getCar)
   			 .flatMap(Car::getInsurance)
             .map(Insurance::getName);
             //최종 결과는 Optional<String>

Optional 스트림 조작

다음과 같이 클래스들이 있다고 하자.

public class Person {
    private Optional<Car> car;
    public Optional<Car> getTeam() { return car; }
}

public class Car {
    private Optional<Insurance> insurance;
    public Optional<Insurance> getInsurance() { return insurance; }
}

public class Insurance {
    private String name;
    public String getName() {  return name; }
}

자바 9에서는 Optional을 포함하는 스트림을 쉽게 처리할 수 있도록 Optional에 stream() 메서드를 추가했다. Optional 스트림 값을 가진 스트림으로 변환할 때 이 기능을 유용하게 활용할 수 있다.

예시로 아래를 보자.

public Set<String> getCarInsuranceNames(List<Person> persons) {
	return persons.stream()
    			  .map(Person::getCar)
                  //Optional<Car> 스트림으로 반환
                  .map(optCar -> optCar.flatMap(Car::getInsurance))
                  //flatMap을 이용해 Optional<Car>을 Optional<Insurance>로 변환
                  .map(optIns -> optIns.map(Insurance::getName)
                  //Optional<Insurance>를 Optional<String>으로 변환
                  .flatMap(Optional::stream)
                  //Stream<Optional<String>>을 Stream<String>으로 변환
                  .collect(toSet());
  • getCar메서드는 단순 Car가 아니라 Optional<Car>를 반환한다. 즉, 값이 있을 수도 있고 없을 수도 있다.

  • Optional 덕분에 편하게 연산할 수 있지만, 마지막 결과를 얻으려면 빈 Optional을 제거하고 값을 언랩해야 한다.

예를 들어 다음과 같다.

Streama<Optional<String>> stream = ...
Set<String> result = stream.filter(Optional::isPresent)
						   .map(Optional::get)
                           .collect(toSet());

하지만 위에서 Optional 클래스의 stream() 메서드를 이용하면 한번의 연산으로 바로 위의 코드와 같은 결과를 얻을 수 있다.

Optional의 stream 메서드는 각 Optional이 비어 있는지 아닌지에 따라 Optional을 0개 이상의 항목을 포함하는 스트림으로 변환한다.


디폴트 액션과 Optional 언랩

Optional 인스턴스에 포함된 값을 읽는 다양한 방법들이 있다.

get()

  • get은 가장 간단하면서 가장 위험한 메서드이다.

  • 값이 있으면 해당 값을 반환하고 값이 없으면 NoSuchElementException을 발생시킨다.

  • Optional에 값이 반드시 있다고 가정할 수 있는 상황이 아니면 get 메서드를 사용하지 않는 것이 좋다.

orElse()

  • Optional이 값을 포함하지 않을 때 기본 값을 제공할 수 있다.
String text = null;
String t = Optional.ofNullable(text).orElse("Hi");
//t에는 "Hi"가 할당된다.

orElseGet(Supplier<? extends T> other)

  • orElse는 Optional에 값이 없을 때만 Supplier가 실행된다.

  • 디폴트 메서드를 만드는데 시간이 걸리거나, Optional이 비어 있을 때만 기본값을 생성하고 싶다면 orElseGet을 사용한다.

orElseThrow()

  • orElseThrow는 Optional이 비어 있을 때 예외를 발생 시킨다.

  • get과 달리 예외의 종류를 선택 가능하다

ifPresent(Consumer<? super T> consumer)

  • 값이 존재할 때 인수로 넘겨준 동작을 실행한다.

  • 값이 존재하지 않으면 아무일도 일어나지 않는다.

ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) : Java 9 에 추가

  • Optional이 비었을 때, 실행할 수 있는 Runnable을 받는다는 점에서 ifPresent와 다르다.

  • Optional 값이 있을 때, 비었을 때 동작을 받는다.


필터로 특정 값 거르기

Insurance의 name 필드가 "CambridgeInsurance"인지 확인해야 된다고 가정하자. 이 작업을 수행하려면 기존의 방식으로 아래와 같이 코드를 작성할 수 있다.

Insurance insurance = ...;
if(insurance != null && "CambridgeInsurance".equals(insurance.getName())) {
	System.out.println("OK");

Optional 객체의 filter 메서드를 통해 다음과 같이 재구현이 가능하다.

Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName())
			.ifPresent(x -> System.out.println("OK"));
  • Optional 객체가 값을 가지며, filter 메서드 내부의 함수와 일치하면 그 값을 반환하고, 그렇지 않으면 빈 Optional 객체를 반환한다.

  • Optional이 비어 있다면 filter 연산은 아무 동작도 하지 않는다.

profile
백엔드 개발자가 되겠어요

0개의 댓글