Optional 설명

Sponge API의 많은 부분에서는 자바에서 제공하는 Optional 시스템을 객체 접근자로 활용합니다. 하지만 당신이 아직 ``Optional``을 써보지 않았다면 다소 이상한 방법처럼 보일 수 있습니다. 따라서 당신은 아마 이런 질문을 할겁니다: “API 객체에서 무언가를 가져올 때 부가적인 단계를 거쳐야 하는 이유는 뭐죠?”

이번 페이지는 ``Optional``을 간단하게 요약하고 이것이 Sponge API에서 어떻게 (“왜?” 가 더 중요하겠군요) 사용되는지를 설명합니다.

짧은 이야기를 통해 시작해보죠. 접근자 (특히 “getter”) 가 ``Optional``을 거치지 않고 일하는 방법을 봅시다.

1. 암묵적 Nullable 계약의 문제점

API 객체로 Entity``가 있다고 생각합시다. 객체의 ``getFoo() 메소드는 ``Foo``를 반환합니다.

../../_images/optionals1.png

옛날 옛적에, 우리가 만든 플러그인은 아래 코드처럼 ``getter``를 사용해 Entity의 ``Foo``를 가져오곤 했답니다.

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo();
    entityFoo.bar();
}

그런데 여기엔 문제가 있습니다. API를 설계할 때 - 우리는 getFoo 메소드가 null 값을 반환할 수 있는지에 대한 암묵적 계약에 의존해야 합니다. *암묵적 계약*은 두 방법 중 하나로 정의할 수 있습니다:

  • Javadoc에 명시하기 - 이것은 플러그인 개발자가 메소드에 대한 javadoc을 반드시 읽어야 하며, 반환 값에 대한 계약 명시가 개발자 입장에서 불분명할 수 있습니다.

  • Nullable 어노테이션 사용 - 일반적으로 어노테이션의 사용은 특별한 도구가 필요하므로 완벽한 방법이 아닙니다. 어노테이션을 관리할 IDE나 컴파일러 의존이 그 예입니다.

../../_images/optionals2.png

getFoo() 메소드가 (계약 내용의 일부인) null 값을 반환할 수 있다고 가정합니다. 그렇게 되면 우리가 위에서 작성한 코드는 ``entityFoo``가 null일 경우 ``NullPointerException``을 초래하므로 불안정한 코드가 됩니다.

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo();
    entityFoo.bar();
}

Let’s assume our plugin author is savvy to the nullable nature of our getFoo method and decides to fix the problem with null checking. Assuming they have defined a local constant Foo, the resultant code looks like this:

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo();
    if (entityFoo == null) {
        entityFoo = MyPlugin.DEFAULT_FOO;
    }
    entityFoo.bar();
}

이 예제에서, 플러그인 개발자는 해당 메소드가 null을 반환할 수 있음을 인식하고 있으며, 반환값 ``Foo``가 없을 때 이를 대신할 기본 인스턴스를 갖는 상수를 개발자가 갖고 있습니다. 물론 플러그인이 호출을 통째로 간략화시키거나, `Foo`를 다른 곳에서 가져오는 시도를 할 수도 있죠. 하지만 이 문제의 핵심은, null 반환값을 관리하는 것은 간단한 코드마저 스파게티 코드로 만들어 버린다는 것과, 플러그인 개발자가 우선적으로 null 값을 확인해야 되는지 알아내기 위해 메소드 계약을 분명하게 확인할 필요가 있다는 것입니다.

하지만 이것만이 문제의 전부는 아닙니다. API가 오랜 기간동안 성장한다고 가정합시다. 그리고 한 개발자가 그의 플러그인을 만들던 때로 돌아가 봅시다. 그는 메소드 Javadoc을 통해 이 메소드가 (애초에 모든 `Entity`는 ``Foo``를 갖고 있으므로) null 값을 반환하지 않음을 확인했습니다. 좋아! 복잡하고 난해한 반환값 확인을 할 필요가 없군!

