1. Capability의 필요성 - Tictim/Capabilities-Within-TileEntity GitHub Wiki

먼저 근본으로 돌아가서, 왜 Capability가 필요한지에 대해서부터 되짚어 봅시다. 여러분은 블럭과 타일엔티티의 구조를 대충 이해했으나, 타일엔티티 간의 상호작용에 대해서는 생각을 해 본 적이 없습니다. 어떻게 하면 서로 다른 타입의 타일엔티티들 사이에 상호작용이 가능한 공통적인 시스템을 만들어낼 수 있을까요? 그냥 인터페이스를 갖다 박으면 되죠! 사용하기 쉽고, 명료하고, 아마 여러분들 머릿속에서 가장 먼저 떠오르는 아이디어였을 것입니다. 바로 이렇게요!

public class MyTileEntity extends TileEntity implements IMyAwesomeMagic {

    // ...

    @Override
    public void doMyAwesomeMagic(){
        // ...
    }

    // ...

}

그리고 사용할 때에는...

TileEntity te = world.getTileEntity(pos);
if(te instanceof IMyAwesomeMagic){
    IMyAwesomeMagic myAwesomeMagic = (IMyAwesomeMagic)te;
    myAwesomeMagic.doMyAwesomeMagic();
}

참 쉽죠?

하지만 이런 디자인은 얼마 못 가 여러분의 발목을 잡게 됩니다. 타일엔티티가 구현해야 되는 인터페이스의 수는 타일엔티티가 지원하고자 하는 기능의 갯수에 비례해 늘어나기 때문에, 곧 여러분은 다음과 같이 시작하는 2,000줄의 타일엔티티 코드가 눈 앞에 있는 것을 알아차리게 됩니다.

