devcken.io

Thoughts, stories and ideas.

Why should Java 8's Optional not be used in arguments

Java 8부터 java.util.Optional<T>이라는 클래스가 지원되기 시작했습니다. Scala에서 Option이라는 이름으로 지원되는 클래스와 거의 동일한 역할을 합니다.

자바의 공식 문서를 보면 다음과 같이 설명하고 있습니다.

null이 아닌 값을 포함하지 않거나 포함하는 컨테이너 객체

즉, null과 관련된 처리를 처리하기 위한 클래스로 보입니다.

Null Island is One of the Most Visited Places on Earth. Too Bad It Doesn’t Exist

Null 참조의 아버지(?)인 Tony Hoare는 2009년 한 컨퍼런스에서 Null 참조를 발명한 것에 대해서 사과했습니다.

저는 그걸 10억 달러짜리 실수라고 부릅니다. 1965년에 null 참조를 발명한 것 말이죠. 그 시대에, 저는 객체 지향 언어(ALGOL W)의 참조를 위한 첫번째 내포 타입 시스템을 설계하고 있었죠. 저의 목표는 모든 참조 사용이 절대적으로 안전하다는 것을 보장하는 것이었는데, 컴파일러에 의해 자동으로 수행되는 검사와 함께 말이죠. 그런데, null 참조를 부여하는 유혹을 견뎌내지 못했습니다. 단순히 구현하기 쉬웠기 때문이죠. 이는 무수한 오류, 취약점 그리고 시스템 충돌들을 야기시켰습니다. 필시 이것이 최근 40년 동안의 10억 달러 만큼의 고통과 상처를 안겨주었습니다.

그만큼 Null 참조는 문제가 많습니다. Null 참조로 인한 잠재적인 버그들이 개발자들을 괴롭혀왔고 여전히 그렇습니다.

이러한 고통과 상처를 해결하기 위한 것이 바로 Optional입니다.

제가 구현 중인 제품의 일부 코드를 예로 들어보겠습니다.

...
return Optional.ofNullable(Longs.tryParse(identifier))  
       .map(this::findOne)
       .orElse(this.repository.findOne(identifier));
...

보시면 아시겠지만, 저장소에서 특정 인스턴스를 가져오는 코드입니다. Optional.ofNullable을 사용했습니다. 그 이유는 identifier라는 변수가 String 타입이고 이를 Guava의 Longs.tryParse 함수를 사용해 구문 분석한 후 그 결과가 null이면 저장소의 findOne 메서드에 identifier 변수를 그대로 넘기고 Long 타입으로 잘 캐스팅되었다면 캐스팅된 값을 넘기도록 한 것입니다.

위 코드를 null을 사용해서 구현하면 다음과 같이 구현해야 할 겁니다.

Long x = Longs.tryParse(identifier)

return x == null ?  
  this.repository.findOne(identifier) :
  this.repository.findOne(x);

뭐 굉장히 평범한 코드이긴 합니다. null을 직접 언급한다는 것이 깨림직한 정도죠. Null 참조가 왜 나쁜가에 대한 논의는 여기서 직접하지 않겠습니다.

제가 이 포스트를 통해 알아보고자 하는 것은 사실 따로 있습니다.

저는 위 예제에서 Optional을 메서드의 반환 타입으로 사용했습니다. 이것이 바로 Optional의 존재 이유죠. 아, 참고로 findOne 함수 또한 Optional을 반환합니다. 만약 findOne의 반환 타입이 Optional이 아니고 Optional의 타입 파라메터 자체가 반환 타입이었다면 null이 반환될 가능성이 존재합니다. 요청된 식별자에 대한 값이 저장소 내에 없을 수도 있기 때문이죠.

만약 그럴 경우, 위 코드의 메서드를 실행하는 호출자는 null에 대한 대비를 해야 합니다. if (x == null) {...이나 x == null ? ... 류의 코드 말이죠.

음, 저는 이런 생각이 들었습니다. 메서드의 파라메터로 Optional 타입을 받으면 어떨까?

T doSomething(Optional<T> tOptional) {  
  tOptional.ifPresent(...);
  ...
}

뭐 대략 이런 식의 코드가 가능해질 겁니다.

저는 IDE로 IntelliJ를 사용 중인데요, 위와 같은 코드를 작성하면 IntelliJOptional<T>에 노란색 박스를 그립니다. 그리고는 아래와 같이 경고합니다.

Reports any uses of java.util.Optional, java.util.OptionalDouble, java.util.OptionalInt, java.util.OptionalLong or com.google.common.base.Optional as the type for a field or a parameter. Optional was designed to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result". Using a field with type java.util.Optional is also problematic if the class needs to be Serializable, which java.util.Optional is not.

말인즉슨, Optional 류의 타입을 파라메터에 적용하지 말라는 겁니다. 원래 그 용도가 반환 값으로 null이 반환될 가능성이 있는 경우, 이를 위한 명확한 방법을 제시하기 위한 것이고, 그것을 파라메터에 적용할 경우 대상 클래스가 Serializable해야 하는 경우 문제를 일으킬 수 있다는 것입니다.

즉, 객체를 직렬화할 때 Optional 자체가 문제가 될 수도 있다는 뜻입니다.

그 이유에 대해서 좀 더 찾아보던 중 StackOverflow에서 도움이 될 만한 질문과 대답을 찾았습니다. 이 포스트의 제목이 바로 그 질문의 제목입니다.

대답의 내용을 보면,

  1. (+) 어떤 의미론적 분석도 없이 Option 결과를 다른 메서드를 전달할 수 있다; 해당 메서드에 그것을 남기는 것(전달)은 꽤 괜찮다.
  2. (-) Optional을 파라메터로 사용하면, 메서드 내부의 조건부 로직이 일어나 비 생산적이다.
  3. (-) 인자를 Optional로 패키징해야 할 필요가 있는데, 이는 컴파일러에 대해 차선책이며, 불필요한 랩핑을 수행하게 됩니다.
  4. (-) null이 가능한 파라메터와의 비교에서 Optional이 좀 더 비용이 듭니다.

일반적으로: Optional은 해결해야 할 두 가지 상태를 통합한다. 따라서 데이터 흐름의 복잡도를 위해 입력보다는 결과(반환 값)에 더 잘 맞는다.

그 외에도 댓글들을 읽어보면 좀 더 도움이 될 것 같습니다. 그리고 채택되지는 않았지만 두번째 대답도 상당히 좋은 인사이트를 제공합니다.