Kotlin

Kotlin Delegation

dev_roach 2022. 5. 16. 22:43
728x90

Delegation 은 코틀린에서 지원하는 문법적 기능 중 하나인데, 이는 Delegation Pattern 의 Boiler Plate 를 줄여주는 Sugar Syntex 중 하나이다. Delegation Pattern 은 쉽게 말해 상속이 아닌 다른 객체에게 세부 구현을 위임하는 것이다.

 

공식문서의 예제를 한번 살펴보자.

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() {
        print(x)
    }
}

class Derived(b: Base) : Base by b

fun main() {
    val b = BaseImpl(4)
    Derived(b).print()
}

위의 예제는 Derived 의 내부 인터페이스인 print() 를  BaseImpl 의 구현에 위임했음을 알 수 있다.

만약 자바 코드였다면 어떤 코드였을까? 아마도 아래와 같을 것이다.

interface Base {
    public void print();
}

class BaseImpl implements delegation.Base {

    private final int x;

    public BaseImpl(int x) {
        this.x = x;
    }

    @Override
    public void print() {
        System.out.println(x);
    }
}

class Derived {
    private final delegation.BaseImpl base;

    public Derived(delegation.BaseImpl base) {
        this.base = base;
    }

    public void print() {
        base.print();
    }
}

코틀린에 비해 훨씬 긴 부분을 알 수 있다. 

코틀린에서는 Delegation Pattern 을 사용하는데 위와 같은 Boiler Plate 를 많이 간소화 시켜주었다.

사실 위의 경우는 사용하는 경우가 그렇게 많지는 않겠다는 생각이 들었고, 나는 Delegation Properties 를 사용하는 경우가 많을것 같다는 생각이 들었다. 그래서 Delegation Properties 에 대해 설명해보려고 한다.

Delegation Properties

Delegation Properties 란 Properties 와 관련된 메소드 들을 또 다른 구현체에 위임하는 것이다.

쉽게 예를 들면, 우리는 val / var 를 사용하면 자동적으로 getter / setter 가 생긴다는 것을 알고 있다. 하지만 Delegation Property 의 경우 이를 직접 구현한 구현체를 대입해주어야 한다. 

class Person {
    var name: String by PersonNameDelegator()
}

class PersonNameDelegator {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

fun main() {
    val p = Person()

    println(p.name)
    p.name = "roach"
    println(p.name)
}

위와 같이 직접 입력해주면 결과 값은 아래와 같다.

위와 같은 Delegation Properties 를 공식문서에서 가이드해주는 사용하기 좋은 상황은 아래와 같다.

사용하기 좋은 상황

  • Lazy properties: 객체가 처음 접근되는 순간 평가되면 좋을 때
  • Observable properties: Listener 에게 객체의 변화되는 시점을 전달해줘야 하는 상황일때
  • 각 속성을 별도의 필드로 두는 것보다 맵에 저장할때? (원문 : Storing properties in a map instead of a separate field for each property.)

Delegation Properties By Lazy

우리가 해당 속성을 Lazy 하게 Loading 해야 될때가 있다고 생각해보자.

예를 들면 Person 의 미국식 나이를 구하는 경우에는 처음 미국식 나이가 필요한 시점에 쓰이고, 그 이후에는 계산된 값을 계속 이용하고 싶다고 해보자. 그렇다면 코드를 아래와 같이 작성할 수 있을 것이다.

class Person {
    var name: String by PersonNameDelegator()
    val ageInKorea: Int = 20
    val ageInUSA: Int by lazy {
        ageInKorea - 1
    }
}

위와 같이 ageInUSA 는 바깥에서 person.ageInUSA 를 하기 전까지 계산되지 않은 상태로 있다가. person.ageInUSA 를 호출되는 순간 평가되며, 그 이후로는 계속 재사용된다. 즉, 자원을 최대한 효율적으로 사용할 수 있게 된다.

Observable Properties

class User {
    var name: String by Delegates.observable("<no name>") { prop, old, new ->
        println("${prop.name} Is Change $old to $new")
        println("$old -> $new")
    }
}

fun main() {
    val user = User()

    user.name = "roach"
    println(user.name) // name Is Change <no name> to roach
    user.name = "roach roach"
    println(user.name) // name Is Change roach to roach roach
}

/**
 * Result
 * name Is Change <no name> to roach
 * <no name> -> roach
 * roach
 * name Is Change roach to roach roach
 * roach -> roach roach
 * roach roach
 */

 

개인적으로 위의 Observable 을 보면서 뭔가 @EntityListener 같다는 생각이 들었다.

근데 사실 개념적으로는 비슷하다. EntityListener 또한 객체의 변경이 되는 경우 해당 EntityListener 구현체가 Trigger 되는 것이기 때문이다. 

Delegating to another property

Delegation Properties 를 설명할때 getter / setter 를 구현해줘야 한다고 했던 말을 잘 생각해보면, 다른 Property 를 위임할수도 있겠는데? 라는 생각이 들것이다. 예를 들면 아래와 같이 말이다.

class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
    var delegatedToMember: Int by this::memberInt
    var delegatedToTopLevel: Int by ::topLevelInt

    val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}

위의 방법을 보면서 느낀게 있는데 우리가 보통 Server Code 를 작성하다보면 계층간의 통신을 위해 어떠한 구조로든 DTO 역할을 하는 파일을 만드는데, 단순히 Entity 의 속성을 뒤집어 씌우는 경우들이 있다. 그래서 Convert 를 위해 of / from 메소드를 만들거나 속성이 많다면 ObjcetMapper 를 사용하기도 한다. 하지만 Kotlin 에서는 아래와 같이 풀어볼 수 있지 않을까? 라는 생각이 강하게 들었다.

class Animal(
    var name: String,
    var age: Int,
    var origin: String
)

class AnimalInfo(
    val dbData: Animal
) {
    val name by dbData::name
    val int by dbData::age
    val origin by dbData::origin
    override fun toString(): String {
        return "The Animal is (name='$name', int=$int, origin='$origin')"
    }
}

fun main() {
    val animal = Animal(
        name = "rabbit",
        age = 5,
        origin = "USA"
    )
    val personInfo = AnimalInfo(dbData = animal)

    println(personInfo)
}

위와 같은 방법으로 코드를 작성하면 대부분의 of 를 작성하는 코드의 양을 줄 일수 있을것 같다는 생각이 많이 들었다.

후기

진짜 코틀린은 많은 기능을 제공해서 공식문서를 많이 읽어보면서 하나하나씩 소화시켜봐야겠다는 생각이 들었다.

그래야 좀 더 코틀린 스럽게 적을 수 있을 것 같다.

 

'Kotlin' 카테고리의 다른 글

Effective Kotlin - Item01  (0) 2022.05.25
GC 분석해보기  (0) 2022.05.24
Kotlin Class 내부 object 에서 Class Property 참조하는 법  (0) 2022.05.11
고차함수에서 Return 이 안되는 이유  (1) 2022.05.03
Kotlin DSL  (0) 2022.03.28