델리게이션은 객체지향 프로그래밍의 디자인 방식이다.

상속과 델리게이션 모두 클래스를 다른 클래스로 부터 확장시키는 것이다.

델리게이션은 상속보다 유연한 개념으로, 객체가 자신이 처리해야할 일을 다른 클래스의 인스턴스에게 위임하거나 넘겨버릴 수 있다. 서로 다른 클래스의 인스턴스끼리 위임을 할 수 있다.

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

  • 다형성이 필요하면 상속을 사용하라.
  • 클래스의 객체가 단순히 다른 클래스의 객체를 사용만 해야한다면 델리게이션을 사용하라.

출처: 다재다능 코틀린

상속을 받는 구조에서 Candidate 클래스의 인스턴스는 내부에 BaseClass의 인스턴스를 같이 가지고 있다고 볼 수 있다.

BaseClass타입으로 Candidate인스턴스를 사용할 수 도 있다. 이런 상속을 통한 다형성을 얻을 수 있다.

하지만, 상속받은 클래스를 자식 클래스에서 마음대로 바꾸려고 하면 오류를 일으킬 있다. 

또한 "자료형 S가 자료형 T의 서브타입라면 필요한 프로그램의 속성의 변경 없이 자료형 T의 객체를 자료형 S의 객체로 교체할 수 있어야 한다"라는 리스코프 치환원칙(LSP)를 지키려면 제약사항이 생기게 된다.

위 오른쪽 그림에서 Candidate 클래스가 DelegationObject로 델리게이션을 하면, Candidate 클래스의 인스턴스는 델리게이션의 참조를 갖는다.

Caller가 Candidate에게 요청을 보내면 Candidate인스턴스는 적절하게 위임하여 처리한다.

상속과 델리게이션 선택

상속

"개는 동물이다"와 같이 포함관계에 있는 다른 클래스로 대체할 때 상속을 사용한다.

델리게이션

오직 다른 객체의 구현을 재사용하는 경우라면 델리게이션을 사용한다.
Manager는 Assistant를 가지고 있고, Assistant에게 일을 넘기기만 한다면 델리게이션을 사용하라.

상속에서 델리게이션으로

상속을 이용해서 작은 문제를 설계해보고, 상속이 방해요소로 변하는 시점과 문제해결을 위해 델리게이션을 사용하는 이유를 알아보자.

상속의 문제

기업의 소프트웨어 프로젝트를 시뮬레이션하는 어플리케이션을 만들 것이다.

먼저 일을 할 작업자인 Worker 인터페이스를 만들자.
Worker는 2가지를 할 수 있다. 1. 일을 하거나, 2. 휴가를 떠난다.

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

Worker가 될 수 있는 JavaProgrammer와 CSharpProgrammer 두 개의 클래스를 구현해보자.

또한 이 Worker들을 관리하기 위한 매니저가 필요하다.

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

class CSharpProgrammer : Worker {
    override fun work() = prinln("...write C#...")
    override fun takeVacation() = println("...branch at the raanch...")
}

class Manager

매니저는 아무일도 하지 않는다.

매니저를 어떻게 설계해야할까?

일단 매니저는 Worker 들에게 work(), takeVacation()을 명령할 수 있어야한다.

 1. 상속을 통해 구현하기

class Manager : JavaProgrammer()
val manager = Manager()
manager.work()
  • JavaProgrammer, CSharpProgrammer를 상속받아 매니저를 구현하면, Manager클래스에서 메소드를 다시 작성할 필요가 없다.
  • 그냥 부모클래스의 함수를 호출하기만 하면 된다.

이런 방식은 문제가 많다. 매니저가 특정 클래스에 갇혀버리게 된다. 특정 언어마다 매니저가 존재해야한다.

우리가 원하는 것은 Manager가 모든 worker에게 일을 시킬 수 있는 구조이다.

이것을 델리게이션을 통해 할 수 있다.

2. 델리게이션 사용하기 (Java 방식)

class Manager(val worker: Worker) {
    fun work() = worker.work()
    fun takeVacation() = worker.takeVacation()
}

