[kotlin] compaion object를 활용한 factory

개요top

자바에서 코틀린으로 건너오는 분들을 위해 static을 대체할 companion object를 소개하곤 합니다.
이렇게 소개되다보니 compaion object의 진짜 의미와 사용법에 대해 잘 알려지지 않는 면이 있는데요, compaion object는 static을 대체하기 위한 프록시객체라는 면도 있지만 더 중요한 사실은 바로 object라는 점입니다.
따라서 이 object특성을 이용한 다양한 패턴을 전개할 수 있습니다.

이번 글에서는 이러한 활용 중에 추상팩토리메서드를 대체하고 리플렉션을 억제하는 용도에 대해 알아보겠습니다.

KClass를 인자로 받으려는 이유top

이건 우리가 왜 리플렉션을 쓰려는가에 대한 이유이기도 합니다. 리플렉션은 금단의 마약처럼 개발자에게 달콤한 유혹이 되는데 반대로 얘기하자면 리플렉션을 사용한 범용로직을 제네릭하게 구현하고 싶기 때문이기도 합니다.

그럼 KClass로 전개될 리플렉션의 로직은 뭘까요?

  1. field리스트를 얻어 동적으로 get, set 사용하기
  2. method리스트를 얻어 동적으로 특정 메소드를 찾아 invoke하기
  3. 해당 객체를 생성하기

근데 1번에 대한 대안으로는 이미 [kotlin] 델리게이터를 이용해 리플렉션을 제거하기편에서 어느 정도 리플렉션을 사용하지 않을 방법을 강구했습니다.

2번의 경우는 동적으로 처리하기엔 이미 그 메소드의 시그니쳐를 확신하고 있어야하는 상황이 많아 일반적인 인터페이스를 구현하지 않은 경우 사용이 쉽지는 않을 것입니다. 스프링의 컨트롤러 처럼 이미 암묵적으로 컨트롤러 메소드들이 따를 시그니쳐 규칙이 확정된 경우에야 도입할만 하지만 일반 클라이언트 개발에서는 미묘한 면이 많았습니다.

집중하고 싶은 부분은 3번입니다. 제네릭을 통해 T형을 받아들였는데도 불구하고 KClass를 전달하려는 이유는 대부분 T형 객체를 생성하는 책임까지 위임하고 싶은 경우가 많기 때문입니다.

보통 리플렉션을 사용하지 않고 생성을 위임하는 패턴은 추상팩토리메소드 패턴입니다. 즉 특정 객체를 생성하기 위해서 그 객체의 클래스가 아닌 그 클래스를 생성할 팩토리객체를 받아서 위임하는 식이죠.

하지만 이렇게 되면 원래 객체 외에도 항상 쌍이 되는 팩토리 클래스와 그 인스턴스를 동시에 관리해야하는 부담이 생기죠.
이러한 이유로 개발자들은 금단의 리플렉션에 빠지게 됩니다.

리플렉션을 통한 인스턴스 생성top

예를 들어 다음과 같은 클래스의 계층 구조에 대해서 생각해보죠.

interface Model{
  fun init(vararg v:Any)
}

class Item:Model{
  override fun init(vararg v:Any){...}
}

class Member:Model{
  override fun init(vararg v:Any){...}
}

이런 타입 계층을 구성한다면 이를 사용하는 측은 Model레벨로 받아들일 수도 있지만 실제 사용하는 구상 클래스의 생성을 대체할 방법은 없습니다.
따라서 보통 생성을 포함한 추상화를 진행하면 다음과 같이 되어버리죠.

fun modelInit(cls:KClass, vararg v:Any) = cls.createInstance().apply{init(*v)}

바로 이 코드에서 별도의 팩토리관련 클래스나 인스턴스를 처리하기 귀찮기(?) 때문에 리플렉션을 쓰고 확장함수인 createInstance나 아니면 아예 .java를 통해 newInstance 등을 호출하게 되는 것을 볼 수 있습니다.

이러한 리플렉션을 통한 특정 클래스에 대한 인스턴스 생성은 워낙 다양한 패턴이 있습니다(다양한 패턴을 정리한 글)

헌데 그러면 어떻게 추가적인 부담이 없이 생성 때문에 리플렉션을 사용하는 경우를 피할까요?(현실은 저 귀찮음도 같이 해결해야하니까 ^^)

