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 |