[이펙티브자바] item55. 옵셔널 반환은 신중히 하라

wally·2022년 8월 28일

1. 반환할값이 없을 때 처리

자바 8 이전

  1. 예외를 던져라
  • 예외는 진짜 예외적인 상황에만 사용되어야 한다.
  • 예외 생성 시 스택 추적 전체를 캡처하므로 비용이 크다
  1. null 를 반환하라.
  • 별도의 null 처리 코드를 추가해야 한다.
  • null 처리를 무시하면 반환된 null 값이 어딘가 저장되고 언젠가 NullPointerException 을 발생시킨다.

자바 8 이후

  1. Optional 활용 - Optional<T>
  • 옵셔널은 원소를 최대 1개 가질 수 있는 '불변' 컬렉션이다.

    Optional<T>Collection<T> 를 구현하지는 않았지만 원칙적으로 그렇게 부른다고 한다.

  • 보통 T 를 반환하고 특정조건(null 이 존재할 때)에서는 Optional<T> 를 반환한다.
Optional<Object> empty = Optional.empty();
Optional<Object> something = Optional.of(new Object());
/////////////////////
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
    if (c.isEmpty())
        return Optional.empty();
        // Optional<E> 를 반환(비어있는)
    E result = null;
    for (E e : c) 
        if (result == null || e.compareTo(result) > )
            result = Objects.reuqireNonNull(e);
    return Optional.of(result);
    // Optional<E> 를 반환(채워있는)
}
  • 위의 코드와 같이 옵셔널을 반환하는 메서드에서는 절대 null 를 반환하지 않게 설계하자

2. Stream 과 Optional

  • Optional 를 유용하게 사용할려면 Optional를 최대 1개의 원소를 가지고 있는 특별한 Stream 으로 생각하면 좋다.
  • Optional 과 Stream 클래스 간의 직접적인 구현이나 상속은 없으나 사용 방법과 기본 사상이 매우 유사하다.
  • Stream 클래스가 가지고 있는 map()이나 flatMap(), filter()와 같은 메소드를 Optional도 가지고 있다

map

/* 주문을 한 회원이 살고 있는 도시를 반환한다 */
public String getCityOfMemberFromOrder(Order order) {
	return Optional.ofNullable(order)
			.map(Order::getMember)
			.map(Member::getAddress)
			.map(Address::getCity)
			.orElse("Seoul");
}
  • Order 객체가 null 인 경우를 대비하여 of() 대신 ofNullable() 사용
  • map 의 연쇄 사용을 통해 Optinal 객체를 3번 변환
  • Optional<Order> -> Optional<Member> -> Optional<Address> -> Optional<String>
  • orElse() 메소드를 호출하여 Optional이 비어있을 경우, 디폴트로 사용할 서울을 세팅

Filter

public Optional<Member> getMemberIfOrderWithin(Order order, int min) {
	return Optional.ofNullable(order)
			.filter(o -> o.getDate().getTime() > System.currentTimeMillis() - min * 1000)
			.map(Order::getMember);
}
  • filter() 메소드는 넘어온 함수형 인자의 리턴 값이 false인 경우, Optional을 비우게 되고 그 이후 메소드 호출은 의미가 없어지게 된다.
  • filter 조건에 맞지 않은경우 빈 Optional 로 반환될 것이다.

FlatMap

		Optional<Optional<String>> map1 = Optional.of(Optional.of("STRING"));
		Optional<Optional<String>> map2 = Optional.of("string")
			.map(s -> Optional.of("STRING"));

		Optional<String> flatmap1 = Optional.of("STRING");
		Optional<String> flatmap2 = Optional.of("string")
			.flatMap(s -> Optional.of("STRING"));
  • 로직에서 Optional 이 연속적으로 리턴되는 경우에 flatMap 을 사용해야 한다.
  • map() 은 결과를 Optional 로 감싸서 리턴하는 반면, flatMap() 은 그냥 반환한다.

  • Optional 은 반환값이 없을 수 있음을 API 사용자에게 명확히 알려준다.
  • 메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못했을때 취할 행동을 선택해야 한다.
  • 기본값 설정을 해보자

