반응형

1. 서론

이번 포스팅에서는 Chapter11의 null 대신 Optional 클래스에 대해 진행하도록 하겠습니다.

 

2. 값이 없는 상황을 어떻게 처리할까?

책에서는 null 관련하여 아래 예제를 말합니다.

 

public class Person {
    private Car car;

    public Car getCar() {
        return car;
    }
}
    
public class Car {
    private Insurance insurance;

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

public String getCarInsuranceName(Person person) {
    return person.getCar().getInsurance().getName();
}

 

위의 코드는 Person이 차를 가지고 있지 않는 경우에는 NullPointerException 이 발생하게 됩니다.

 

1) 보수적인 자세로 NullPointerException 줄이기

 

위 예제에서 null를 예방하기 위해 if-else 를 사용하게 된다면 아래와 같은 코드가 될 것입니다.

 

public String getCarInsuranceName(Person person) {
    if (person != null) {
        Car car = person.getCar();
        if (car != null) {
            Insurance insurance = car.getInsurance();
            if (insurance != null) {
                return insurance.getName();
            }
        }
    }
    return "Unknown";
}

 

한눈에 봐도 코드가 난잡하며, 객체의 연관도가 깊을수록 if의 깊이는 증가됩니다.

 

가끔 깊이가 너무 깊어져 이를 예방하기 위해 아래와 같은 코드가 나올수도 있습니다.

 

public String getCarInsuranceName(Person person) {
    if (person == null) {
        return "Unknown";
    }
    Car car = person.getCar();
    if (car == null) {
        return "Unknown";
    }
    Insurance insurance = car.getInsurance();
    if (insurance == null) {
        return "Unknown"; 
    }
    
    return insurance.getName();
}

 

 

이 코드의 경우에는 깊이는 깊어지지 않지만 너무 많은 return 출구가 있어 유지보수가 어렵다는 단점을 가지게 됩니다.

 

 

 

2) null 때문에 발생하는 문제

 

위와 같이 null로 인해서 발생하는 문제로는 아래와 같습니다.

 

  1. 에러의 근원 : NullPointerException 은 자바에서 가장 흔히 발생하는 에러입니다.
  2. 코드를 어지럽힘 : null 체크를 통해 코드가 난잡하고 어지럽게 됩니다.
  3. 아무 의미가 없음 : null은 아무 의미도 표현하지 않으며, 이는 값이 없음을 표현하기에 부적합 합니다.
  4. 자바 철학에 위배된다 : 자바의 경우, 개발자에게 모든 포인터를 숨겼지만 null은 유일하게 포인터를 숨길 수 없었습니다.
  5. 형식 시스템에 구멍을 만든다 : null은 무형식이며 정보를 포함하고 있지 않으므로 모든 참조 형식에 null을 할당 할 수 있고, 이는 결국 위험한 코드를 만들게 됩니다.

 

 

 

 

 

 

 

반응형

 

 

 

 

 

 

 

3. Optional 클래스 소개

 

자바 8 에서는 null 문제를 해결하기 위해 Optional<T> 라는 클래스를 제공합니다.

 

이는 사실상 단순히 null를 위한 Wrapper 클래스입니다.

 

아래는 Optional 를 간단히 보여주는 그림입니다.

 

 

 

모든 null 참조를 Optional로 대치하는것은 바람직하지 않습니다.

Optional은 객체에 대해서 null 체크, null 인 경우 대처를 어떻게 할지를 도와주는 역할입니다.

 

때문에, Optional를 사용한다고 NullPointerException이 나지 않는것은 아닙니다.

 

4. Optional 적용 패턴

 

1) Optional 객체 만들기

 

Optional 도 클래스이기 때문에 객체를 생성해서 사용해야 합니다.

 

1. 빈 Optional

 

아래와 같이 빈 Optional 객체를 생성할 수 있습니다.

 

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

 

2. null이 아닌 값으로 Optional 만들기

 

아래와 같은 기존에 있는 객체로 Optional 객체를 만들 수도 있습니다.

 

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

 

단, 이 경우 인자인 car가 null인 경우 NullPointerException이 발생합니다.

 

 

3. null 값으로 Optional 만들기

 

아래와 같은 방법으로도 Optional 객체를 만들 수 있습니다.

 

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

 

2번의 of 메서드와의 차이점으로는 car가 null인 경우 NullPointerException 가 아닌 빈 Optional을 반환한다는 점입니다.

 

 

2) 맵으로 Optional의 값을 추출하고 변환하기

 

Optional 클래스는 스트림 메서드와 비슷한 map 메서드를 지원합니다.

 

아래와 같이 map 을 사용할 수 있습니다.

 

Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);

 

