본문 바로가기

Kotlin

[Kotlin] 위임 (Delegation)

https://pixabay.com

 

안녕하세요. 오늘은 위임 (Delegation) 에 대해 알아보겠습니다.

 

다른 부서의 코드를 참고하다가 아래와 같은 코드를 봤습니다.

class UserRepository(userDao: UserDao) : UserDao by userDao

 

오늘은 이 코드를 이해해 보도록 하겠습니다.


위임?

위임 (Delegation) 은 객체지향 프로그래밍에서 구성 (Composition)을 활용하여

객체의 일부 기능을 다른 객체에 위임하는 디자인 패턴입니다.

 

코틀린에서는 by 키워드를 통해 위임을 쉽게 구현하도록 도와줍니다.

 

위임은 크게 2가지가 있습니다.

1) 클래스 위임 (Class Delegation)

2) 위임 프로퍼티 (Delegated Properties)


클래스 위임

클래스 위임은 by 키워드를 사용하여 클래스에서 특정 인터페이스의 구현을 다른 객체에 위임하는 방식입니다.

 

예제

인터페이스가 2개 있고, 이를 구현하는 클래스가 2개 있습니다.

interface Printable {
    fun print()
}

interface Clickable {
    fun click()
}

class Printer : Printable {
    override fun print() {
        println("Printing...")
    }
}

class Button : Clickable {
    override fun click() {
        println("Button clicked")
    }
}

 

by 키워드를 사용하지 않은 예제

아래 코드에선, UserInterface 클래스 안에 인터페이스의 구현을 override 해서 정의해야 합니다.

class UserInterface(
    private val printable: Printable, // Printable 타입의 객체를 받음
    private val clickable: Clickable  // Clickable 타입의 객체를 받음
) : Printable, Clickable {

    // Printable의 구현
    override fun print() {
        printable.print() 
    }

    // Clickable의 구현
    override fun click() {
        clickable.click() 
    }
}

 

by 키워드를 사용한 예제

by를 사용하면 아래와 같이 코드를 생략할 수 있습니다.

class UserInterface(printable: Printable, clickable: Clickable) :
    Printable by printable,
    Clickable by clickable

 

override

당연히 필요한 기능만 override해서 써도 됩니다.

class UserInterface(printable: Printable, clickable: Clickable) :
    Printable by printable,
    Clickable by clickable {

    // Clickable의 click 메서드 재정의
    override fun click() {
        println("Custom click action")
    }
}

 

사용

fun main() {
    val ui = UserInterface(Printer(), Button())
    ui.print()   // 출력: Printing... 
    ui.click()   // 출력: Custom click action 
}

위임 프로퍼티

클래스 위임과 똑같이 by 키워드를 이용합니다.

프로퍼티의 getter, setter 동작을 위임 객체의 getValue, setValue 메서드로 처리합니다.

val/var <property name>: <Type> by <delegate>

 

예제

Custom Delegation 객체를 만들어줍니다.

import kotlin.reflect.KProperty

class LoggingDelegate<T>(private var value: T) {

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        println("${property.name} 프로퍼티를 가져옵니다. 값: $value")
        return value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
        println("${property.name} 프로퍼티를 $newValue 로 변경합니다.")
        value = newValue
    }
}

 

by 키워드를 사용해 프로퍼티의 getter, setter를 Custom Delegation 객체에 위임합니다.

class Person {
    var name: String by LoggingDelegate("Unknown")
    var age: Int by LoggingDelegate(0)
}

fun main() {
    val person = Person()
    println(person.name) 
    person.name = "Alice" 
    println(person.name) 
}

위 코드의 출력값


코틀린에서 제공하는 Delegates

코틀린에서 제공하는 위임 클래스가 있습니다.


Delegates.observable

프로퍼티의 값이 변경될 때마다 콜백 함수를 호출합니다.

값이 변할 때마다 특정 로직이 수행됩니다.

import kotlin.properties.Delegates

class User {
    var email: String by Delegates.observable("<no email>") { prop, old, new ->
        println("${prop.name}이(가) $old 에서 $new 로 변경되었습니다.")
    }
}

