본문 바로가기

스터디/다재다능 코틀린 프로그래밍

9. 델리게이션을 통한 확장

다재다능 코틀린 프로그래밍 책으로 코틀린 스터디를 진행하면서 발표를 위해 준비했던 글입니다.


  • '개는 동물이다'와 같은 확실한 IS-A 관계일 때만 상속을 사용하고 그외엔 델리게이션을 사용하자.
  • 코틀린은 델리게이션을 사용할 때 중복 코드 작성을 예방해준다.

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

  • Java를 사용한 델리게이션의 문제점
    1. DRY(Don't Repeat Yourself)원칙 위반: 위임하는 객체의 메소드가 늘어날 수록 위임을 사용하는 클래스의 중복 코드가 늘어난다. 메소드 내부에서 메소드명이 거의 비슷한 델리게이션 객체 메소드를 호출하는 형태들
    2. OCP(Open-Closed Principle)원칙 위반: 위임하는 객체가 수정되면 위임을 사용하는 클래스도 수정되어야한다.
  • 언어의 지원이 부족하기 때문에 생기는 문제점

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

  • 가장 간단한 형태의 델리게이션
interface Worker {
    fun work()
}
class JavaProgrammer: Worker {
    override fun work() {
        println("write java")
    }
}
class CProgrammer: Worker {
    override fun work() {
        println("write C")
    }
}
class Manager() : Worker by JavaProgrammer()
val manager = Manager()
manager.work()
  • Manager 클래스는 JavaProgrammer 클래스를 상속받지 않는다.
    • JavaProgrammer 타입 참조가 필요한 곳에 사용하면 오류 발생
  • 컴파일러가 내부적으로 Manager 클래스에 JavaProgrammer 메소드를 생성하고 요청한다.
    • 다시 말해서 직접 구현하지 않아도 코틀린 컴파일러에 의해 합성된다.

파라미터에 위임하기

  • 위에서 살펴본 가장 간단한 델리게이션 형태의 문제점
    1. JavaProgrammer 인스턴스에만 요청이 가능, 다른 종류는 불가
    2. Manager 클래스 내부에서는 델리게이션한 객체에 접근이 불가능하다.
  • 생성자에 델리게이션 파라미터를 전달함으로써 해결 가능
class Manager(val staff: Worker) : Worker by staff {
    fun meeting() = println("meeting with ${staff.javaClass.simpleName}")
}
val javaManager = Manager(JavaProgrammer())
val CManager = Manager(CProgrammer())
  • 델리게이션할 객체를 파라미터에서 val로 받으면 해당 객체의 속성이 되기 때문에 클래스 내부 meeting 메소드에서 접근이 가능하다.
  • val로 받지 않으면 속성이 되지 않고 그냥 델리게이션으로 사용한다.

메소드 충돌 관리

  • 델리게이션을 사용하는 클래스는 코틀린 컴파일러가 랩퍼를 만든다.
  • 델리게이션을 사용하는 클래스에서 델리게이션 객체와 메소드 시그니처가 같은 메소드를 구현하는 경우엔 override 키워드로 우선순위를 부여해 충돌을 해결한다.
class Manager(val staff: Worker) : Worker by staff {
    override fun work() {
        println("manager work")
    }
}
  • 여러 개의 인터페이스를 델리게이션하는 경우 메소드 충돌
class Manager(val staff: Worker, val assistant: Assistant) :
    Worker by staff, Assistant by assistant {
        override fun eat() {
            assistant.eat()
        }
}
  • 델리게이션하는 인터페이스들 중 메소드 시그니처가 같은 메소드가 있다면 override 하지 않으면 컴파일 에러가 난다.
  • override로 충돌을 방지해줘야하고 override한 메소드에서 수동으로 어떤 위임 객체를 사용할 지 선택하면 된다.

델리게이션 주의사항

  • 델리게이션의 진짜 목적은 Manager가 Worker를 이용하는 것, 하지만 코틀린의 부작용으로 Manager 타입이 Worker 타입의 한 종류가 된다.
interface Worker {
    fun work()
}
class JavaProgrammer() : Worker {
    override fun work() {
        print("work")
    }
}
class Manager(val staff: Worker) : Worker by staff {
}
val coder : JavaProgrammer = Manager() // 에러
val manager : Worker = Manager(JavaProgrammer()) // 가능
  • 델리게이션을 var로 사용할 때의 주의 사항
interface Worker {
    fun work()
}
class JavaProgrammer() : Worker {
    override fun work() {
        println("java work")
    }
}
class CProgrammer() : Worker {
    override fun work() {
        println("C work")
    }
}
class Manager(var staff: Worker) : Worker by staff {
}
val manager = Manager(JavaProgrammer())
manager.work()
manager.staff = CProgrammer()
manager.work()
  • 위 코드의 출력 결과를 예상과 다르게 work() 두 번 다 "java work"를 출력한다.
  • 델리게이션을 사용하는 객체는 두 개의 참조가 있다.
    1. 백킹 필드로서 존재하는 참조
    2. 델리게이션의 목적으로 존재하는 참조
  • 객체를 생성할 때 staff란 이름의 멤버에 파라미터로 받은 객체를 할당한다. (마치 this.staff = staff)
  • 속성을 변경해도 필드만 변경되고 델리게이션의 참조가 변경되지 않는다.
  • 뿐만 아니라, 원래 사용하던 JavaProgrammer 인스턴스에 접근할 수 없게 된다.

변수와 속성 델리게이션

변수 델리게이션

  • 속성이나 지역변수를 읽을 때, 코틀린 내부에서는 getValue() 함수를 호출
  • 속성이나 변수를 설정할 때, 코틀린 내부에서는 setValue() 함수를 호출
  • 객체의 델리ㄱ이션을 위의 두 메소드와 함께 제공함으로써 객체의 속성과 지역변수를 읽고 쓰는 요청을 가로챌 수 있다.
import kotlin.reflect.KProperty

var comment = "This is Stupid"
println(comment) // This is stupid

class PoliteString(var content: String) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>) =
        content.replace("stupid", "s*****")
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        content = value
    }
}