하지만, 시간이 지나고 게임 개발자가 차기 버전을 출시했습니다. 이제 게임에서 Foo 개념은 사용되지 않고 곧 사라집니다. API 개발자는 이에 따라 API를 수정하고 이제부터 ``getFoo()``는 ``null``값을 **반환할 수 있다**고 밝혔으며, 이를 해당 메소드의 Javadoc에 반영했습니다. 이제 문제가 생깁니다: 메소드 계약에 따라 코드를 짠 성실한 플러그인 개발자조차 그 메소드를 자신도 모르게 잘못 조작하는 것입니다. 그의 코드에서 ``getFoo()``를 호출해 ``Foo``를 받아 사용하는 모든 부분에서 NPE 오류가 발생할 것입니다.

따라서 우리는 암묵적 Nullable 계약이 갖는 문제를 해결하려면 다음 중 하나의 대안을 선택해야 합니다:<br>(전혀 내키지 않는 방법이긴 합니다만..)

  • 플러그인 개발자가 모든 메소드의 반환 값이 null일 수 있음을 가정하여 이에 따른 방어적인 코드를 짜는 방법이 있습니다. 하지만 이는 스파게티 코드를 만들어 내기 쉽습니다.

  • The API authors can define an implicit nullable contract on every API method, in an attempt to make null handling the plugin author’s problem, which only exacerbates the previous problem.

  • API 작성자가 모든 Nullable의 암묵적 계약을 깨지 않는 것을 확고히 하는 방법이 있습니다. 이 경우 그들은 게임에서 어떠한 기능을 제거할 날이 오면 다음과 같은 대안을 사용해야 합니다:

  • 예외 처리하기 - 격은 떨어지지만 코드의 어디에선가 발생되어 추적하기 어려운 NPE 문제를 해결하는 것보다 쉬운 방법임은 분명합니다.

  • “가짜” 객체나 잘못된 값을 반환하기 - 이 방법을 사용하면 사용자 코드(플러그인) 은 API가 바뀌어도 계속 작동할 것입니다. 하지만 사용이 중단되는 모든 기능들이 늘어날 수록 더 많은 가짜 객체를 만들어 낼 필요가 있어 API 개발자에게 업데이트에 대한 부담이 날로 증가할 것입니다.

메소드의 Nullable 반환값에 대한 암묵적 계약이 여러가지로 뒷목 잡는 상황을 만드는 것은 확실하게 입증되었습니다. 문제의 API가 극단적으로 불안정한 제품의 코드 위에 지어졌다면 상황은 더 심각해집니다. 다행스럽게도, 이것의 해결책이 존재합니다:

2. Optional vs. 암묵적 Nullable 계약

위에서 언급한 것처럼, 마인크래프트의 API는 어려운 상황에 놓여 있습니다. 궁극적으로 마인크래프트 API는 암묵적 안전성이 전혀 보장되지 않는 플랫폼(게임) 위에서 암묵적 안전성이 충분히 보장된 플랫폼이 되어야 합니다. 따라서 마인크래프트와 상호작용하는 모든 API는 게임의 모든 부분이 생각할 수 있는 어떤 방법으로든 어떤 이유에서든 어느 때라도 바뀔 수 있음을 분명하게 인식한 채로 설계되어야 합니다; 언젠가는 게임과 함께 사라질 것도 포함해서 말이죠!

게임의 변덕스러운 점은 위에서 설명한 Nullable 메소드 계약의 문제를 유발합니다.

Optional solves the above problems by replacing implicit contracts with explicit ones. The API never advertises, “here is your object, kthxbai”, instead it presents accessors with a “here is a box which may or may not contain the object you asked for, ymmv”.

../../_images/optionals3.png

null 값 반환의 가능성을 계약 내용에 분명히 밝히는 방법으로, 우리는 null값을 감시하여 대응*하는 개념을 *값이 없는 때도 있다*라는 미묘한 차이로 바꾸었습니다. 우리는 또한 이 계약을 *첫 번째 날에 규정합니다.

흠.. 그래서 이게 무슨 뜻인가요?

간결하게 말하자면, 플러그인 개발자는 더 이상 null 값이 반환될 가능성에 대해 걱정할 필요가 없어졌습니다. 대신 특정 객체가 사용 불가능할 가능성이 그들의 플러그인 코드의 가장 밑바닥에 부호화됩니다. 이것은 지속적으로 null 값을 판독하는 안전성 내장 기능과 같은 단계에 있지만, 필요에 따라 격식 있고 해독이 쉬운 코드가 되는 이점을 갖습니다.

왜 그런지 보려면, 위 예제와 아래를 비교하여 getFoo() 메소드가 ``Optional<Foo>``를 반환하는 것을 확인하세요.

public void someEventHandler(Entity someEntity) {
    Optional<Foo> entityFoo = someEntity.getFoo();
    if (entityFoo.isPresent()) {
        entityFoo.get().bar();
    }
}

당신은 이 예제가 일반적인 null값 감지와 유사하다고 느낄 지도 모릅니다만, ``Optional``를 사용하면 정말로 같은 양의 코드에 좀 더 많은 양의 정보를 실을 수 있습니다. 그 예로, 누군가가 이 메소드 계약을 확인하기 위해 위 코드를 읽는 것은 필요하지 않습니다. 이 코드에서 메소드는 반환 값이 없을 수 있다는 사실과 반환 값의 부재 처리 부분은 명백하고 깔끔하게 나타나 있습니다.

그래서 어쩌라는거죠? 여기서 우리들의 확실한 계약(explicit contract) 은 null값을 검사하는 코드와 결과적으로 같습니다 - 비록 getter에 의해 계약적으로 강요된 부분이 있지만요. “그래서 Optional을 왜 쓰라는거죠?”

`Optional`로 객체를 포장하면 전통적인 null값 감지 코드의 불편한 측면을 좀 더 격식적으로 바꿀 수 있습니다. 다음 코드를 확인해보세요:

public void someEventHandler(Entity someEntity) {
    Foo entityFoo = someEntity.getFoo().orElse(MyPlugin.DEFAULT_FOO);
    entityFoo.bar();
}

잠깐만요! 아까 코드에서 null 체크 & 기본값을 대입하는 부분을 코드 한 줄로 요약한건가요? 네! 정답입니다. 사실 여기에서 할당 연산자도 생략하면 정말로 한 줄만에 이벤트 연산의 모든 것을 끝낼 수 있답니다:

public void someEventHandler(Entity someEntity) {
    someEntity.getFoo().orElse(MyPlugin.DEFAULT_FOO).bar();
}

``getFoo()``를 받아올 때 기본값으로 항상 사용가능한 ``MyPlugin.DEFAULT_FOO``를 지정했으므로 메소드의 반환값이 없어진다 해도 전혀 문제가 없는 코드입니다.

이번 예제에선 두 개의 개체(Entity) 를 사용하겠습니다. 아래 코드는 암묵적 Nullable 계약을 사용하는 방법으로, 첫 번째 개체로부터 ``Foo``를 가져오거나, 받아올 수 없다면 두번째 ``개체``로부터 ``Foo``를 가져오고, 실패를 대비하여 기본값도 준비해 두었습니다:

public void someEventHandler(Entity someEntity, Entity entity2) {
    Foo entityFoo = someEntity.getFoo();
    if (entityFoo == null) {
        entityFoo = entity2.getFoo();
    }
    if (entityFoo == null) {
        entityFoo = MyPlugin.DEFAULT_FOO;
    }
    entityFoo.bar();
}

그렇다면 위 코드와 동일한 기능을 갖지만 ``Optional``을 사용하여 코드를 간결하게 짜보겠습니다:

public void someEventHandler(Entity someEntity, Entity entity2) {
    someEntity.getFoo().orElse(entity2.getFoo().orElse(MyPlugin.DEFAULT_FOO)).bar();
}

여기까지 설명한 Optional``의 기능은 그저 빙산의 일각입니다. Java 8에서 ``Optional``은 ``Consumer``와 ``Supplier 인터페이스도 제공하는데, 이를 통해 *반환값 부재*의 대체 기능을 람다식으로 표현할 수 있습니다. 사용 예제 페이지에서 이 예제를 확인할 수 있습니다.

참고

null값 참조를 피해야 할 근거에 대한 또다른 설명은 Guava: Using And Avoiding Null Explained <https://github.com/google/guava/wiki/UsingAndAvoidingNullExplained/>`_에서 찾을 수 있습니다. 링크로 연결되는 문서에서 언급될 guava가 제공하는 ``Optional` 클래스는 java의 ``java.util.Optional``과 달라서 메소드 이름들이 여기서 설명한 것과 다름에 주의하세요.