Optional은 정말로 필요한가? (feat. JSpecify)

SightStudio

·

2024. 12. 29. 10:56

필자는 실무에서 처음 Java를 사용할 때부터 Optional을 싫어했습니다.
 
최소한 한 번은 팀에서 "Optional을 사용하지 말자"는 의견을 제안하는데요.
이제는 시간을 아끼고자 이런 생각을 정리해서 블로그로 기록하려합니다.
 
 

null 일 수 도 있고, 아닐 수도 있습니다.

 

Optional의 본래 목적 

 
- [출처] 
 
Java Language Architect 중 한 명인 Brian Goetz의 말을 따르면, Java 8에서 Optional을 추가한 목적은
라이브러리 메서드에서 반환값이 없을 수도 있음을 명확히 표현하기 위함이었다고 합니다.
 
이는 라이브러리 메서드에서, 특히 메서드 체이닝과 같은 경우, 아래 방식으로 처리하는 것보다
 

Method matching =
    Arrays.asList(enclosingInfo.getEnclosingClass().getDeclaredMethods())
    .stream()
    .filter(m -> Objects.equals(m.getName(), enclosingInfo.getName())
    .filter(m ->  Arrays.equals(m.getParameterTypes(), parameterClasses))
    .filter(m -> Objects.equals(m.getReturnType(), returnType))
    .getFirst();
if (matching == null)
  throw new InternalError("Enclosing method not found");
return matching;

 
 
이렇게 "라이브러리에서만 제한적인 Maybe 타입을 쓰는 게 훨씬 더 가독성이 좋으니 추가했다". 라고 이해 할 수 있습니다.
 

return Arrays.asList(enclosingInfo.getEnclosingClass().getDeclaredMethods())
    .stream()
    .filter(m -> Objects.equals(m.getName(), enclosingInfo.getName())
    .filter(m ->  Arrays.equals(m.getParameterTypes(), parameterClasses))
    .filter(m -> Objects.equals(m.getReturnType(), returnType))
    .findFirst() // 여기가 Optional
    .getOrThrow(() -> new InternalError(...));

 
원래부터 이런 목적으로 추가되었던 클래스였던 겁니다.
저도 이 부분 한정으로 사용하는것은 동의합니다. 하지만 흔치않은 상황이죠.

또한 Brian Goetz는 사용자들이 Optional을 일반적인 Maybe 타입으로 사용하지 않기를 바랐습니다.
Maybe 타입은 타입시스템 차원에서 "값이 존재하지 않을 수 도 있음"을 나타내는 것을 말합니다. 
(그의 의도와는 맞지 않게 대부분이 Optional을 이렇게 사용하고 있죠.)
 
 

잘못쓰기 정말 쉬운 Optional 

 
Optional은 잘못 사용하기 너무나도 쉽습니다. 실제로 Optional 사용 시 주의해야 할 사항이 무려 26가지... 에 달할 정도입니다.
여러 코드리뷰를 거치면서 봐왔던 Optional은 대부분 아래와 같은 Null 체크를 피하기 위한 경우가 대부분입니다.
 
 

User a = xxxService.getxxx();
if (a != null) {
    throw new CustomException();
}

 
 
근데 이마저도 isPresent()를 써버리면 Optional을 사용하는 본래 목적이 퇴색됩니다.
 

// 이건 최악이고 
Optional<User> a = xxxService.getxxx();
if (a.isPresent()) {
    throw new CustomException();
}

// 차라리 이게 낫다.
User a = xxxService.getxxx()
		.orElseThrow(CustomException::new);

 
 
그 외에도 대표적으로 잘못 사용되는 Optional 사용 사례는
 

  • Serialize가 필요한 클래스에서 Optional 사용
  • setter에서 Optional 사용
  • Field에서 Optional 사용
  • 생성자나 메서드 인자로 Optional 사용 
  • Optional에 null 할당

 
등이 있습니다. 
 
그 외 단점으론 Optional을 사용하기 위한 래핑 비용정도가 있겠네요.
(코드의 복잡성,  Boxing & unboxing 등등)
 
 

Optional을 쓰기 적합한 곳은?

 
 
자바 9 이후에 Optional 클래스 주석에 아래의 메모가 추가되었습니다.

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은 메서드의 반환타입에서만 사용하는 것이 적합해 보입니다.
하지만, 아래 설명할 JSpecify가 있다면 이 부분에서도 Optional을 사용할 필요가 없다고 생각합니다. 
 
 

Optional의 대안은 무엇이 있나?

 
그렇다면 Optional 없이 Null 체크를 효과적으로 처리하려면 어떻게 해야 할까요?
아래 방법들을 통해 Optional 없이도 Null 체크를 달성할 수 있습니다. 
 
 

Early Throw

 
예측하지 못한 NPE가 발생하는 상황을 최대한 줄이는 방식으로 가야 합니다.
Early Return과 유사하게 Null 이 발생할 때 최대한 예외를 던져서 NPE를 막는 게 좋습니다.
하지만 이것만으론 부족합니다.
 

public class XXXService {
   
  public User getUser(/* .... */) {
  
		if (user == null) {
			throw new UserNotFoundException();
		}    
    
		// ....
    
		return user;
	}
}

 
 

Null 검증 에너테이션 사용

 
제일 효과적인 Null 체크 방법은 바로 @NotNull, @Nullable과 같은 Null 검증 애너테이션을 사용하는 것입니다.
다만 이전까지 자바진영에서는 'Null 검증 애너테이션'에 대한 표준화가 부족했다는 문제가 있었습니다.
 
현재 여러 라이브러리에서 제공하는 Null 검증 애너테이션을 보면, 표준화되지 않아 혼란을 야기하고 있음을 알 수 있습니다.
 

@Nullable, @NonNull (Checker Framework, FindBugs/SpotBugs, IntelliJ, Lombok 등 각종 패키지)
@NotNull (JPA, Hibernate 등에서도 일부 사용)
@Nullable, @NonNull (Spring, Android용 등)

 
 
이러한 혼란을 끝내고자, 자바 커뮤니티와 구글(Google) 등이 주축이 되어 만든 프로젝트가 바로 JSpecify입니다.
JSpecify는 기존에 난립하던 Null 검증 애너테이션을 통합하여, 단일화된 Null 규칙을 제공하는 것을 목표로 합니다.
 
JSpecify 1.0 정식 릴리즈가 2024.7에 출시되었고
2025.11 GA가 예정되어 있는 스프링 7 / 부트 4에서도
기존의 애너테이션을 JSpecify로 변경하는 내용이 포함되어 있습니다.
 

이 방식이 제일 깔끔하다고 생각된다.

 
@NullMarked는 "해당 클래스의 모든 리턴값이 @NotNull이다."를 나타내는 애너테이션인데
클래스나 메서드뿐 아닌 module이나 package 단위로도 선언이 가능해서, 활용성이 높아 보입니다.
(여기서의 module은 자바 9에서 추가된 JMOD를 말함)
 
마이그레이션 가이드도 존재하고, 스프링과 같은 대형 생태계들도 같이 움직이는 걸 보니 
미래의 방향은 JSpecify로 가는데 합당해 보입니다. :)
 

코틀린?

 
뜬금없지만, 자바가 아닌 코틀린으로 가게 되면 애초에 할 필요가 없던 고민입니다.
코틀린은 타입 레벨에서 변수의 nullablity를 명시할 수 있게 돼있고
이를 통해 대부분의 NPE를 컴파일 타임에 방지할 수 있습니다.


코틀린을 쓰면 이런 자바에서 고민하던 문제들을

언어레벨에서 해결해주는 경우가 많아 정말 좋습니다. ㅎㅎ

(자바에도 이걸 적용하는 JEP draft가 올라와있습니다. [링크])
 

기습 코틀린 찬양

 
 

결론

 
이렇게 정리해 보면 Optional이 사용될 만한 곳은 크게 줄어듭니다.

자바에서 Optional 만큼이나 안티패턴이 많은 클래스가 없다고 생각합니다.

 

Optional이 필요한 상황은 극히 제한적이고, 메서드의 리턴타입이나 시그니처를 더럽히기 쉬워서

기본적으로 사용을 금지해도 좋다고 생각합니다.

앞으로는 Optional 없어도 JSpecify를 통해 null 처리를 효과적으로 할 수 있다고 생각합니다.

 

Reference

 
https://stackoverflow.com/a/26328555/12478333
 
https://jspecify.dev/docs/start-here/
 
https://dzone.com/articles/using-optional-correctly-is-not-optional

'개발 > Java' 카테고리의 다른 글

jOOQ 를 좀 더 알아보자  (1) 2022.01.03
[GC] 1. JVM 가비지 컬랙터란?  (0) 2020.08.19
[Java] NIO, 그리고 Netty  (5) 2020.08.12
[Reactive] Reactive Programming 과 Reactive Stream  (3) 2020.08.08