자바 옵셔널

김명후·2023년 1월 10일
0

1. Java 8 Optional이란?

자바 옵셔널은 자바8에서 처음 도입이 되었고 도입된 이유는 null 때문이다. null은 자바 프로그래머들에게 불편함을 더 많이 제공한다.

class Person {
	private String name;

	// constructor, getter, setter method 생략
}

class House {
	private Person owner;
	private String address;

	// constructor, getter, setter method 생략
}

위 코드를 보자. Person 클래스는 한 사람을 표현한다. HouseownerAddress를 표현할 수 있다.

어떤 개발자가 다음과 같은 코드를 구현했다고 하자.

void main() {
	// 이 메서드는 자신이 가지고 있는 집들 중 아무거나 반환합니다. null 은 나오지 않는다고 가정
	House house = houseService.getRandomHouse(); 
	System.out.println("owner: " + house.getOwner().getName());
	System.out.println("address: " + house.getAddress());
}

집 주인과 주소가 있다면 별 문제 없이 잘 실행될 것이다. 근데 만약 주인이 없는 집이 있다면?

void main() {
	House house = houseService.getRandomHouse();
	System.out.println("onwer: " + house.getOwner().getName()); // java.lang.NullPointerException 발생!
	System.out.println("address: " + house.getAddress());
}

콘솔에 노출하려고할 때 NPE 예외가 일어나서 프로그램이 제대로 수행되지 않을 것이다.

void main() {
	House house = houseService.getRandomHouse();
	if (house.getOnwer() != null) {
		System.out.println("onwer: " + house.getOwner().getName());
	}
	System.out.println("address: " + house.getAddress());
}

null인 경우를 제외시키기 위해 if문을 써서 해결할 수 있지만, 새로운 요구 사항이 생길 때마다 if문을 넣으면 코드가 너무 지저분해진다.

void main() {
	House house = houseService.getRandomHouse();
	if (house.getOwner() != null && house.getOwner().getName() != null) {
		System.out.println("onwer: " + house.getOwner().getName());
	}
	if (house.getAddress() != null) {
		System.out.println("address: " + house.getAddress());
	}
}
if문으로 해결을 하면 당장 해결을 할 수는 있지만 요구사항이 추가될 때마다 코드가 지저분해져서 읽기가 싫어진다.

하지만 Optional을 쓰면 깔끔하게 코드를 작성할 수 있다.

void main() {
	House house = houseService.getRandomHouse();
	Optional.of(house)
        .map(House::getOwner)
        .map(Person::getName)
        .ifPresent(name -> System.out.println("owner:" + name));

	Optional.of(house)
        .map(House::getAdress)
        .ifPresent(address -> System.out.println("address:" + address));
}

여기서 집주인이 없으면 '없음'이라고 나오고, 주소도 없으면 '발급 되지 않음'이라고 나오게 해주세요라는 요구사항이 추가되면?

void main() {
	House house = houseService.getRandomHouse();
	String onwerName = Optional.of(house).map(House::getOnwer).map(Person::getName).orElse("없음");
	String address = Optional.of(house).map(House::getAdress).orElse("발급 되지 않음");

	System.out.println("onwer:" + onwerName);
	System.out.println("address:" + address);
}

위와 같이 코드를 작성할 수 있다. 더 가독성이 좋아졌다.

2. Optional 객체

2.1 Optional 객체 생성

Optional 객체를 생성하기 위해 사용해야 하는 메서드가 있다.

2.1.1 Optional.of

value 값이 null인 경우 NPE 예외를 던진다. 반드시 값이 있어야 하는 경우 사용한다.

// 메서드 시그니처
public static <T> Optional<T> of(T value);
// 예제
Optional<String> opt = Optional.of("result");

2.1.2 Optional.ofNullable

value 값이 null인 경우 비어있는 Optional을 반환한다. 값이 null일 수도 있는 경우 사용한다.

// 메서드 시그니처
public static <T> Optional<T> ofNullable(T value);
// 예제
Optional<String> opt = Optional.ofNullable(null);

2.1.3 Optional.empty

비어있는 옵셔널 객체를 생성한다. 조건에 따라 분기를 태워야하고 반환할 값이 없는 경우에도 사용한다.

// 메서드 시그니처
public static<T> Optional<T> empty();
// 예제
Optional<String> emptyOpt = Optional.empty();

2.2 Optional 중간 처리

옵서녈 객체를 생성한 뒤 사용 가능한 메서드이다. 이 메서드들은 다시 옵셔널을 반환하므로 메서드 체이닝을 통해 원하는 로직을 반복 삽입할 수 있다.

