반응형

2021.07.25 - [Kotlin] - [Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는걸까?

 

[Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는걸까?

상속과 델리게이션(위임) 모두 객체지향 프로그래밍의 디자인 방식입니다. 두 방식 모두 클래스를 다른 클래스로부터 확장하는데요. 개발자는 종종 두 방식 사이에서 선택을 해야 하는데, 언어

readystory.tistory.com

 

이전 글에 이어서 2편입니다!

 

이번 글에서는 생성자의 파라미터와 프로퍼티에 위임하는 방법에 대해 자세하게 알아보겠습니다. 지난 글이 Delegation 에 대한 개념적인 내용이었다면 이번 글은 좀 더 코틀린 문법에 대해 포커스를 맞출 예정입니다.

 

파라미터에 위임하기

이전 예제에서는 아래와 같은 코드를 작성했습니다.

 

interface Worker {
  fun work()
  fun takeVacation()
}

open class JavaProgrammer : Worker {
  override fun work() = println("code with Java")
  override fun takeVacation() = println("code at the beach")
}

class PythonProgrammer : Worker {
  override fun work() = println("code with Python")
  override fun takeVacation() = println("code at the home")
}

class Manager : Worker by JavaProgrammer()

 

Manager 클래스를 보시면 Worker by JavaProgrammer() 란 코드를 작성 했었습니다. 즉, Manager 의 인스턴스가 명시적으로 생성된 JavaProgrammer() 의 인스턴스로 위임(Delegation) 합니다.

 

하지만 이런 구현에는 두 가지 이슈가 있습니다.

첫 째, Manager 클래스의 인스턴스는 오직 JavaProgrammer 의 인스턴스에게만 요청할 수 있습니다. 다른 종류의 Worker 인터페이스를 구현한 클래스에게 요청이 불가능합니다.

둘 째, Manager 의 인스턴스는 델리게이션에 접근할 수 없습니다. 이는 Manager 클래스 안에 다른 메소드를 작성하더라도 해당 메소드에서는 델리게이션에 접근할 수 없다는 의미입니다.

 

이런 제약은 인스턴스를 생성하면서 델리게이션을 지정하지 않고, 생성자에 델리게이션 파라미터를 전달함으로써 해결 가능합니다. 코드를 작성해보겠습니다.

class Manager(val staff: Worker) : Worker by staff {
  fun meeting() = println("organizing meeting with ${staff.javaClass.simpleName}")
}

 

Manager 클래스의 생성자는 staff 라는 파라미터를 받습니다. staff 는 val 로 정의했기 때문에 프로퍼티가 됩니다. 만약 val 이 제거된다면 staff 는 클래스의 프로퍼티가 아니고 그냥 파라미터로 남습니다.

위 코드에서는 val 이 사용되든 안되든 상관없이 Manager 클래스는 staff 파라미터를 델리게이션으로 사용합니다.

 

staff 는 Manager 클래스의 프로퍼티기 때문에 meeting() 함수에서 접근할 수 있습니다. 그리고 이렇게 구현하면 Manager 클래스가 더이상 JavaProgrammer 에 묶이지 않고 생성자 파라미터를 통해 Worker 인터페이스를 구현하는 어떠한 객체도 받을 수 있게 됩니다.

 

Manager 인스턴스 두 개를 생성해서 이런 동작들을 확인해보겠습니다.

val javaManager = Manager(JavaProgrammer())
val pythonManager = Manager(PythonProgrammer())
javaManager.work()  // code with Java
javaManager.meeting()  // organizing meeting with JavaProgrammer
pythonManager.work()  // code with python
pythonManager.meeting()  // organizing meeting with PythonProgrammer

 

위 코드에서 보시다시피 이제 Manager 클래스는 어느 하나의 Worker 인터페이스 구현체에 묶이지 않고, 유연하게 사용 가능해졌음을 확인할 수 있습니다. 두 개의 Manager 인스턴스 모두 work() 함수가 호출되면 코틀린이 자동으로 연결된 델리게이션으로 요청을 전달합니다. 그리고 Manager 클래스에 정의된 meeting() 함수가 호출되면 Manager 인스턴스의 속성이 staff 를 이용해서 함수를 수행합니다.

 

함수 충돌 관리

코틀린 컴파일러는 델리게이션에 사용되는 클래스마다 델리게이션 함수를 위한 wrapper 를 만듭니다. 예를 들어 Manager 클래스에 work(), takeVacation() 함수를 구현하지 않아도 Manager 클래스를 정의할 때 델리게이션을 사용했기 때문에 컴파일러가 자동으로 Manager 클래스에 work(), takeVacation() 함수를 구현하여 호출되면 델리게이션 인스턴스에 위임하게 됩니다.

 

그렇다면 만약 사용하는 클래스와 델리게이션 클래스에 동일한 이름과 시그니처가 있는 함수가 있다면 어떻게 될까요?

 

코틀린은 이런 충돌을 해결할 수 있도록 도와줍니다. 결론부터 말하자면 델리게이션은 선택이 가능하며, 델리게이션 클래스의 모든 함수를 일일이 위임할 필요가 없습니다.

 

이전 예제에서 Worker 인터페이스는 takeVacation() 함수를 가지고 있고, Manager 클래스는 해당 함수를 델리게이션인 Worker 에게 위임했습니다. 하지만 이대로 사용했다간 우리는 Manager 의 휴가를 구현하고 싶은데도 델리게이션 하고 있는 Worker 에게 휴가처리도 위임해버리게 됩니다. 이는 의도하지 않은 설계를 가져오게 됩니다.

 

코틀린에서는 델리게이션을 이용하는 클래스가 델리게이션 클래스의 인터페이스를 구현해야 합니다. 하지만 실제로는 인터페이스의 각 메소드를 모두 구현하지 않습니다. 앞서 보았듯, Manager 는 Worker 인터페이스를 구현하지만 work() 나 takeVacation() 메소드를 실제로 구현하여 제공하지 않습니다.

 

델리게이션 클래스의 모든 인터페이스를 위해서 코틀린 컴파일러가 Wrapper 를 만듭니다. 하지만 델리게이션 클래스가 이미 인터페이스의 함수를 구현한 상태에서 델리게이션을 이용하는 클래스에서 다시 함수를 구현하려고 하는 경우에는 override 키워드를 사용해야 합니다. 그렇게 하면 클래스에서 구현한 메소드에 우선 순위가 생기고, 컴파일러는 Wrapper 함수를 생성지 않습니다.

 

이를 확인하기 위해 Manager 클래스에 takeVacation() 함수를 구현해보겠습니다.

class Manager(val staff: Worker) : Worker by staff {
  override fun takeVacation() = println("manager vacation")
}

 

override 키워드를 사용했기 때문에 코드를 읽는 사람들은 해당 함수가 어쩌다보니 델리게이션의 함수와 같은 이름으로 생성된게 아니라 인터페이스의 함수를 구현했다는 사실을 명확하게 알 수 있습니다. 이것을 보고 코틀린 컴파일러는 takeVacation() 함수의 Wrapper 를 생성하지 않고 work() 함수의 Wrapper 만을 생성할 것입니다.

 

이번 버전 Manager 클래스의 인스턴스에서 인터페이스의 함수를 사용해 보겠습니다.

val manager = Manager(JavaProgrammer())
manager.work()  // code with Java
manager.takeVacation()  // manager vacation

 

Delegation 의 주의사항

지금까지 우리가 만든 예제에서 Manager 는 JavaProgrammer 의 인스턴스에게 델리게이션을 요청했습니다. 하지만 Manager 의 참조는 JavaProgrammer 의 참조에 할당할 수 없습니다. 이 말의 뜻은 Manager 는 JavaProgrammer 를 사용할 수 있지만 Manager 를 JavaProgrammer 로 사용할 수는 없다는 이야기입니다.

 

다시 말해 Manager 는 JavaProgrammer 를 가지고 있는 것이지, JavaProgrammer 의 한 종류가 아니라는 뜻입니다. 앞서 말한 바와 같이 델리게이션은 상속과 다르게 대체될 가능성이 없는 재사용성을 제공해 줍니다.

 

하지만 코틀린이 델리게이션을 사용하게 되면 해당 클래스는 위임할 인터페이스를 구현해야 합니다. 그래서 델리게이션을 사용하는 클래스를 참조하면 위임할 인터페이스의 참조에 해당할 수 있습니다.

 

즉 다음과 같은 현상이 발생합니다.

val manager = Manager(JavaProgrammer())
val coder: JavaProgrammer = manager  // Error : type mismatch
val employee: Worker = manager  // OK

 

위 코드가 의미하는 바는 Manager 는 JavaProgrammer 의 자식은 아니기에 대체될 수 없지만, Worker 로 취급될 수는 있습니다. 이는 어쩌면 문법적 한계에 따른 부작용으로 바라볼 수도 있을 것 같습니다.

 

주의 사항은 이 뿐만이 아닙니다.

우리는 아까 staff 를 Manager 의 생성자로 전달할 때 val 을 사용했습니다. 하지만 이 때 val 을 var 로 변경하면 몇 가지 이슈가 발생합니다.

 

val manager = Manager(JavaProgrammer())

println("Staff is ${manager.staff.javaClass.simpleName}")  // Staff is JavaProgrammer
manager.work()  // code with Java

manager.staff = PythonProgrammer()
println("Staff is ${manager.staff.javaClass.simpleName}")  // Staff is PythonProgrammer
manager.work()  // code with Java

 

Manager 의 생성자는 staff 란 이름의 델리게이션을 가변(mutable)으로 정의했습니다.

생성 당시에 JavaProgrammer() 인스턴스를 주입하고서 이후에 PythonProgrammer() 인스턴스로 staff 를 변경해주었음에도 manager.work() 의 결과는 여전히 처음에 설정했던 "code with Java" 가 출력되는 것을 확인할 수 있습니다.

 

이는 코틀린 컴파일러가 어떻게 델리게이션을 처리하는지 보다 자세히 알 필요가 있습니다. Manager 클래스를 정의할 때 사용하였던 Worker by staff 델리게이션에서 staff 는 프로퍼티가 아니라 파라미터입니다. 마치, Manager 생성자에 staff 파라미터에 val 이나 var 을 선언하지 않은 것과 같습니다.

 

코틀린은 객체의 프로퍼티가 아닌 프라이머리 생성자에 보내진 파라미터로 델리게이션을 합니다. 그렇기에 이 과정을 잘 이해하고 사용할 필요가 있습니다.

 

문제는 이 뿐만이 아닙니다. staff 를 PythonProgrammer 인스턴스로 변경했을 때 원래 사용하던 JavaProgrammer 의 인스턴스엔 더 이상 접근할 수 없어졌음에도 불구하고 델리게이션이 JavaProgrammer 인스턴스를 사용중이기 때문에 가비지 콜렉터가 수집해 가지 않아 메모리릭이 발생할 수 있습니다.

 

이런 여러 이유로 생성자 파라미터를 통해 델리게이션을 사용할 경우에는 반드시 파라미터로 선언하거나, val 로 선언하여 사용하시길 권장합니다.

 

 

 

반응형
반응형