Today I Learned

델리게이션을 통한 확장 (1) 본문

Kotlin/다재다능 코틀린 프로그래밍

델리게이션을 통한 확장 (1)

하이라이터 2023. 9. 1. 14:42
728x90

상속 대신 델리게이션을 써야 하는 상황

  • 클래스의 객체가 다른 클래스의 객체가 들어갈 자리에 쓰여야 한다면 상속을 사용해라
  • 클래스의 객체가 단순히 다른 클래스의 객체를 사용만 해야 한다면 델리게이션을 사용해라

 

델리게이션을 사용한 디자인

디자인적 문제점

interface Worker {
  fun work()
  fun takeVacation()
}
class JavaProgrammer : Worker {
  override fun work() = println("...write java...")
  override fun takeVation() = println("...code at the beach...")
}
class CSharpProgrammer : Worker {
  override fun work() = println("...write C#...")
  override fun takeVation() = println("...branch at the ranch...")
}
class Manager
  • 위 코드는 작업자인 Worker 인터페이스와 Worker를 구현한 두 개의 프로그래머 클래스, 팀을 관리하기 위한 매니저 클래스이다.
  • Worker에게 일을 시키기위해 Manger를 사용하도록 디자인하는 것은 쉽지 않다.

 

잘못된 경로로의 상속

  • Manager를 JavaProgrammer에서 상속받으면 Manager 클래스에서 구현을 다시 작성할 필요가 없다. 상속으로 디자인해보자.
open class JavaProgrammer : Worker {
Class Manager : JavaProgrammer()
val doe = Manager()
doe.work() //...write Java...
  • 하지만 이렇게하면 Manager 클래스가 JavaProgrammer 클래스에 갇혀버리게 된다.

 

어려운 델리게이션

