Skip to content

Latest commit

 

History

History
303 lines (220 loc) · 10.5 KB

00.Optional 정리.md

File metadata and controls

303 lines (220 loc) · 10.5 KB

이분의 블로그를 따라쓰면서 정리한 글입니다.
http://homoefficio.github.io/2019/10/03/Java-Optional-%EB%B0%94%EB%A5%B4%EA%B2%8C-%EC%93%B0%EA%B8%B0/

Optional 의 정의

Optional은 단순하게 null을 방지하기 위한 목적이 아니라, 반환 값이 없을 수도 있는 , 즉 null이 될 수도 있는 객체를 감싸고 있는 일종의 wrapper 클래스라고 볼 수 있습니다.

메서드가 반환할 값이 '없음'을 명확하게 표현할 필요가 있고, null을 반환하면 에러가 날만한 method 반환 type 으로 optional을 사용하자! 가 주목적.

Optional의 효과는 다음과 같습니다.

  • NPE를 유발할 수 있는 null을 직접 다루지 않아도 됩니다.
  • null 체크를 일일이 하거나, 명시적으로 이 변수가 null 가능성이 있음을 표현할 수 있습니다.

Optional 객체는 3가지 정적 팩토리 메소드를 제공하고 있습니다.

Optional<Post> post = Optional.empty(); // null 값을 가진 빈 optional객체

Optional<Post> post = Optional.of(aPost); // 객체를 담고 있는 optional객체
=> null이 들어오면 NPE 발생

=> null이 들어오면 .empty와 같은 옵셔널 객체를 가져온다.
Optional<Post> post = Optional.ofNullable(aPost);
Optional<Post> post = Optional.ofNullable(null);
// 해당 객체가 null이 아닌지 모른다면 이거 사용

Optional 객체에 접근하기

optional 클래스가 담고 있는 객체를 가져오기 위한 인스턴스 메소드들.

가져올 객체가 null일 경우 비어있는 Optional 객체에 대해.....

  • get() : NoSuchElementException 던짐
  • orElse(T other) : 넘어온 인자를 반환
  • orElseGet(Supplier<? extends T> other) : 넘어온 함수형 인자를 통해 생성된 객체를 반환. orElse의 lazy 버전. 비어있는 경우에만 함수 호추루
  • orElseThrow(Supplier<? extends X> exceptionSupplier) : 넘어온 함수형 인자를 통해 생성된 예외를 던짐

Optional 적용을 하고 나면 null 체크를 할 필요가 없다.

if..else....분기처리를 한 줄로 처리할 수 있다.

int length = Optional.ofNullable(getText()).map(String::length).orElse(0);

null이 아니면, getText()를 한다음 처리, orElse 아니면 0으로 초기세팅을 하라.

올바르게 Optional 사용하기

optional을 제대로 사용하기 위해서는, 최대 1개의 원소를 갖고 있는 특별한 stream이라고 이해하면 좋습니다. stream 클래스가 가지고 있는 map(), flatMap(), filter()도 Optional이 갖고 있고, 비슷한 사상을 가지고 있습니다.

/* 주문을 한 회원이 살고 있는 도시를 반환한다 */
public String getCityOfMemberFromOrder(Order order) {
	return Optional.ofNullable(order)
			.map(Order::getMember)
			.map(Member::getAddress)
			.map(Address::getCity)
			.orElse("Seoul");
}
  • ofNullable() 정적 팩토리 메소드를 통해 order 객체를 Optional로 감싸주었습니다.
  • map() 메소드를 연쇄 호출하여 Optional 객체를 3번 변환하였습니다. 안에 담긴 order 객체의 타입을 바꿔주는 작업입니다.
  • 마지막으로 orElse()를 호출해서 전 과정에서 얻은 Optional이 비어있다면 디폴트로 사용할 도시 이름을 세팅해주었습니다.

** map() filter() 사용

filter() 메소드를 사용하면 if문 없이도 메소드 연쇄호출만으로도 읽기 편한 코드를 작성할 수 있습니다.

이전 코드

public Member getMemberIfOrderWithin(Order order, int min) {
	if (order != null && order.getDate().getTime() > System.currentTimeMillis() - min * 1000) {
		return order.getMember();
	}
}
필터 사용

public Optional<Member> getMemberIfOrderWithin(Order order, int min) {
	return Optional.ofNullable(order)
			.filter(o -> o.getDate().getTime() > System.currentTimeMillis() - min * 1000)
			.map(Order::getMember);
}
  1. null 반환

map 인터페이스의 get() 메소드를 ⇒ Optional로 감싸주면 null-safe 코드를 만들 수 있습니다. 도시 문자열 길이를 반환하고 디폴트 값을 설정해주는 과정에서 null check를 편하게 할 수 있습니다.

String city = cities.get(4); // returns null
int length = city == null ? 0 : city.length(); // null check
System.out.println(length);
Optional<String> maybeCity = Optional.ofNullable(cities.get(4)); // Optional
int length = maybeCity.map(String::length).orElse(0); // null-safe
System.out.println("length: " + length);
  1. 예외 발생

null을 반환하는 게 아니라 예외를 던져 처리하는 경우입니다. List 인터페이스에서 get() 메소드는 인덱스에 해당하는 값이 없으면 ArrayIndexOutOfBoundsException을 던지는데, try catch null check을 하느라 여간 귀찮은게 아닙니다.

List<String> cities = Arrays.asList("Seoul", "Busan", "Daejeon");
String city = null;
try {
	city = cities.get(3); // throws exception
} catch (ArrayIndexOutOfBoundsException e) {
	// ignore
}
int length = city == null ? 0 : city.length(); // null check
System.out.println(length);

이런 경우는 예외 처리부를 정적 유틸리티 메소드로 분리하면 좋습니다. Optional 클래스의 정적 팩토리 메소드를 사용하여 정상처리/ 예외 처리 시 반환할 Optional 객체를 지정해줍니다.

public static <T> Optional<T> getAsOptional(List<T> list, int index) {
	try {
		return Optional.of(list.get(index)); // 해당 인덱스의 값을 가져옴
	} catch (ArrayIndexOutOfBoundsException e) {
		return Optional.empty(); // null을 가진 optional 객체 
	}
}

1) isPresent .get() 보다 orElse, orElseGet(), orElseThrow() 사용

BAD

Optional<Member> member = ...;
if (member.isPresent()) {
    return member.get();
} else {
    return null;
}
GOOD

Optional<Member> member = ...;
return member.orElse(null);

Optional<Member> member = ...;
return member.orElseThrow(() -> new NoSuchElementException());

2) orElse 보다는 orElseGet( () → new) 를 쓰자

아까 적었다싶이, orElse는 값이 있든 없든 무조건 실행됩니다.

따라서 새로운 객체를 생성하거나 새로운 연산을 수행해야하는 경우에는 orElseGet()을 쓰는 것이 좋습니다.

Optional에 값이 있을 때 orElse()의 인자는 무시되기 때문에, orElse()는 이미 생성된 객체나 이미 계산된 값일때만 사용합니다.

orElseGet()은 Optional에 값이 없을 때만 실행됩니다. 그래서 불필요한 오버헤드가 없습니다.

BAD

Optional<Member> member = ...;
return member.orElse(new Member());
GOOD

Optional<Member> member = ...;
return member.orElseGet(Member::new);  // member에 값이 없을 때만 new Member()가 실행됨

Member EMPTY_MEMBER = new Member();
...
Optional<Member> member = ...;
return member.orElse(EMPTY_MEMBER);// 이미 생성됐거나 계산된 값은 orElse()를 사용해도 무방

3) 값만 얻을거면!!! Optional 대신 null 비교

// 안 좋음
return Optional.ofNullable(status).orElse(READY);

// 좋음
return status != null ? status : READY;

4) Optional 대신 빈 컬렉션 반환하기(JPA)

컬렉션은 null이 아니라 비어있는 컬렉션을 반환하는 것이 좋을 때가 많다. 따라서 Optional 로 감싸는 것을 경계할 것

// 안 좋음
List<Member> members = team.getMembers();
return Optional.ofNullable(members);

// 좋음
List<Member> members = team.getMembers();
return members != null ? members : Collections.emptyList();

마찬가지로 JPARepoitory에서도 null 이 아니라 빈배열같이 비어있는 컬렉션을 반환하므로 Optional로 감쌀 필요가 없다.

5) Optional은 필드로 사용 금지

// 안 좋음
public class Member {

    private Long id;
    private String name;
    private Optional<String> email = Optional.empty();
}

6) Optional은 생성자, 메서드 인자, 컬렉션 원소로 사용금지

method 인자로 사용하면 API가 호출될 때마다 Optional을 생성해서 인자로 전달되어야 한다. 그렇기 때문에 호출하고 나서 null 체크를 남겨두는 편이 좋다. 인자를 받을 때부터 null 체크를 하는 것이 아닌, 코드 상에서 null 체크를 하는 것이 더 좋다는 뜻.

// 안 좋음
public class HRManager {
    
    public void increaseSalary(Optional<Member> member) {
        member.ifPresent(member -> member.increaseSalary(10));
    }
}
hrManager.increaseSalary(Optional.ofNullable(member));

// 좋음
public class HRManager {
    
    public void increaseSalary(Member member) {
        if (member != null) {
            member.increaseSalary(10);
        }
    }
}
hrManager.increaseSalary(member);

컬렉션에는 다양한 원소가 들어갈 수 있지만, Optional은 비싼 놈이기 때문에 원소를 꺼내거나 사용할 때 null 체크를 하는 것이 좋다. Map같은 경우 null 체크가 들어간 메소드를 제공하기 때문에 Map에서 제공하는 메서드를 활용하는 것이 더 낫다.

// 안 좋음
Map<String, Optional<String>> sports = new HashMap<>();
sports.put("100", Optional.of("BasketBall"));
sports.put("101", Optional.ofNullable(someOtherSports));
String basketBall = sports.get("100").orElse("BasketBall");
String unknown = sports.get("101").orElse("");

// 좋음
Map<String, String> sports = new HashMap<>();
sports.put("100", "BasketBall");
sports.put("101", null);
String basketBall = sports.getOrDefault("100", "BasketBall");
String unknown = sports.computeIfAbsent("101", k -> "");

7) of(), ofNullable() 혼동 주의

of(x)는 x가 null 이 아닌것이 확실할때만 사용해야함. 아니면 NPE

ofNullable()은 x가 null일 수도 있을 때만 사용해야함.

// 안 좋음
return Optional.of(member.getEmail());  // member의 email이 null이면 NPE 발생

// 좋음
return Optional.ofNullable(member.getEmail());

// 안 좋음
return Optional.ofNullable("READY"); <- 확실하게 있는 값이니까....

// 좋음
return Optional.of("READY");

9) Optional 대신 OptionalInt, OptionalLong, OptionalDouble

Option에 담길 값이 int, long, double 같은 숫자라면 Optional로 감싸지 말고 위에처럼 사용하자.

// 안 좋음
Optional<Integer> count = Optional.of(38);  // boxing 발생
for (int i = 0 ; i < count.get() ; i++) { ... }  // unboxing 발생

// 좋음
OptionalInt count = OptionalInt.of(38);  // boxing 발생 안 함
for (int i = 0 ; i < count.getAsInt() ; i++) { ... }  // unboxing 발생 안 함