Java Optional 바르게 쓰기

Brian Goetz는 스택오버플로우에서 Optional을 만든 의도에 대해 다음과 같이 말했다.

… it was not to be a general purpose Maybe type, as much as many people would have liked us to do so. Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result” …

Optional은 많은 사람들이 우리(자바 언어 설계자)에게 기대했던 범용적인 Maybe 타입과는 다르다. 라이브러리 메서드가 반환할 결과값이 ‘없음’을 명백하게 표현할 필요가 있는 곳에서 제한적으로 사용할 수 있는 메커니즘을 제공하는 것이 Optional을 만든 의도였다.

뭔 소린지 아리까리하지만 요는 반환값이 ‘없음’을 나타내는 것이 주목적이며, (이유야 있겠지만) 사람들이 기대하는 것과는 다르게 만들었다는..
그럼에도 불구하고 사람들은 기대했던 대로 사용해버려서 주의사항이 26가지나 되었.. (의도와 다른 방식으로 사용되는 것을 허용한 이유는 또 뭘까..)

참고로 Java9에는 Brian Goetz가 설명한 의도가 다음과 같이 API Note라는 형식으로 공식 API 문서에도 포함되었다. (알려주신 김인태님 감사드립니다!)

API Note:
Optional is primarily intended for use as a method return type where there is a clear need to represent “no result,” and where using null is likely to cause errors. A variable whose type is Optional should never itself be null; it should always point to an Optional instance.

메서드가 반환할 결과값이 ‘없음’을 명백하게 표현할 필요가 있고, null을 반환하면 에러를 유발할 가능성이 높은 상황에서 메서드의 반환 타입으로 Optional을 사용하자는 것이 Optional을 만든 주된 목적이다. Optional 타입의 변수의 값은 절대 null이어서는 안 되며, 항상 Optional 인스턴스를 가리켜야 한다.

어쨌든 원래 의도에 맞게 쓰는 것이 가급적 해가 없을 것이고, 우리는 우리가 만드는 시스템에 해를 끼치지 말아야 한다. 그래서 Optional 사용 시 무심결에 잘못 사용하는 안티패턴과 올바른 사용법을 자바8 기준으로 갈무리해봤다.

1. isPresent()-get() 대신 orElse()/orElseGet()/orElseThrow()

이왕에 비싼 Optional 쓰기로 한 거 코드라도 줄이자. 설명보다 그냥 코드를 보는 게 훨씬 낫다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 안 좋음
Optional<Member> member = ...;
if (member.isPresent()) {
return member.get();
} else {
return null;
}

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



// 안 좋음
Optional<Member> member = ...;
if (member.isPresent()) {
return member.get();
} else {
throw new NoSuchElementException();
}

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

2. orElse(new ...) 대신 orElseGet(() -> new ...)

orElse(...)에서 ...Optional에 값이 있든 없든 무조건 실행된다. 따라서 ...가 새로운 객체를 생성하거나 새로운 연산을 수행하는 경우에는 orElse() 대신 orElseGet()을 써야한다.

이거 사실 생각해보면 굉장히 당연한 내용이다. method1(method2())이 실행되면 method2()method1()보다 먼저 그리고 언제나 실행된다. 따라서 orElse(new ...)에서도 new ...가 무조건 실행되는 것이 당연하다.

그런데 아마도 이름 때문이겠지만 묘하게도 무심결에 생각없이 orElse(new ...)를 써보면 new ...Optional에 값이 없을 때만 실행될 것 같은 착각이 든다.

암튼 Optional에 값이 없으면 orElse()의 인자로서 실행된 값이 반환되므로 실행한 의미가 있지만, Optional에 값이 있으면 orElse()의 인자로서 실행된 값이 무시되고 버려진다. 따라서 orElse(...)...가 새 객체 생성이나 새로운 연산을 유발하지 않고 이미 생성되었거나 이미 계산된 값일 때만 사용해야 한다.

orElseGet(Supplier)에서 SupplierOptional에 값이 없을 때만 실행된다. 따라서 Optional에 값이 없을 때만 새 객체를 생성하거나 새 연산을 수행하므로 불필요한 오버헤드가 없다. 물론 람다식이나 메서드참조에 대한 오버헤드는 있겠지만 불필요한 객체 생성이나 연산을 수행하는 것에 비하면 대부분 경미할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 안 좋음
Optional<Member> member = ...;
return member.orElse(new Member()); // member에 값이 있든 없든 new Member()는 무조건 실행됨

// 좋음
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()를 사용해도 무방

참고로 Collections.emptyList()는 호출될 때마다 비어있는 리스트를 반환하는 것이 아니라 이미 생성된 static 변수인 EMPTY_LIST를 반환하므로 orElse(Collections.emptyList())를 써도 괜찮다. 하지만, 이런 용법은 많이 사용되면 orElse(new ...)orElse(연산을유발하는메서드()) 같은 안티 패턴마저 정상적인 사용법으로 인식되게 하는 좋지 않은 착시 효과가 발생할 수 있으므로 orElseGet(Collections::emptyList)를 사용하는 것이 더 좋다.

