Optional

3 분 소요

import java.util.Optional;
import java.util.Random;

public class Main {

	static class User {
		private int num;
		
		public User() {
			System.out.println("User() called.");
		}
		
		public User(int num) {
			this.num = num;
			System.out.println("User(int num) called.");
		}
		
		public void printNum() {
			System.out.println(this.num);
		}
	}
	
	static Optional<User> randomMakeUserOrNull() {
		Random rand = new Random();
		int k = rand.nextInt(10);
		Optional<User> user = null;
		if ((k & 1) == 1) {
			user = Optional.of(new User(10));
			return user;
		} else {
			user = Optional.empty();
		}
		return user;
	}
	
	// Code 1
	static User getUserVer1() {
		Optional<User> user = randomMakeUserOrNull();
		if (user.isPresent()) {
			return user.get();
		} else {
			return new User();
		}
	}
	
	// Code 2
	static User getUserVer2() {
		Optional<User> user = randomMakeUserOrNull();
		return user.orElse(new User());
	}
	
	// Code 3
	static User getUserVer3() {
		Optional<User> user = randomMakeUserOrNull();
		return user.orElseGet(() -> new User());
	}

	public static void main(String[] args) {
		User user = getUserVer2();
		user.printNum();
	}
}

Optional - isPresent, orElse

Code 1과 Code 2에 대해서

메소드 설명

randomMakeUserOrNull()

  • Optional<User>를 반환하는 메소드이다.
    • randomMakeUserOrNull() 를 통해 생성되는 객체에서 num 값은 항상 10인 상태로 생성된다.
// Code 1
static User getUserVer1() {
	Optional<User> user = randomMakeUserOrNull();
	if (user.isPresent()) { // user 객체가 존재하는지 확인한다.
		return user.get();
	} else {
		return new User();
	}
}

// Code 2
static User getUserVer2() {
  // User 객체가 존재하면 해당 객체를 반환하고
	// 그렇지 않으면 새로운 객체를 orElse(new User())를 통해 생성된 객체를 반환한다.
	Optional<User> user = randomMakeUserOrNull();
	return user.orElse(new User());
}
  • Code 1보다는 Code 2가 더 깔끔하고 if-else 분기를 제거할 수 있다.

주의점

orElse(…)에서 …는 항상 실행된다.

주의할 점으로는 Code 1Code 2가 완전히 동일하지는 않다는 것이다. user.orElse(new User()) 는 user 변수가 객체를 담고 있지 않는 경우 orElse(new User())를 통해 새로운 User 객체를 생성하여 반환하는 것으로 오해할 수 있다. Code 1은 if-else를 사용하여 Optional이 비어있는지 확인하고 비어있는 경우에만 new User()를 호출하는 반면, orElse는 Optional이 비어있든 비어있지 않든 항상 new User()를 호출한다.

실제 호출해보면 getUserVer2()에서 user가 randomMakeUserOrNull() 반환값으로 비어있지 않은 옵셔널을 참조하고 있는 상태에서도 디폴트 생성자는 항상 호출된다.

User(int num) called.
User() called.
10
  • 구체적으로 설명하면 getUserVer2()가 호출되었을 때 randomMakeUserOrNull() 메소드가 num 값이 10인 객체를 생성했을지라도, 객체를 담고 있지 않을 때 반환할 기본값을 마련하기 위해 orElse(new User())에서 new User()가 호출되어 default 생성자가 함께 호출된다.

orElse, orElseGet

Code 2와 Code 3에 대해서

// Code 2
static User getUserVer2() {
	Optional<User> user = randomMakeUserOrNull();
	return user.orElse(new User());
}

// Code 3
static User getUserVer3() {
	Optional<User> user = randomMakeUserOrNull();
	return user.orElseGet(() -> new User());
}

이전까지의 예제를 통해 orElse(...)에서 ...부분은 항상 먼저 수행된다는 것을 확인하였다.

Optional 이 객체를 갖고 있는 경우에는 곧바로 반환하면 되기 때문에 항상 새로운 객체를 생성해두는 것은 오버헤드가 될 수 있다.

이것을 방지하기 위해 Code 3에서처럼 orElseGet(...)을 사용한다.

이때는 Optional 객체가 비어있는 경우에만 new User()를 호출하여 새로운 객체를 생성한다.

Effective Java 3/E

Item 55. 옵셔널 반환은 신중히 하라

빈 옵셔널은 Optional.empty()로 만들고, 값이 든 옵셔널은 Optional.of(value)로 생성한다.

Optional.of(value)에 null을 넣으면 NullPointerException을 던지니 주의한다.

null 값도 허용하는 옵셔널을 만들려면 Optional.ofNullable(value)를 사용하면 된다.

옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말자. 이것은 옵셔널을 도입한 취지를 완전히 무시하는 행위다.

메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못했을 때 취할 행동을 선택해야 한다. 그중 하나는 기본값을 설정하는 방법이다.

String lastWordInLexicon = max(words).orElse("단어 없음...");

또는 상황에 맞는 예외를 던질 수 있다. 다음 코드에서 실제 예외가 아니라 예외 팩터리를 건넨 것에 주목하자. 이렇게 하면 예외가 실제로 발생하지 않는 한 예외 생성 비용은 들지 않는다.

Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

옵셔널에 항상 값이 채워져 있다고 확신한다면 그냥 곧바로 값을 꺼내 사용하는 선택지도 있다. 다만 잘못 판단한 것이라면 NoSuchElementException이 발생할 것이다.

Element lastNobleGas = max(Elements.NOBLE_GASES).get();

이따금 기본값을 설정하는 비용이 아주 커서 부담이 될 때가 있다. 그럴 때는 Supplier를 인수로 받는 orElseGet을 사용하면, 값이 처음 필요할 때 Supplier를 사용해 생성하므로 초기 설정 비용을 낮출 수 있다.

옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거이 없다.

예를 들어 옵셔널을 맵의 값으로 사용하면 안된다. 만약 그렇게 되면 맵 안에 키가 없다는 사실을 나타내는 방법이 두 가지가 된다. 하나는 키 자체가 없는 경우고, 다른 하나는 키는 있지만 그 키가 속이 빈 옵셔널인 경우다. 복잡성을 높여 혼란과 오류 가능성을 키울 뿐이다.

옵셔널을 인스턴스 필드에 저장해두는 게 필요할 때가 있을까?

이런 상황의 대부분은 필수 필드를 갖는 클래스와, 이를 확장해 선택적 필드를 추가한 하위 클래스를 따로 만들어야 함을 암시하는 “Bad smell”이다.

하지만 가끔은 적절한 상황도 있다. 예를 들어 어떤 A라는 클래스 인스턴스의 필드 중 상당수는 필수가 아니다. 또한 그 필드들은 기본 타입이라 값이 없음을 나타낼 방법이 마땅치 않다. 이러한 경우라면 선택적 필드의 getter에 메서드들이 옵셔널을 반환하게 해주면 좋을 것이다. 따라서 이럴 때는 필드 자체를 옵셔널로 선언하는 것도 좋은 방법이다.

태그:

카테고리:

업데이트: