Optional 설명

Much of SpongeAPI makes use of Java’s Optional system on object accessors, but if you’ve never used Optional before this might seem like a bit of a peculiar way of doing things. You might be tempted to ask: “why do I need to perform an extra step when fetching something from an API object?”

This section gives a brief summary of Optional and explains how - and perhaps more importantly why - it’s used throughout SpongeAPI.

짧은 이야기를 통해 시작해보죠. 접근자 (특히 “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();
}

In this example, the plugin author is aware that the method can return null and has a constant available with a default instance of Foo which can be used instead. Of course, the plugin could just short-circuit the call entirely, or it could attempt to fetch Foo from somewhere else. The key message is that handling nulls even in simple cases can lead to spaghetti code quite quickly, and moreover relies on the plugin author to explicitly visit the method’s contract to check whether a null check is necessary in the first place.

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

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

Thus, we can see that allowing implicit nullable contracts leaves us with a selection of pretty awful solutions to choose from:

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

  • API 작성자가 모든 API 메소드에 Nullable의 암묵적 계약을 정의하여, null 반환 값 관리의 책임을 플러그인 개발자에게 떠넘기는 방법은 이전의 접근 방법을 악화시킬 뿐입니다.

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

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

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

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

2. Optional vs. 암묵적 Nullable 계약

As mentioned above, APIs for Minecraft are in a difficult situation. Ultimately, they need to provide a platform with a reasonable amount of implied stability atop a platform (the game) with absolutely no amount of implied stability. Thus, any API for Minecraft needs to be designed with full awareness that any aspect of the game is liable to change at any time for any reason in any way imaginable; up to and including being removed altogether!

게임의 변덕스러운 점은 위에서 설명한 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값을 감시하여 대응*하는 개념을 *값이 없는 때도 있다*라는 미묘한 차이로 바꾸었습니다. 우리는 또한 이 계약을 *첫 번째 날에 규정합니다.

So, what does this mean?

간결하게 말하자면, 플러그인 개발자는 더 이상 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();
}

This is merely the tip of the Optional iceberg. In java 8 Optional also supports the Consumer and Supplier interfaces, allowing lambdas to be used for absent failover. Usage examples for those can be found on the 사용 예제 page.

참고

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