3. 단지 값을 얻을 목적이라면 Optional 대신 null 비교

Optional은 비싸다. 따라서 단순히 값 또는 null을 얻을 목적이라면 Optional 대신 null 비교를 쓰자.

1
2
3
4
5
// 안 좋음
return Optional.ofNullable(status).orElse(READY);

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

4. Optional 대신 비어있는 컬렉션 반환

Optional은 비싸다. 그리고 컬렉션은 null이 아니라 비어있는 컬렉션을 반환하는 것이 좋을 때가 많다. 따라서 컬렉션은 Optional로 감싸서 반환하지 말고 비어있는 컬렉션을 반환하자.

1
2
3
4
5
6
7
// 안 좋음
List<Member> members = team.getMembers();
return Optional.ofNullable(members);

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

마찬가지 이유로 Spring Data JPA Repository 메서드 선언 시 다음과 같이 컬렉션을 Optional로 감싸서 반환하는 것은 좋지 않다. 컬렉션을 반환하는 Spring Data JPA Repository 메서드는 null을 반환하지 않고 비어있는 컬렉션을 반환해주므로 Optional로 감싸서 반환할 필요가 없다.

1
2
3
4
5
6
7
8
9
// 안 좋음
public interface MemberRepository<Member, Long> extends JpaRepository {
Optional<List<Member>> findAllByNameContaining(String part);
}

// 좋음
public interface MemberRepository<Member, Long> extends JpaRepository {
List<Member> findAllByNameContaining(String part); // null이 반환되지 않으므로 Optional 불필요
}

5. Optional을 필드로 사용 금지

Optional은 필드에 사용할 목적으로 만들어지지 않았으며, Serializable을 구현하지 않았다. 따라서 Optional은 필드로 사용하지 말자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 안 좋음
public class Member {

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

// 좋음
public class Member {

private Long id;
private String name;
private String email;
}

6. Optional을 생성자나 메서드 인자로 사용 금지

Optional을 생성자나 메서드 인자로 사용하면, 호출할 때마다 Optional을 생성해서 인자로 전달해줘야 한다. 하지만 호출되는 쪽, 즉 api나 라이브러리 메서드에서는 인자가 Optional이든 아니든 null 체크를 하는 것이 언제나 안전하다. 따라서 굳이 비싼 Optional을 인자로 사용하지 말고 호출되는 쪽에 null 체크 책임을 남겨두는 것이 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 안 좋음
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);

7. Optional을 컬렉션의 원소로 사용 금지

컬렉션에는 많은 원소가 들어갈 수 있다. 따라서 비싼 Optional을 원소로 사용하지 말고 원소를 꺼낼 때나 사용할 때 null 체크하는 것이 좋다. 특히 Map은 getOrDefault(), putIfAbsent(), computeIfAbsent(), computeIfPresent() 처럼 null 체크가 포함된 메서드를 제공하므로, Map의 원소로 Optional을 사용하지 말고 Map이 제공하는 메서드를 활용하는 것이 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 안 좋음
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 -> "");

8. of(), ofNullable() 혼동 주의

of(X)Xnull이 아님이 확실할 때만 사용해야 하며, Xnull이면 NullPointerException 이 발생한다.
ofNullable(X)Xnull일 수도 있을 때만 사용해야 하며, Xnull이 아님이 확실하면 of(X)를 사용해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 안 좋음
return Optional.of(member.getEmail()); // member의 email이 null이면 NPE 발생

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



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

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

9. Optional<T> 대신 OptionalInt, OptionalLong, OptionalDouble

Optional에 담길 값이 int, long, double이라면 Boxing/Unboxing이 발생하는 Optional<Integer>, Optional<Long>, Optional<Double>을 사용하지 말고, OptionalInt, OptionalLong, OptionalDouble을 사용하자.

1
2
3
4
5
6
7
// 안 좋음
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 발생 안 함

정리

  1. isPresent()-get() 대신 orElse()/orElseGet()/orElseThrow()

  2. orElse(new ...) 대신 orElseGet(() -> new ...)

  3. 단지 값을 얻을 목적이라면 Optional 대신 null 비교

  4. Optional 대신 비어있는 컬렉션 반환

  5. Optional을 필드로 사용 금지

  6. Optional을 생성자나 메서드 인자로 사용 금지

  7. Optional을 컬렉션의 원소로 사용 금지

  8. of(), ofNullable() 혼동 주의

  9. Optional<T> 대신 OptionalInt, OptionalLong, OptionalDouble


크리에이티브 커먼즈 라이선스HomoEfficio가 작성한 이 저작물은(는) 크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 4.0 국제 라이선스에 따라 이용할 수 있습니다.