2.2.1 filter

predicate 값이 참이면 해당 필터를 통과시키고 거짓이면 통과시키지 않는다.

// 메서드 시그니처
public Optional<T> filter(Predicate<? super T> predicate);
// 예제
Optional.of("True").filter((val) -> "True".eqauls(val)).orElse("NO DATA"); // "True"
Optional.of("False").filter((val) -> "True".eqauls(val)).orElse("NO DATA"); // "NO DATA"

2.2.2 map

mapper 함수를 통해 입력값을 다른 값으로 변환하는 메서드

// 메서드 시그니처
public<U> Optional<U> map(Function<? super T, ? extends U> mapper);
// 예제
Integer test = Optional.of("1").map(Integer::valueOf).orElseThrow(NoSuchElementException::new); // string to integer

2.2.3 flatMap

mapper 함수를 통해 입력값을 다른 값으로 변환하는 메서드이다. map() 메서드와 다른 점은 메서드 시그니처 매개변수이다. map()에서는 제네릭으로 U를 정의했지만 flatMap()에서는 Optional(U)를 정의했다.

즉, flatMap() 메서드가 반환해야 하는 값은 Optional이라는 것이다.

// 메서드 시그니처
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper);
// 예제
String result = Optional.of("result")
        .flatMap((val) -> Optional.of("good"))
        .get();
System.out.println(result); // print 'good'

2.2.4 or (java9)

기본값을 제공할 수 있는 공급자 함수를 정의할 수 있다. .orElseGet() 과 유사하지만 다른 점은 중간에 체이닝을 통해 우선 순위를 결정할 수 있다. .or() 연산 중에 비어있는 옵셔널이 된다면 다음 .or() 메서드로 진행하게 된다.

// 메서드 시그니처
public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier);
// 예제
String result = Optional.ofNullable("test")
        .filter(value -> "filter".equals(value))
        .or(Optional::empty)
        .or(() -> Optional.of("second"))
        .orElse("final");
System.out.println(result); // print 'second'

2.2.5 stream (java9)

이 메서드는 기존 자바8에서 옵셔널 객체가 바로 스트림 객체로 전환되지 않아 불편했던 부분을 해소시켜준다.

// 메서드 시그니처
public Stream<T> stream();
// 예제
List<String> result = List.of(1, 2, 3, 4)
    .stream()
    .map(val -> val % 2 == 0 ? Optional.of(val) : Optional.empty())
    .flatMap(Optional::stream)
    .map(String::valueOf)
    .collect(Collectors.toList());
System.out.println(result); // print '[2, 4]'

2.3 Optional 종단 처리

종단 처리는 옵셔널 객체의 체이닝을 끝낸다는 것이다.

2.3.1 ifPresent

최종적으로 연산을 끝낸 후 값이 비어있지 않다면 입력값으로 주어진다. 이 값을 가지고 원하는 작업을 수행하면 된다. 하지만 연산을 하다 비어있는 옵셔널 객체를 받게 되면 .ifPresent() 메서드의 내용을 수행하지 않는다.

// 메서드 시그니처
public void ifPresent(Consumer<? super T> consumer);
// 예제1
Optional.of("test").ifPresent((value) -> {
	// something to do
});
// 예제2 (ifPresent 미수행)
Optional.ofNullable(null).ifPresent((value) -> {
	// nothing to do
});

2.3.2 isPresent

최종적으로 연산을 끝낸 후 객체가 존재하는지 여부를 판별한다.

// 메서드 시그니처
public boolean isPresent();
// 예제
Optional.ofNullable("test").isPresent(); // true
Optional.ofNullable("test").filter((val) -> "result".eqauls(val)).isPresent(); // false

2.3.3 get

최종적으로 연산을 끝낸 후 객체를 꺼낸다. 이 때, 비어있는 옵셔널 객체였다면 예외가 발생한다.

// 메서드 시그니처
public T get();
// 예제
Optional.of("test").get(); // 'test'
Optional.ofNullable(null).get(); // NoSuchElementException!

2.3.4 orElse

최종적으로 연산을 끝낸 후에도 옵셔널 객체가 비어있다면 기본값으로 제공할 객체를 지정한다.

// 메서드 시그니처
public T orElse(T other);
// 예제
String result = Optional.ofNullable(null).orElse("default");
System.out.println(result); // print 'default'

2.3.5 orElseGet

최종적으로 연산을 끝낸 후에도 옵셔널 객체가 비어있다면 기본값으로 제공할 공급자 함수를 지정한다.