람다를 이용하는 경우top

물론 KClass대신 람다를 이용하면 생성 전용 코드를 클라이언트 쪽으로 밀어낼 수 있습니다. 일종의 제네릭을 대체하는 효과도 생겨나죠.

inline fun modelInit(vararg v:Any, block:()->Model) = block().apply{init(*v)}

modelInit("hika"){Member()}

위 코드에서 block에 주어진 람다가 구상 Model인 Member의 인스턴스를 반환하게 함으로서 modelInit의 리플렉션을 제거했습니다.

이 방법은 일견 괜찮아보이지만, 매번 람다를 넘겨야하는 부담이 생기고 그렇기 때문에 람다를 맵이나 companion object에 잡으려고 하게 됩니다.

간단하게 companion object에 람다는 잡는 경우부터 보면 다음과 같습니다.

interface Model{
  fun init(vararg v:Any)
}

class Item:Model{
  companion object{
    val factory:()->Model = {Item()}
  }
  override fun init(vararg v:Any){...}
}

class Memeber:Model{
  companion object{
    val factory:()->Model = {Member()}
  }
  override fun init(vararg v:Any){...}
}

inline fun modelInit(vararg v:Any, block:()->Model) = block().apply{init(*v)}

modelInit("hika", block = Member.factory)

마지막 줄을 보면 매번 람다를 지정하지 않고 Member.factory를 통해 갈음하는 것을 볼 수 있습니다. 헌데 사실 이는 불안정합니다.
왜냐면 Member.factory나 Item.factory는 개발자가 임의로 이름을 맞춘거지 일관성 있는 인터페이스로 보장되는 것이 아니기 때문이죠.

companion object에 제약을 걸기top

원래 classA와 classB 사이에서 static수준의 인터페이스란 만들 수 없습니다. 하지만 코틀린은 static대신 compaion object를 사용하므로 감싸고있는 클래스와 무관한 인터페이스를 compaion object에 내릴 수 있습니다. 간단히 factory라는 별도의 인터페이스를 정의하고 이를 이용해보죠.

interface Model{
  fun init(vararg v:Any)
}

//팩토리용 인터페이스
interface Factory{
  operator fun invoke()
}

class Item:Model{
  companion object:Factory{
    override fun invoke() = Item()
  }
  override fun init(vararg v:Any){...}
}

class Member:Model{
  companion object:Factory{
    override fun invoke() = Member()
  }
  override fun init(vararg v:Any){...}
}

우선 일어난 변화를 보면 factory를 관리하기 위한 인터페이스와 그걸 각 구상 클래스의 companion object가 구상하고 있는 것을 볼 수 있습니다.

물론 Member나 Item의 companion object가 반드시 Factory를 구상해야한다는 제약은 걸 방법은 없지만, 큰 부담없이 Model의 구상클래스 내에 팩토리를 일정한 인터페이스에 따라 구축하고 있는 것을 볼 수 있습니다.

여기에 맞춰 클라이언트 코드도 정리해보죠.

fun modelInit(factory:Factory, vararg v:Any) = factory().apply{init(*v)}

modelInit(Member, "hika")

우선 modelInit의 첫번째 인자로 팩토리 객체를 받아들여 이를 이용해 인스턴스를 만드는 것은 자연스럽고 당연한 거죠.
핵심은 그 다음에 modelInit를 실제로 사용하는 부분입니다!!!

인자로 그냥 클래스인 Member를 넘겼는데요, 코틀린은 언어차원에서 이렇게 클래스이름을 넘기면 사실 상 Member의 compaion object가 넘어온 것으로 처리합니다. 따라서 굉장히 깔끔한 형태로 코드를 정리할 수 있게 됩니다.

결론top

앞으로 다른 글에서 다루게 되겠지만 compaion object를 활용하는 수 많은 패턴이 존재합니다. compaion object는 static의 대체물이 아니라 정말로 object이기 때문에 실체가 있고 이 실체를 컴파일러 수준에서 보장하기 때문이죠.

이번 글에서는 factory에 응용하여 훨씬 적은 수고로 구상 클래스별 팩토리를 관리하여 응집성을 높이면서도 실제 사용시의 코드에서 정말 깔끔하게 클래스명만 넘기는 것으로 복잡한 별도의 factory객체를 전달하지 않아도 되는 패턴을 소개했습니다.