Kotlin - 9. Delegation

2021/11/02
language

이전 글에서 마지막에 delegation(위임)에 대한 기능에 대해 간략하게 정리했습니다.
이번에는 delegation을 이용한 클래스의 확장에 대해 좀 더 깊게 알아보겠습니다.

상속과 위임

일반적으로 상속과 위임은 공통된 코드를 재사용 할 수 있다는 점은 동일하지만 그 영향력은 매우 다릅니다.
많은 디자인패턴 자료에서 상속보다는 위임을 하라는 이야기를 합니다. 무분별 한 상속의 경우 코드 변경에 대한 영향 범위가 너무 넓기 때문에 너무 많은 단계의 상속은 지양하고 위임을 권한다고 합니다. 심지어 자바의 개발자로 알려진 제임스 고슬링이 최근에 만든 고 언어의 경우 상속이라는 개념 자체를 만들지 않기도 했습니다.

그럼 우리가 개발 할 때 이 둘은 어떻게 구분해야 할까요? 상속은 상속받은 클래스를 부모 클래스로 인식하게 되기 때문에(is) 두 클래스가 같은 종류라면 적용할 수 있을 것이고 위임은 그외의 클래스가 같은 종류는 아니지만 해당 기능을 가지고 있을때(has) 사용 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Calculable {
fun calculate()
}

abstract class Cpu: Calculable

class X86Cpu: Cpu {
fun calculate() {
// do something
}
}

class ArmCpu: Cpu {
fun calculate() {
// do something
}
}

class Computer(val cpu: Cpu): Calculable by cpu

간단한 예를 들면 컴퓨터와 cpu 는 계산을 할 수 있습니다. 하지만 Cpu와 컴퓨터는 같은 물건이 아닙니다. (Computer has Cpu)
하지만 pc에 들어가는 x86 cpu 와 스마트폰에 주로 들어가는 arm cpu는 둘 다 같은 cpu 로 취급합니다. (X86Cpu is Cpu)

변수와 속성에 대한 위임

코틀린에서는 클래스 간의 위임 뿐 아니라 단순 변수에 대해서도 위임이 가능합니다.

변수에서의 위임

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DelegateString(var str: String = "") {
operator fun getValue(thisRef: Any?, property: KProperty<*>?) =
"Delegated: $str"

operator fun setValue(thisRef: Any, property: KProperty<*>, value: String) {
str = value
}
}


var msg: String by DelegateString("This is test")
println(msg) // Delegated: This is test
msg = "test!"
println(msg) // Delegated: test!

오버로딩한 getValue, setValue 연산자는 코틀린에서 변수에 값을 할당하거나 값을 사용할 때 실행되는 연산자 메소드 입니다. 따라서 msg 변수에 값을 할당할 땐 DelegateString 의 setValue 메소드가 호출되고 println 등을 통해 값을 사용할 땐 getValue 가 실행됩니다.

클래스 속성에서의 위임

변수에서 위임하듯이 클래스의 속성으로의 위임도 가능합니다. 이런 경우 다음과 같은 기능을 구현하기 쉽습니다.

  1. 지연 로딩
  2. 변화 감지
  3. 실제 필드를 map 으로 관리

코드는 아래와 같이 작성합니다.

1
2
3
4
5
6
7
class Test {
var p: String by DelegateString()
}

val t = Test()
t.p = "test"
println(t.p) // Delegated: test

t.p 를 사용할 때 지정된 메소드가 호출 된 것을 확인 할 수 있습니다.

표준 위임

위에서 설명한 기능을 구현하기 위해 코틀린에서 기본적으로 관련된 기능들을 제공합니다.

lazy

lazy 는 Lazy 라는 인터페이스를 리턴하는 함수로 파라메터로 넘어간 함수를 Lazy 객체로 감쌉니다.

1
2
3
val v by lazy { test() }

println(v) // execute lambda that passed to lazy

필요할 때 함수를 사용하고자 하면 그 때 람다가 실행되는 구조를 가집니다.

observable

1
2
3
4
5
6
7
var v by Delegate.observable("init") {
prop, old, new -> println("$old -> $new")
}

println(v) // init
v = "1" // init -> 1
println(v) // 1

observable은 이름 그대로 변수의 변경을 감지하여 실행될 함수를 바인딩 해줍니다. 같이 정의하는 람다는 리턴값이 없는 람다로 위와 같이 구현 할 수 있습니다.

votoable

1
2
3
4
5
6
7
8
9
10
var v by Delegates.vetoable("init") {
_, _, new -> new.isNotEmpty()
}

println(v) // init
v = ""
println(v) // init
v = "1"
println(v) // 1

vetoable은 변경이 일어날 때 람다를 실행시켜 나오는 결과에 따라 데이터 업데이트를 허용할지 말지 결정하는 메소드 입니다. 위 예제에서 값이 비어있지 않으면 변경을 허용하도록 되어 있기때문에, 빈 문자열을 할당하려 했을때 실패하고 실제 “1”을 할당할 때는 정상적으로 값이 반영되었음을 알 수 있습니다.


출처

다재다능 코틀린 프로그래밍
코틀린 공식문서