// 메서드 시그니처
public T orElseGet(Supplier<? extends T> other);
// 예제
String result = Optional.ofNullable("input").filter("test"::equals).orElseGet(() -> "default");
System.out.println(result); // print 'default'

2.3.6 orElseThrow

최종적으로 연산을 끝낸 후에도 옵셔널 객체가 비어있다면 예외 공급자 함수를 통해 예외를 발생시킵니다.

// 메서드 시그니처
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X;
// 예제
Optional.ofNullable("input").filter("test"::equals).orElseThrow(NoSuchElementException::new);

2.3.7 ifPresentOrElse (java 9)

ifPresent 와 유사하지만 한가지 매개변수를 더 받을 수 있다. 첫번째 매개변수인 action은 유효한 객체를 받을 경우 실행하고, 두번째 매개변수인 emptyAction은 유효한 객체를 받지 못한 경우에 실행한다.

// 메서드 시그니처
public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction);
// 예제
Optional.ofNullable("test")
    .ifPresentOrElse(value -> System.out.println(value), () -> System.out.println("null")); // print 'test'
Optional.ofNullable(null)
    .ifPresentOrElse(value -> System.out.println(value), () -> System.out.println("null")); // print 'null'

2.3.8 orElseThrow (java 10)

매개변수가 필요없는 예외 메서드를 추가했다. 이 메서드를 사용하면 기존에는 아래처럼 호출해야 했던 코드를 간결하게 해준다.

// 메서드 시그니처
public T orElseThrow();
// 예제 (자바 8)
Optional.ofNullable(something).orElseThrow(NoSuchElementException::new);
// 예제 (자바 10)
Optional.ofNullable(something).orElseThrow();

정리

Optional 시작 연산자

메서드설명
of()value 값이 null인 경우 NPE 예외를 던진다. 반드시 값이 있어야 하는 경우 사용한다.
ofNullable()value 값이 null인 경우 비어있는 Optional을 반환한다. 값이 null일 수도 있는 경우 사용한다.
empty비어있는 옵셔널 객체를 생성한다. 조건에 따라 분기를 태워야하고 반환할 값이 없는 경우에도 사용한다.

Optional 중간 처리

메서드설명
filterpredicate 값이 참이면 해당 필터를 통과시키고 거짓이면 통과시키지 않는다.
mapmapper 함수를 통해 입력값을 다른 값으로 변환하는 메서드
flatMap  apper 함수를 통해 입력값을 다른 값으로 변환하는 메서드이다.
or기본값을 제공할 수 있는 공급자 함수를 정의할 수 있다. .orElseGet() 과 유사하지만 다른 점은 중간에 체이닝을 통해 우선 순위를 결정할 수 있다. .or() 연산 중에 비어있는 옵셔널이 된다면 다음 .or() 메서드로 진행하게 된다.
stream  이 메서드는 기존 자바8에서 옵셔널 객체가 바로 스트림 객체로 전환되지 않아 불편했던 부분을 해소시켜준다.

Optional 종단 처리

메서드설명
ifPresent최종적으로 연산을 끝낸 후 값이 비어있지 않다면 입력값으로 주어진다. 이 값을 가지고 원하는 작업을 수행하면 된다. 하지만 연산을 하다 비어있는 옵셔널 객체를 받게 되면 .ifPresent() 메서드의 내용을 수행하지 않는다.
isPresent최종적으로 연산을 끝낸 후 객체가 존재하는지 여부를 판별한다.
get최종적으로 연산을 끝낸 후 객체를 꺼낸다. 이 때, 비어있는 옵셔널 객체였다면 예외가 발생한다.
orElse최종적으로 연산을 끝낸 후에도 옵셔널 객체가 비어있다면 기본값으로 제공할 객체를 지정한다.
orElseGet최종적으로 연산을 끝낸 후에도 옵셔널 객체가 비어있다면 기본값으로 제공할 공급자 함수를 지정한다.
orElseThrow    최종적으로 연산을 끝낸 후에도 옵셔널 객체가 비어있다면 예외 공급자 함수를 통해 예외를 발생시킵니다.
ifPresentOrElse     ifPresent 와 유사하지만 한가지 매개변수를 더 받을 수 있다. 첫번째 매개변수인 action은 유효한 객체를 받을 경우 실행하고, 두번째 매개변수인 emptyAction은 유효한 객체를 받지 못한 경우에 실행한다.
orElseThrow매개변수가 필요없는 예외 메서드를 추가했다. 이 메서드를 사용하면 기존에는 아래처럼 호출해야 했던 코드를 간결하게 해준다.

출처

0개의 댓글