orElse()

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

orElseThrow()

Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
  • 위 코드는 실제 예외가 아닌 예외 팩터리를 건넨다.
  • 예외가 실제로 발생하지 않는 한 예외 생성 비용은 들지 ㅇ낳는다.

get()

  • get() : Optional 에 항상 값이 채워져 있다 확신할때 곧바로 값을 꺼낸다. 없을때 NoSuchElementException 이 발생한다. 따라서 ifPresent() 와 같이 쓰이기도 한다.
parentProcess.ifPresent() ? String.valueOf(parentProcess.get().pid()) : "N/A"));

orElseGet()

  • orElseGet() : orElseGet은 null일 때만 불린다

orElse는 null이던말던 항상 불립니다. - 의도치 않은 함수를 호출할 수 있다!!
orElseGet은 null일 때만 불립니다.
차이점을 꼭 기억하자! -> 플젝때 orElse로 했다가 배포중에 에러가 터진기억이.. ㅠㅠ

    public void ohMyGod() {
        String username = null;
        String result1 = Optional.ofNullable(username).orElse(getDefaultName());
        System.out.println(result1);

        String result2 = Optional.ofNullable(username).orElseGet(() -> getDefaultName());
        System.out.println(result2);
    }

	private String getDefaultName() {
		// 어떤 로직
		return "new value";
	}
  • orElse의 경우는 "값"을 취한다
  • orElseGet은 "Supplier"를 취한다.
  • 즉 Optional 안에 값이 null 인 경우에만 호출되는 경우는 orElseGet 이고 null 이든 아니든 무조건 orElse 는 실행이된다.
  • 성능이 중요한 경우에는 치명적일 수 있다.
  • 만약 orElse 안에 로직이 존재하여 로직이 실행된다면?
    • orElse 의 경우 null 이든 아니든 항상 로직을 실행시키므로 의도치 않게 에러가 뜰것이다!!
    • 중요!!!!!! 언젠가 한번 오잉 왜 에러가 뜨지 하는 일이 생긴다면 한번 orElse 를 돌아보세용

3. 항상 옵셔널일 좋은건 아니다

  1. 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안된다
  • Optional<List<T>> 보다는 그냥 비어있는 List<T> 를 반환하는게 좋다.
  • 결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 할때 Optional<T> 를 반환하자
  1. Optional 도 새로 할당 후 초기화해야 하는 객체이고 그안에서 값을 꺼내는 단계를 거치므로 성능이 중요할 때는 옵셔널이 좋지 않을 수 있다.

  2. 박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무겁다

  • 따라서 OptionalInt, OptionalLong, OptionalDouble 을 제공한다.
  1. 맵의 값으로 옵셔널을 사용하면 안된다.
  • 맵 안에 키가 없다는 사실을 나타내는 방법이 2개가 되면서 쓰잘데기 없이 복잡하고 난해해진다.

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

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

  • 이런 상황 대부분은 필수 필드를 갖는 클래스와, 이를 확장해 선택적 필드를 추가한 하위 클래스를 따로 만들어야 함을 암시하므로 코드스멜이 진하게 난다.
  • 만약 필드 대부분이 필수가 아니고 기본 타입이 값이 없을을 나타내기 어려울때 적용시킬수는 있을 수 있다.
public class EffectiveTest {
	@Test
	void testtest () {
		String name = "1";
		Long age = 1L;
		TestMember member = new TestMember(1L, Optional.ofNullable(name), Optional.ofNullable(age));
	}
	
	static class TestMember{
		Long id;
		Optional<String> name;
		Optional<Long> age;

		public TestMember(Long id, Optional<String> name, Optional<Long> age) {
			this.id = id;
			this.name = name;
			this.age = age;
		}
	}
}
  • 직접 코드를 짜봤는데 그냥 뭔가 자꾸 감싸니까 더 복잡하고 지저분하다
  • 인스턴스 필드에 사용할 일은 앞으로도 없을 거 같다.

참고사이트

profile
클린코드 지향

0개의 댓글