fun main() {
    val user = User()
    user.email = "user@example.com" // 출력: email이(가) <no email> 에서 user@example.com 로 변경되었습니다.
}

위 코드의 출력값


Delegates.vetoable

프로퍼티의 값 변경을 조건에 따라 허용하거나 거부할 수 있습니다.

import kotlin.properties.Delegates

class Account {
    var balance: Int by Delegates.vetoable(0) { prop, old, new ->
        if (new >= 0) {
            true
        } else {
            println("잔액은 음수가 될 수 없습니다.")
            false
        }
    }
}

fun main() {
    val account = Account()
    account.balance = 100
    println(account.balance) 
    account.balance = -50    
    println(account.balance) 
}

위 코드의 출력값


lazy

지연 초기화 (Lazy Initialization) 을 지원하는 기능으로

객체나 변수를 처음 사용할 때 초기화 로직을 수행할 수 있게 도와줍니다.

fun main() {
    val lazyValue: String by lazy {
        println("Computed!") // 초기화 시에만 실행
        "Hello, Kotlin!"
    }

    println(lazyValue)
    println(lazyValue)
    println(lazyValue)
}

위 코드의 출력값


코드 다시 보기

제일 처음 봤던 코드를 다시 보겠습니다.

class UserRepository(userDao: UserDao) : UserDao by userDao

 

해당 코드는 Kotlin의 delegation 기능을 활용하여

UserRepository 클래스가 userDao 객체에 위임하는 방식입니다.

 

이를 통해 UserRepository가 UserDao의 구현을 직접 작성하지 않고도

UserDao의 기능을 그대로 사용할 수 있습니다.


장점

이 코드를 통해 아래와 같은 장점을 확인할 수 있습니다.

 

1) 코드 중복 감소

UserRepository는 UserDao의 메서드를 직접 구현하지 않아도 되므로

중복 코드가 제거됩니다.

 

2) 유연성 증가

UserRepositoy의 위임 객체를 외부에서 주입하기에, 쉽게 교체할 수 있습니다.

→ 테스트 및 유지보수가 쉬워집니다.

 

예를 들어) 테스트용 DAO (Mock DAO)를 주입하여 테스트를 수행할 수 있습니다.

 

3) 구조적 명확성

UserRepository는 실제로 userDao의 역할을 하는게 아니라

단순히 위임한다는 사실이 명확히 드러납니다.


컴파일에 위임 객체 고정

by를 사용한 위임은 컴파일 타임에 구현이 결정됩니다.

런타임에는 구현이 바뀌지 않는데, 아래 예시를 한번 살펴보겠습니다.


디자인 패턴 중에 전략 패턴이 있습니다.

전략 패턴을 사용하면 런타임에 동적으로 동작을 변경할 수 있다는 장점이 있습니다.

(전략 패턴 설명 : https://mw9911.tistory.com/m/7)

 

전략 패턴(Strategy Pattern)

전략 패턴(Strategy Pattern)전략 패턴은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해줌. 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독집적

mw9911.tistory.com

 

위임 기능을 사용하면 전략 패턴을 훨씬 쉽게 구현할 수 있을거라 생각하고 구현해봤습니다.

 

하지만, by 키워드를 사용한 코틀린 위임은 컴파일 시점에 위임 객체가 고정됩니다.

중간에 전략을 바꿨음에도 반영되지 않고, 똑같은 출력값이 나옵니다.

interface Renderer {
    fun render(content: String)
}

class ConsoleRenderer : Renderer {
    override fun render(content: String) {
        println("Rendering to console: $content")
    }
}

class FileRenderer : Renderer {
    override fun render(content: String) {
        println("Rendering to file: $content")
    }
}

class ContentRenderer(private var renderer: Renderer) : Renderer by renderer {
    fun changeRenderer(newRenderer: Renderer) {
        renderer = newRenderer
    }
}
fun main() {
    val consoleRenderer = ConsoleRenderer()
    val fileRenderer = FileRenderer()

    val contentRenderer = ContentRenderer(consoleRenderer)
    contentRenderer.render("Hello, World!") 

    contentRenderer.changeRenderer(fileRenderer)
    contentRenderer.render("Hello, World!") 
}

위 코드의 출력값

 

'Kotlin' 카테고리의 다른 글