코틀린 델리게이션

상속과 델리게이션

상속과 델리게이션 모두 객체지향 프로그래밍 디자인 방식 중 하나이다. 이 둘의 공통점은 클래스를 다른 클래스로 부터 ‘확장'하는 것이다.

코틀린은 이런 상속과 델리게이션 두 가지 방식을 모두 지원 해준다.

상속의 경우 부모 클래스로부터 속성, 메서드 등 을 가져올 때, 클래스가 선택을 할 권한이 없다. 또한 자식 클래스에 부모클래스에 대한 정보(속성, 메서드)가 귀속 된다. 하지만 델리게이션은 이런 상속 보다 유연하다고 한다.

그렇다면 사람들은 왜 상속을 사용하고 델리게이션을 쓰지 않을까? 아마 대부분의 프로그래밍 언어에서 델리게이션을 제대로 지원해주지 않기 때문이라고 한다.

사실 나도 코틀린을 공부하면서 델리게이션이라는 개념을 알게 되었다. 자바에서도 이를 사용할 수 있다고는 하지만, 그렇게 언어적으로 친철?하게 지원해주지는 않는다고 한다. 새롭게 나오는 언어들에서 많이 지원한다고 한다.

그렇다면 상속은 델리게이션에 비해 좋지 않은가?(뒤떨어지는 기술인가?) 라는 생각이 들었다.

정답을 그렇지는 않다. 상황에 따라 다르다.

그렇다면 어떤 상황에서 델리게이션을 쓰고 상속을 써야할까? ‘다재다능 코틀린’책에서는 이렇게 규정한다.

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

상속의 경우 리스코프 치환 원칙에 의해서 자식 클래스는 부모 클래스의 메소드를 오버라이드 할 때, 부모 클래스의 외부 동작을 유지해야함 → 이런 행위(수정)은 오류를 발생 시킬 수 있다. → 상속을 사용하면 자식 클래스 설계 시 제약사항 발생

but 델리게이션 클래스는 상속과 다르게 인스턴스의 분리가 가능하고 상속에 비해 제약사항이 줄어듬 → 유연성이 강화됨

만약 철수는 사람이다 처럼 포함 관계의 경우 클래스를 ‘상속'으로 구현, 반대로 오직 다른 객체의 구현을 재사용 하는 경우(Manager는 Assistant를 가지고 있고, Assistant에게 일을 넘기기만 하는경우)라면 델리게이션 사용


기존 자바 스타일대로 구현한 코드를 통해 델리게이션의 이점을 알아보자

예제를 통해 기존 자바 방식의 상속과 자바 코드로 짜본 델리게이션을 비교해보면서 코틀린의 델리게이션에 대해 알아보자

우선 기존의 자바와 같은 언어에서 상속을 통해, worker와 Manager를 구현하는 것을 코틀린으로 해보겠다.

예제 코드 1 - 기존 자바의 상속을 이용한 Woker들과 Manager 구현 - 코틀린 ver

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

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

class CsharpProgrammer :Worker{
    override fun work() =println("... write Csharp...")
    override fun takeVacation() =println("...branch at the ranch...")
}

class Manager() : JavaProgrammer()

fun main() {
    val manager = Manager()
    manager.work()
}

코드를 확인해 보자. Worker 인터페이스를 상속받는 JavaProgrammer와 sharpProgrammer 클래스를 각각 구현하였다. 또한 Worker를 상속 받는 Manager 클래스가 있다. 이 구조는 문제점이 있다. 우선 Manager 클래스가 JavaProgrammer 클래스에 갇혀버린다.(의존도가 매우 높음)

그렇기 떄문에 Manager 클래스는 CsharpProgrammer로 부터 제공되는 구현을 사용할 수 없다. → 사실 델리게이션을 모르는 입장에선 당연한 소리다


그렇다면 델리게이션을 적용해보자! (자바 버전)

  • 자바에서 구현하는 델리게이션을 코틀린으로 구현
interface Worker{
    fun work()
    fun takeVacation()
}

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

class CsharpProgrammer :Worker{
    override fun work() =println("... write Csharp...")
    override fun takeVacation() =println("...branch at the ranch...")
}

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

fun main() {
    val manager = Manager(JavaProgrammer());
    manager.work()
}

위 코드를 살펴보자. 기존의 코드와 다르게 Manager 클래스는 생성자 주입을 통해 Worker을 받을 수 있다. 이는 기존의 JavaProgrammer에게 강하게 의존적이었던 Manager가 유연하게 worker의 구현체들을 받을 수 있도록 해준다.

또한 기존에 open으로 선언된 JavaProgrammer 클래스를 open으로 선언하지 않아도 된다. 하지만 이런 구조는 객체지향적으로 좋지가 않다.