  • 먼저 Java에서 Manager가 Worker에게 델리게이션을 사용하는 방식을 코틀린 코드로 나타내보자.
class Manager(val worker: Worker) {
  fun work() = worker.work()
  fun takeVataion() = worker.work()
}
val doe = Manager(JavaProgrammer())
doe.work() //...write Java...
  • Manager 인스턴스를 만든 후 JavaProgrammer 인스턴스를 생성자로 전달했다.
  • 이렇게하면 Manager의 인스턴스를 JavaProgrammer, CSharpProgrammer 등 Worker 인터페이스를 구현한 클래스의 인스턴스에게 위힘할 수 있다.
  • 또한 더이상 JavaProgrammer에 open을 입력할 필요가 없다.
  • 하지만 이런 디자인은 코드가 장황할 뿐만 아니라, 소프트웨어 디자인의 기본사항 몇가지도 어기고 있다.
  • Manager는 Worker의 인스턴스를 호출하는 기능만 가지고 있고, 모든 호출 코드는 호출할 메소드명을 제외하고는 거의 비슷하다. 이는 DRY(Don't Repeat yourself) 원칙을 위반한다.
  • 그리고 Work 인터페이스에 deploy() 메소드를 추가한다면 Manger가 해당 메소드를 위임하는 호출을 하기 위해 Manager 클래스도 변경해야 한다. 이는 OCP(Open-Closed Pinciple) 위반이다.

 

코틀린의 by 키워드를 사용한 델리게이션

class Manager() : Worker by JavaProgrammer()
  • 코드 작성 시점에 Manager는 어떤 메소드도 가지고있지 않다.
  • 코틀린 컴파일러는 Worker에 속하는 Manager 클래스의 메소드를 바이트코드 수준에서 구현하고, by 키워드 뒤에 나오는 JavaProgrammer 클래스의 인스턴스로 호출을 요청한다.
  • 즉, by 키워드가 컴파일 시간에 이전 예제에서 수동으로 구현했던 델리게이션을 대신 해준다.
val doe = Manger()
doe.work() //...write Java...
  • 상속이 아니기 때문에 Manger의 인스턴스를 JavaProgrammer 타입의 참조가 필요한 곳에서 사용할 수 없다.
val coder = JavaProgrammer = doe // ERROR: type mismatch

 

파라미터에 위임하기

  • Worker by JavaProgrammer() 코드에선 명시적으로 생성된 JavaProgrammer의 인스턴스로 델리게이트한다.
  • 이렇게 되면 Manager 클래스의 인스턴스는 오직 JavaProgrammer의 인스턴스에만 요청해야 한다.
  • 또한 Manger 클래스 안에 다른 메소드를 작성하더라도 해당 메소드에서는 델리게이션에 접근할 수 없다.
  • 이런 제약은 인스턴스를 생성하면서 델리게이션을 지정하지 않고 생성자에 델리게이션 파라미터를 전달함으로써 해결 가능하다.
class Manager(val staff: Worker) : Worker by staff {
  fun meeting() = println("meeting with ${staff.javaClass.simpleName}")
}
  • staff가 Manger 객체의 속성이기 때문에 meeting() 메소드로 staff에 접근할 수 있다.
  • work() 메소드를 호출할땐 staff가 델리게이션이기 때문에 staff로 요청이 전달된다.
val doe = Manager (CSharpProgrammer())
val roe = Manager (JavaProgrammer())
doe.work() //...write C#...
doe.meeting() //meeting with CShrapProgrammer
roe.work()//...write Java...
roe.meeting() //meeting with JavaProgrammer

 

메소드 충돌 관리

  • 코틀린에서는 델리게이션을 이용하는 클래스가 델리게이션 클래스의 인터페이스를 구현해야한다.
  • 하지만 실제로는 인터페이스의 각 메소드를 모두 구현하지 않는다.
  • 델리게이션 클래스의 모든 인터페이스를 위해 코틀린 컴파일러가 만든 랩퍼를 만든다.
  • 하지만 델리게이션 클래스가 인터페이스의 메소드를 구현하지 않은 경우엔 델리게이션을 이용하는 클래스에서 메소드를 구현해야한다.
  • 델리게이션 클래스가 인터페이스의 메소드를 이미 구현한 상태에서 델리게이션을 이용하는 클래스에서 메소드를 다시 구현하려고 하는 경우엔 override 키워드를 사용해야 한다.
class Manager(val staff: Worker) : Worker by staff {
  override fun takeVacation() = println("of course")
}
  • 클래스는 여러 개의 인터페이스 역시 델리게이션할 수 있다.
  • 만약 인터페이스 사이에서 메소드 충돌이 있다면 후보 클래스가 충돌된 메소드를 오버라이드해야 된다.
interface Worker {
  fun work()
  fun takeVacation()
  fun fileTimeSheet() = println("Why? really?")
}
interface Assistant {
  fun doChores()
  fun fileTimeSheet() = println("NO escape from that")
}
class Manager(val staff:Worker, val assistant : Assistant) :
  Worker by staff, Assistant by assistant {
  override fun fileTimeSheet() {
    print("manually forwarding this..")
    assistant.fileTimeSheet()
  }
}
  • Manger 인스턴스가 fileTimeSheet() 메소드의 호출을 가로채서 Worker와 Assistant에 있는 메소드가 충돌하거나 임의의 메소드로 실행되는 것을 방지할 수 있었다.
  • Manager 인스턴스는 수동으로 Worker에게 요청할지 혹은 Assistant에게 요청할지 아니면 둘다에게 요청할지 결정할 수 있다.

 

 델리게이션 주의사항

  • 지금까지 Manager는 JavaProgrammer의 인스턴스에게 델리게이션을 요청했다. 하지만 Manager의 참조는 JavaProgrammer의 참조에 할당될 수 없다. 델리게이션은 상속과 다르게 우연히 대체될 가능성이 없는 재사용성을 제공해준다.
  • 하지만 델리게이션을 사용할 클래스는 위임할 인터페이스를 구현해야한다. 따라서 델리게이션을 사용하는 클래스를 참조하면 위임할 인터페이스의 참조에 할당될 수 있다.
//불가
val coder: JavaProgrammer = doe //ERROR : type mismatch

//가능
val employee: Worker = doe
  • 델리게이션의 진짜 목적은 Manager가 Worker를 이용하는 것이지만, 코틀린의 델리게이션 구현의 부작용으로 Manger는 Worker로 취급된다.
  • 또한 델리게이션 속성 선언을 val이 아닌 var로 하게 되면 몇 가지 문제가 발생한다.
class Manager(var staff: Worker) : Worker by staff
val doe = Manager(JavaProgrammer())
prinln("Staff is #{doe.staff.javaClass.simpleName}") //Staff is JavaProgrammer
doe.work() //..write Java..
doe.staff=CSharpProgrammer()
prinln("Staff is #{doe.staff.javaClass.simpleName}") //Staff is CSharpProgrammer
doe.work() //..write Java..
  • staff 객체에는 두 개의 참조가 있다. 하나는 클래스 안에 백킹 필드로서 존재하는 참조, 또 하나는 델리게이션의 목적으로 존재하는 참조이다.
  • staff 속성을 CSharpProgrammer 클래스로 변경하면 필드만 변경되고 델리게이션의 참조를 변경한 것은 아니기 때문에 위와 같은 결과가 발생한다.
728x90

'Kotlin > 다재다능 코틀린 프로그래밍' 카테고리의 다른 글

함수형 코틀린 (1)  (0) 2023.09.08
델리게이션을 통한 확장 (2)  (0) 2023.09.08
클래스 계층과 상속  (0) 2023.08.24
객체와 클래스  (0) 2023.08.01
오류를 예방하는 타입 안정성 (2)  (0) 2023.08.01
Comments