만약, Optional이 비어있으면 아무일도 일어나지 않습니다.

 

 

3) flatMap으로 Optional 객체 연결

 

스트림 메서드의 flatMap 비슷한 기능의 메서드도 제공하고 있습니다.

 

Optional의 flatMap도 스트림 메서드와 동일하게 Optional<Optional<T>> 와 같은 depth 가 생기는 것을 Optional<T> 로 평준화 해주는 메서드입니다.

 

 

4) Optional로 자동차의 보험회사 이름 찾기

 

그럼 이제 배운 Optional를 사용하여 예제를 해결하게 되면 아래와 같은 코드가 나오게 됩니다.

 

public class Person {
    private Car car;
    public Optional<Car> getCarAsOptional() {
        return Optional.ofNullable(car);
    }
}

public class Car {
    private Insurance insurance;
    public Optional<Insurance> getInsuranceAsOptional() {
        return Optional.ofNullable(insurance);
    }
}

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

public String getCarInsuranceName(Person person) {
    return Optional.of(person)
            .flatMap(Person::getCarAsOptional)
            .flatMap(Car::getInsuranceAsOptional)
            .map(Insurance::getName)
            .orElse("other value");
}

 

Optional 클래스는 필드 형식으로 사용할 것을 가정하지 않아, Serializable 인터페이스를 구현하지 않습니다.
때문에, 위와 같이 get 메서드에만 Optional 를 사용하는것을 권장합니다.

 

5) Optional 스트림 조작

 

자바 9 에서는 Optional 의 스트림 처리를 제공하기 위해 Optional에 stream() 메서드를 제공합니다.

 

아래는 Optional의 stream 함수를 사용한 예제입니다.

 

public Set<String> getCarInsuranceNames(List<Person> persons) {
    return persons.stream()
            .map(Person::getCarAsOptional)
            .map(optCar -> optCar.flatMap(Car::getInsuranceAsOptional))
            .map(optIns -> optIns.map(Insurance::getName))
            .flatMap(Optional::stream)
            .collect(Collectors.toSet());
}

 

stream 메서드를 지원함으로 위와 같이 더욱 간단하게 null 처리를 할 수 있게 되었습니다.

 

하지만, 마지막 결과를 얻기 위해서는 빈 Optional 은 제거하고 있는것은 언랩해야 하는 문제가 있습니다.

 

이는 위 예제에서 Optional::stream 메서드로 해결할 수 있습니다.

stream 메서드는 값이 있는 것 만을 Stream에 담아서 전달하기 때문입니다.

 

 

6) 디폴트 액션과 Optional 언랩

 

아래는 Optional 클래스가 가지고 있는 디폴트 액션입니다.

 

디폴트 액션 설명
get Optional 의 값을 가져오는 메서드입니다.
만약, 값이 없다면 NoSuchElementException이 발생하기 때문에 위험한 메서드입니다.
orElse(T other) orElse는 값이 없는경우 인자인 other를 반환합니다.
orElseGet(Supplier<? extends T> other) orElseGet는 orElse의 게으른 버전입니다.
값이 없는경우에서야 Supplier 를 수행하여 값을 반환하기 때문입니다.
orElseThrow(Supplier<? extends T> exceptionSupplier) orElseThrow는 값이 없는 경우 예외를 발생합니다.
ifPresend(Consumer<? super T> consumer) ifPresend는 값이 존재할때만, 인자의 Consumer를 수행합니다.
ifPresendOrElse(Consumer<? super T> action, Runnable emptyAction) ifPresendOrElse는 자바 9에서 추가된 메서드로,
위의 ifPresent와의 차이점으로는 값이 비어있는 경우 Runnable 인자를 실행한다는 점입니다.

 

7) 필터로 특정값 거르기

 

Optional은 filter 메서드를 지원하고 있습니다.

 

이 메서드는 프레디케이트를 인자로 받으며, Optional 객체가 값을 가지고 있는 경우 프레디 케이트를 적용하고, 값이 없는경우에는 빈 Optional를 반환합니다.

프레디 케이트 적용 결과가 false의 경우에도 빈 Optional 을 반환합니다.

 

아래는 filter 사용 예제 입니다.

 

Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
optInsurance.filter(insurance -> "CabridgeInsurance".equals(insurance.getName()))
            .ifPresent(x -> System.out.println("ok"));

 

6. 마무리

 

이번 포스팅에서는 Chapter11 null 대신 Optional 클래스에 대해 진행하였습니다.

다음에는 Chapter12 새로운 날짜와 시간 API에 대해 포스팅하겠습니다.

 

반응형

+ Recent posts