우선 Manager의 입장에서 주입 받은 객체의 메소드를 사용하려면 호출 메소드를 모두 구현해야하는 불편함이있다. 위 코드를 예시로 Manager에 주입된 worker의 메소드 개수 만큼 Manager는 호출 메소드반복 구현해야하한다. 만약 JavaProgrammer가 100개의 메소드를 가지고 있다면, 이를 생성자로 입력받은 Manager는 javaProgrammer의 기능을 사용하기 위해 호출 메서드를 100번 구현해야한다. →DRY(Don’t Repeat Yourself, 반복하지 말 것!) 원칙에 위배된다.

또한 OCP(Open-Closed Principle, 개방 폐쇄 원칙)에도 위배가 된다. 확장에는 열려 있어야하고, 변경에는 닫혀있어야 한다. 하지만 위와 같은 구조는 그렇지 않다. Worker 인터페이스에 새로운 메서드가 추가 된다면, 이를 호출하기 위한 메서드를 Manager 클래스에서 추가해줘야한다. → 결과적으로 델리게이션을 사용하기 위해서는 하위 클래스에서 호출 메서드를 만들어야하는 불편한 작업을 해야한다


코틀린의 by 키워드

그렇다면 코틀린은 기존의 이런 불편함을 어떻게 없앴을까??

바로 by 키워드를 통해 이러한 불편함을 해결했다.

그렇다면 이전에 우리가 만들어둔 코드를 by 키워드를 이용해 리팩터릴 해보자

by를 이용한 델리게이션 적용 후

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

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

class CsharpProgrammer : Worker {
    override fun work() = println("... write Csharp...")
    override fun takeVacation() = println("...branch at the ranch...")
}

class Manager() : Worker by JavaProgrammer()

fun main() {
    val manager = Manager()
    manager.work()
    manager.takeVacation()
}

달라진 점이 보이는가? 바로 기존에 있는 Manager 클래스의 호출 메서드(work, takeVacation)들이 사라졌다.!!

우리가 코틀린 by 키워드를 사용할 때, 꼭 인지하고 넘어가야할 내용 두가지가 있다.

첫번째로 Manager 클래스는 JavaProgrammer를 상속 받지 않는 다는 것이다. 이는 둘의 타입이 다르다는 이야기다.

val coder: JavaProgrammer = manager // ERRPR : type mismatch

두번째로, 상속을 사용한 구조에서는 work() 같은 메소드는 Manager 클래스에서는 구현되지 않았다. 대신 베이스 클래스(부모 클래스)로 요청을 넘겼다.
하지만 코틀린의 델리게이션을 이용하면 컴파일러가 내부적으로 Manager 클래스에 요청 메서드를 만든다. 따라서 우리는 보이지 않는 work() (내부적으로는 코틀린이 만들어둠)을 호출 할 수 있다.


위에서는 우리는 by 키워드를 이용해 코드를 획기적으로 리팩터링할 수 있었다.

하지만 이런 코드도 역시 제약사항이 있다. 이에 대한 해결책을 확인해보자

우선 어떤 문제점을 갖고 있는지 확인해보자

  • Manger는 JavaProgrammer에게만 요청가능
  • Manage의 인스턴스는 델리게이션에 접근 할 수 없음
    → Manager 클래스 내부에 다른 메소드를 작성할 때, 델리게이션에 접근 할 수 없음.
    → 이말은 상속아니기 때문에, 델리게이션에 접근 못한다고 이해함.
    (Manger 내에서 JavaProgrammer에 접근 불가능)

이런 두가지 문제점을 해결해보자

파라미터에 위임하기

델리게이션 적용 후 파라미터에 이임하기 예제 코드

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

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

class CsharpProgrammer : Worker {
    override fun work() = println("... write Csharp...")
    override fun takeVacation() = println("...branch at the ranch...")
}

class Manager(val staff: Worker) : Worker by staff{
    fun meeting() = println("${staff.javaClass.simpleName}와 미팅을 잡습니다.")
}

fun main() {
    val manager1 = Manager(JavaProgrammer())
    manager1.work()
    manager1.takeVacation()
    manager1.meeting()

    val manager2 = Manager(CsharpProgrammer())
    manager2.work()
    manager2.takeVacation()
    manager2.meeting()
}

위 코드를 살펴보자, 생성자 주입을 통해 Manager는 JavaProgrammer 뿐만아니라 CsharpProgrammer 까지 사용할 수 있게 되었다. 또한 Woerker의 구현체는 모두 받을 수 있게 되었다. 또란 staff라는 파라미터를 val로 받음으로써 staff를 델리게이션으로 사용 할 수 있다.

결과

좋은 웹페이지 즐겨찾기