val manager = Manager(JavaProgrammer())
manager.work()
  • 매니저 클래스의 생성자로 JavaProgrammer() 를 넘겨주었다.
    • Manager가 JavaProgrammer클래스에 강하게 묶이지 않는다.
    • Worker를 구현한 어떤 클래스든 넘길 수 있다.
    • Manager의 인스턴스는 Worker인터페이스를 구현한 클래스에 인스턴스에게 위임할 수 있다.
  • JavaProgrammer 클래스에 open을 적지 않아도 된다.

하지만, 이 방식에도 문제가 있다.

매니저의 work()메소드는 Worker의 인스턴스를 호출만 하고 있다.

개방-폐쇄 원칙(OCP)를 생각해보면, 확장에는 열려있고, 변경에는 닫혀있어야한다.

현재 구조에서 Worker인터페이스에 메소드를 추가한다면 Manager클래스도 해당 메소드를 호출하는 메소드를 추가해야한다.
이런 방식은 OCP를 위반한다.

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

코틀린에서는 개발자가 직접 손대지 않고도 컴파일러에게 코드를 라우팅하도록 할 수 있다.

class Manager() : Worker by JavaProgrammer()
val manager = Manager()
manager.work()

Manager는 어떤 메소드도 가지고 있지 않다. 

코틀린 컴파일러는 Worker에 속하는 Manager클래스의 메소드를 바이트코드 수준에서 구현하고, by 키워드 뒤에 나오는 JavaProgrammer 인스턴스로 호출을 라우팅한다.

by왼쪽에는 인터페이스, 오른쪽에는 인터페이스를 구현한 클래스가 필요하다.

파라미터에 위임하기

위 예제의  문제점

  1. Manager 클래스의 인스턴스는 오직 JavaProgrammer의 인스턴스에만 요청할 수 있다.
  2. Manager의 인스턴스는 델리게이션에 접근할 수 없다.

이런 제약은 인스턴스를 생성할 때, 생성자 파라미터로 델리게이션을 전달하면 해결할 수 있다.

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

val manager1 = Manager(JavaProgrammer())
val manager2 = Manager(CSharpProgrammer())

메소드 충돌 관리 

델리게이션을 사용할 때 메소드가 충돌되는 경우는 어떤게 있을까?

  1. 델리게이션 메소드와 클래스 내부 메소드의 선언이 같은 경우
  2. 두 개 이사의 인터페이스를 델리게이션하는 경우

델리게이션 메소드와 클래스 내부 메소드의 선언이 같은 경우

코틀린 컴파일러는 델리게이션에 사용되는 클래스마다 델리게이션 메소드를 위한 Wrapper를 만든다.

Worker 인터페이스는 work()메소드를 가지고 있다. 만약 Manager 클래스도 work() 메소드를 가지고 있다면 둘 중에 어느 것을 호출해야할까?

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

클래스 안에 델리게이션 인터페이스와 같은 이름, 시그니처의 메소드를 사용하려면 override 키워드를 명시적으로 사용해야한다.

이렇게 함으로써 코드를 읽는 사람은 해당 메소드가 어쩌다보니 같은 이름으로 생성된 게 아니라, 인터페이스의 메소드를 구현했다는 사실을 명확히 알 수 있다.

코틀린 컴파일러는 work() 메소드의 wrapper를 생성하지 않고, takeVacation()의 wrapper만 생성한다.

두 개 이상의 인터페이스를 델리게이션 하는 경우

코틀린에서 클래스는 여러 개의 인터페이스를 델리게이션 할 수 있다.

이런 인터페이스 사이에서 메소드 충돌이 있다면 어떻게 될까?

interface Worker {
    fun work()
    fun takeVacation()
    fun fileTimeSheet() = println("Hello")
}

interface Assistant {
    fun doChores()
    fun fileTimeSheet() = println("Hi")
}

class DepartmentAssistant : Assistance {
    override fun doChores() = prinltn("HHHH")
}

같은 메소드 선언을 갖는 인터페이스가 2개 있을 때,

class Manager(val staff: Worker, val assistant: Assistant) :
  Worker by staff, Assistant by assistant {
  
  override fun fileTimeSheet() {
    assistant.fileTimeSheet()
  }
}

충돌된 메소드를 오버라이딩 해야한다.

 

 

+ Recent posts