public class MyTileEntity extends TileEntity
           implements IMyAwesomeMagic
                IEnergyStorage,
                IItemHandler,
                IFluidHandler,
                ISomeFeatureHandler,
                ISomeAnotherFeatureHandler {

    // 그리고... 끔찍한 메소드 덩어리.

타일엔티티마다 새로운 구현을 작성해야 하는 점도 문제를 야기합니다. 그야, 타일엔티티 코드의 80%를 이미 다른 곳에서 백만 번씩 쓰였던 코드로 채우게 되니까요. 이렇게 복사+붙여넣기한 코드는 내용이 완전히 같음에도 불구하고 재사용할 수 없게 됩니다. 게다가, 이 코드에 버그픽스 혹은 수정을 적용시켜야 한다면? 상상하기도 싫군요.

모든 문제를 해결시켜 주지는 않지만, 구조적인 하자를 약간이나마 줄일 수 있는 방법이 있습니다. Delegate의 사용입니다. 쉽게 말하자면, 모든 인터페이스의 메소드 콜을 기능이 이미 구현된 인스턴스에 맡기는 거죠.

public class MyTileEntity extends TileEntity implements IMyAwesomeMagic {
    private final IMyAwesomeMagic myAwesomeMagicDelegate = <기능 구현>;

    // ...

    @Override
    public void doMyAwesomeMagic(){
        myAwesomeMagicDelegate.doMyAwesomeMagic(); // Delegate 인스턴스로 콜 넘기기
    }

    // ...
}

이렇게 되면 우리 타일엔티티가 구현하는 인터페이스와 메소드 갯수가 줄지는 않았어도, 적어도 코드의 재사용은 가능해졌습니다.

여기서 더 멀리 가 봅시다. 만약 이 Delegate 인스턴스를 그냥 넘겨주어 사용할 수 있게 만든다면? 예를 들어, 이런 식으로요.

public class MyTileEntity extends TileEntity implememts IMyAwesomeMagicProvider {
    private final IMyAwesomeMagic myAwesomeMagic = <기능 구현>;

    @Override
    public IMyAwesomeMagic getMyAwesomeMagic() {
        return myAwesomeMagic;
    }

    // ...
}

타일엔티티에 필요한 메소드는 단 한 개 뿐이군요! 이 코드는 다음과 같이 사용될 것입니다.

TileEntity te = world.getTileEntity(pos);
if(te instanceof IMyAwesomeMagicProvider){
    IMyAwesomeMagic myAwesomeMagic = ((IMyAwesomeMagicProvider)te).getMyAwesomeMagic();
    myAwesomeMagic.doMyAwesomeMagic();
}

따라서, 위의 끔찍한 메소드 덩어리는 다음과 같은 "조금 더 볼만한" 덩어리로 축약될 수 있습니다.

public class MyTileEntity extends TileEntity
           implements IMyAwesomeMagicProvider
                IEnergyStorageProvider,
                IItemHandlerProvider,
                IFluidHandlerProvider,
                ISomeFeatureHandlerProvider,
                ISomeAnotherFeatureHandlerProvider {
    private final IMyAwesomeMagic myAwesomeMagic = /* ... */;
    private final IEnergyStorage energyStorage = /* ... */;
    private final IItemHandler itemHandler = /* ... */;
    private final IFluidHandler fluidHandler = /* ... */;
    private final ISomeFeatureHandler someFeature = /* ... */;
    private final ISomeAnotherFeatureHandler someAnotherFeature = /* ... */;

    @Override
    public IMyAwesomeMagic getMyAwesomeMagic() {
        return myAwesomeMagic;
    }

    @Override
    public IEnergyStorage getEnergyStorage() {
        return energyStorage;
    }

    @Override
    public IItemHandler getItemHandler() {
        return itemHandler;
    }

    @Override
    public IFluidHandler getFluidHandler() {
        return fluidHandler;
    }

    @Override
    public ISomeFeatureHandler getSomeFeature() {
        return someFeature;
    }

    @Override
    public ISomeAnotherFeatureHandler getSomeAnotherFeature() {
        return someAnotherFeature;
    }

그런데... 과연 여기서 이 모든 Provider들의 의미가 있을까요? 물론, Provider들이 없으면 delegate를 받아올 수 있는 통일된 방법이 사라집니다만, 이 모든 Provider들이 각각 다른 타입의 고유한 인스턴스를 반환한다는 점을 생각해보면, 모든 타입마다 Provider를 만들지는 않아도 됩니다. 대신 키 값을, 예를 들어 Class를 받아서, 해당 값에 해당하는 인스턴스를 넘겨줄 수도 있습니다. 이런 식으로요.

public class MyTileEntity extends TileEntity implements IContentProvider {
    private final IMyAwesomeMagic myAwesomeMagic = /* ... */;
    private final IEnergyStorage energyStorage = /* ... */;
    private final IItemHandler itemHandler = /* ... */;
    private final IFluidHandler fluidHandler = /* ... */;
    private final ISomeFeatureHandler someFeature = /* ... */;
    private final ISomeAnotherFeatureHandler someAnotherFeature = /* ... */;

    @Override
    @Nullable // 이 타일엔티티에 존재하지 않는 기능의 요청에는 아무 것도 반환해서는 안 되기에, getContent 메소드는 null을 반환할 수 있어야 합니다.
    public <T> T getContent(Class<T> classOfContent){
        if(classOfContent==IMyAwesomeMagic.class) return (T)myAwesomeMagic;
        else if(classOfContent==IEnergyStorage.class) return (T)energyStorage;
        else if(classOfContent==IItemHandler.class) return (T)itemHandler;
        else if(classOfContent==IFluidHandler.class) return (T)fluidHandler;
        else if(classOfContent==ISomeFeatureHandler.class) return (T)someFeature;
        else if(classOfContent==ISomeAnotherFeatureHandler.class) return (T)someAnotherFeature;
        else return null;
    }
}

이거 보세요! 우리는 인터페이스 단 한 개메소드 단 한 개로 모든 기능을 핸들링했으며, 코드는 가독성 있고 재사용과 유지보수에도 아무런 문제가 없습니다. 이러한 형태의 타일엔티티는 다음과 같이 사용되겠죠.

TileEntity te = world.getTileEntity(pos);
if(te instanceof IContentProvider){
    IMyAwesomeMagic myAwesomeMagic = ((IContentProvider)te).getContent(IMyAwesomeMagic.class);
    if(myAwesomeMagic!=null){
        myAwesomeMagic.doMyAwesomeMagic();
    }
}

하지만 슬프게도, 우리의 IContentProvider 디자인에는 치명적인 단점이 있습니다. IContentProvider는 모든 모드가 사용 가능한 보편적인 라이브러리에 포함되어 있어야 하며, 그렇지 않을 경우에는 다른 모드들이 추가하는 기능은 이 구조를 사용할 수 없습니다.

흠, 만약 모든 모드들이 공유하는 라이브러리 중 IContentProvider와 같은 기능을 제공하는 라이브러리가 존재한다면...

...잠깐, 우리 무슨 얘기 중이었죠? 아하, Capability!

⚠️ **GitHub.com Fallback** ⚠️