var comment2: String by PoliteString("some message")
println(comment2) // some message
comment2 = "This is stupid"
println(comment2) // This is s*****

속성 델리게이션

import kotlin.reflect.KProperty

class PoliteString(val dataSource: MutableMap<String, Any>) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>) =
        (dataSource[property.name] as? String)?.replace("stupid", "s*****") ?: ""

    operator fun setValue(thisRef: Any, property: KProperty<*>, value: String) {
        dataSource[property.name] = value
    }
}

class PostComment(dataSource: MutableMap<String, Any>) {
    val title: String by dataSource
    var likes: Int by dataSource
    val comment: String by PoliteString(dataSource)
    override fun toString() = "Title: $title Like: $likes Comment: $comment"
}

val postComment = PostComment(
    mutableMapOf(
        "title" to "Using Delegation",
        "likes" to 1,
        "comment" to "stupid"
    )
)
postComment.likes++
println(postComment.toString())

// Title: Using Delegation Like: 2 Comment: s*****
  • MutableMap도 getValue(), setValue()를 가지고 있기 때문에 델리게이션을 사용할 수 있다.
  • 속성을 다른 객체에게 위임할 수 있고 내부적으로 필드를 저장하고 있을 수도 있다.

빌트인 스탠다드 델리게이션

지연 델리게이션

  • lazy 랩퍼 함수 사용하기
fun getTemperature(city: String) : Double {
    println("this $city")
    return 30.0
}
val city = "seoul"
var isShow = false
val temperature by lazy { getTemperature(city) }
if (isShow && temperature > 20) println("warm") else println("cold")
  • lazy 랩퍼 함수를 사용해서 결과가 진짜 필요할 때 까지 식을 실행하지 않도록 지연 연산을 요청할 수 있다.
  • 식 결과가 어떠한 경우에도 필요하지 않으면 식 전체를 스킵해 버린다.
  • 람다 표현식이 실행되면 델리게이션은 결과를 저장하고 있다가 미래에 요청이 있으면 저장된 값을 알려준다.
    • 람다 표현식이 다시 실행되는게 아님

observable 델리게이션

  • observable() 메소드를 사용하면 변화를 지켜볼 수 있다.
import kotlin.properties.Delegates.observable

var count by observable(0) { property, oldValue, newValue ->
    println("property: $property oldValue: $oldValue newValue: $newValue")
}
println("count is $count")
count++
println("count is $count")

// count is 0
// property: ....count: kotlin.Int oldValue: 0 newValue: 1
// count is 1
  • observable 핸들러로 등록하면 모니터링과 디버깅 목적으로 사용할 때 매우 유용하다.

vatoable 델리게이션

  • 지켜보기만 하는 게 아니라 변경을 허가할건지 말건지를 정하려면 vetoable 델리게이션을 사용하자.
import kotlin.properties.Delegates.vetoable

var count by vetoable(0) { _, oldValue, newValue -> newValue > oldValue}
println("count is $count")
count++
println("count is $count")
count--
println("count is $count")

// count is 0
// count is 1
// count is 1
  • vetoable 핸들러를 등록하면 Boolean 결과를 리턴받을 수 있다. true 허가 / false 불허