본문 바로가기
코틀린/개발자를 위한 코틀린 프로그래밍

[chapter11] 위임(delegation) 확장알아보기

by 측면삼각근 2023. 9. 21.
728x90
반응형

들어가기 전에

본 포스팅은 개발자를 위한 코틀린 프로그래밍의 chapter단위로 공부하고, 정리, 부족한 내용의 추가 학습내용을 정리하는 블로깅입니다.

이전 포스팅 ⬇️

[chapter10] 함수 추가사항 알아보기

 

[chapter10] 함수 추가사항 알아보기

들어가기 전에 본 포스팅은 개발자를 위한 코틀린 프로그래밍의 chapter단위로 공부하고, 정리, 부족한 내용의 추가 학습내용을 정리하는 블로깅입니다. 이전 포스팅 ⬇️ [chapter09] 추상 클래스,

messycode.tistory.com

모든 객체지향 프로그래밍 언어에서는 클래스 간의 상속관계를 기본 문법으로 제공한다. 코틀린에서는 위임 관계를 문법으로 제공해서 다양한 클래스의 관계를 쉽게 처리할 수 있도록 지원한다.


01 클래스 위임 알아보기

1.1 클래스 위임 규칙

  • 위탁자 클래스(delegator), 수탁자 클래스(delegate) 두 클래스가 상속하는 인터페이스로 구성
  • 위탁자 클래스에서 수탁자 클래스의 객체를 속성으로 만든다.
    위탁자 클래스에 인터페이스의 추상메서드를 구현하지만, 내부 처리는 수탁자 클래스가 한다.
    위탁자는 수탁자의 메서드를 래핑 해서 구현하는 방식을 사용하며, 실제 처리는 수탁자가 진행한다.
  • 코틀린에서는 위탁자 클래스의 래핑 처리를 컴파일 타임에 자동으로 만들어주기 위해 by 예약어를 지원한다.
    그래서 별도의 래핑 메서드를 작성할 필요가 없다.

1.2 클래스 위임처리

보통 하나의 클래스에 하나의 책임(responsibility)을 부여해서 설계하는 방식을 사용한다.
하지만 위임은 2개의 클래스가 동일한 책임을 가지고 나눠서 처리한다.

클래스 위임은 동일한 책임을 위탁자가 수탁자에게 일을 맡겨서 처리하도록 만드는 것이다.
다양한 기능을 하나의 클래스를 통해서 받고 처리할 수 있도록 구조화할 수 있어서 좋다.

클래스 위임 정의

interface Base {
    fun say()
}

class BaseImpl(val x: Int) : Base { // 인터페이스 구현한 위임 클래스 (delegated class)
    override fun say()  =  println("BaseImpl say $x")
}

class Derived(b: Base) : Base by b  // Derived 클래스는 Base 인터페이스를 위임 받음


fun main() {
    val b = BaseImpl(10)
    Derived(b).say() // Derived 클래스는 Base 인터페이스를 위임 받았으므로 BaseImpl의 say() 메소드가 호출됨
    // BaseImpl say 10
}

 

 

생성자의 매개변수 속성으로 위임 처리

하나의 인터페이스를 여러 클래스에서 위임을 처리 할 수 있다.

interface Sayable {
    fun say()
}

class Person : Sayable {
    override fun say() {
        println("Hello")
    }
}

class Dog : Sayable {
    override fun say() {
        println("Bark")
    }
}

class Saying(val say: Sayable) : Sayable by say

fun main() {
    val person = Person()
    val dog = Dog()
    
    Saying(person).say() // Hello
    Saying(dog).say() // Bark
}

상속관계 클래스 위임에서 처리 가능

실제 일을 수행하는 수탁자 클래스가 다양한 계층 구조를 가져도
위탁자 클래스와 동일한 인터페이스 구현이 되면 이 인터페이스를 기준으로 위임 관계를 구성할 수 있다.

interface Showable {
    fun show()
}

open class View : Showable {
    override fun show() {
        println("View Show")
    }
}

class Button : View() {
    override fun show() {
        println("Button Show")
    }
}

//인터페이스만 위임 처리 가능
class Screen(val showable: Showable) : Showable by showable

fun main() {
    val view = View()
    val button = Button()

    val screen = Screen(view)
    val screen2 = Screen(button)

    screen.show() // View Show
    screen2.show() // Button Show
}

위임을 통한 Mixin패턴 처리

객체 지향 프로그래밍 언어에서 믹스인(mixin)은 다른 클래스의 부모클래스가 되지 않으면서 다른 클래스에서 상속할 수 있는 메서드를 포함하는 클래스이다.
다른 클래스가 믹스인 메소드에 액세스 하는 방법은 언어에 따라 다르다. 믹스인은 때때로 "상속" 이 아니라 "포함"으로 설명된다.

믹스인 클래스는 필요로 하는 기능들을 포함하는 상위 클래스로서 역할을 한다.
일반적으로 믹스인은 엄격한 단일 관계(is-a)를 만들지 않고 원하는 기능을 하위 클래스에 전달한다.

// example mixin pattern by using delegation
class Balance(val id: Int, var amount: Double) {
    override fun toString(): String {
        return "Balance(id=$id, amount=$amount)"
    }
}

interface Deposiable {
    fun deposit(balance: Balance, amount: Double)
}

class DepositableImpl : Deposiable {
    override fun deposit(balance: Balance, amount: Double) {
        balance.amount += amount
    }
}

interface Withdrawable {
    fun withdraw(balance: Balance, amount: Double)
}

class WithdrawableImpl : Withdrawable {
    override fun withdraw(balance: Balance, amount: Double) {
        balance.amount -= amount
    }
}

class Agreements(val id: Int, val with: Withdrawable, val dep: Deposiable) : Withdrawable by with, Deposiable by dep {
}

fun main() {
    val balance = Balance(1, 100.0)
    val agreements = Agreements(1, WithdrawableImpl(), DepositableImpl())
    agreements.deposit(balance, 100.0)
    println(balance)

    agreements.withdraw(balance, 50.0)
    println(balance)
}

02 속성 위임 알아보기

속성은 게터와 세터 접근자를 정의해서 실제 변수를 참조하는 것이 아니라, 메서드를 참조해서 처리하는 방식이다. 그래서 속성에도 위임을 처리할 수 있다.

2.1 속성 위임 규칙

  • 속성에도 by로 위임을 처리할 수 있다.
  • 코틀린 내부적으로 위임을 처리할 수 있는 notNull, vetoable, observable메서드를 제공하며, lazy함수도 사용할 수 있다.

2.2 속성 위임 정의

속성에 대한 위임은 속성을 사용할 때 특정 값을 세팅해서 처리하는 것이다.
이 때 Delegate객체의 notNull, observable, vetoable를 사용한다.

notNull 위임처리

Delegates object내의 notNull메서드를 사용해서 null값이 들어가지 않도록 처리할 수 있다.

import kotlin.properties.Delegates // 코틀린 속성 지연처리 라이브러리

// 최상위 속성 지연 초기화는 참조 객체만 가능
lateinit var str: String
// 최상위 속성 지연 초기화할때는 기본 자료형은 불가능하다.
// lateinit var int: Int

fun main() {
	// 기본 자료형은 lateinit을 사용할 수 없지만, Delegates.notNull()을 사용하면 가능하다.
    var str1: String by Delegates.notNull<String>()
    var int1: Int by Delegates.notNull<Int>()
    // val은 재할당이 금지되기 때문에, 지연 초기화를 사용하지 못한다.

    // lateinit와 Delegates.notNull() 선언한 변수는 초기화 하지 않으면 에러가 발생한다.
    str = "Hello"
    str1 = "World"
    int1 = 10

    println(str) 
    println(str1) 
    println(int1) 

}

클래스 맴버인 속성의 지연초기화

클래스 속성에도 동일하게 notNull 메서드로 지연초기화 처리를 할 수 있다.

import kotlin.properties.Delegates // 코틀린 속성 지연처리 라이브러리

class Rectangle {
    lateinit var area: Area // 지연 초기화
    fun initArea(param:Area):Unit{ // 메서드를 작성해서 속성 초기화
        this.area = param// 속성 초기화 처리
    }
}

class Area {
    var value: Int by Delegates.notNull<Int>()// 속성 위임으로 지연 초기화
    constructor(value: Int) {
        this.value = value * value
    }
}

fun main() {
    val rect = Rectangle()
    rect.initArea(Area(10)) // 메서드를 통한 속성 초기화
    println(rect.area.value) // 100
}

속성 변경 관찰

속성 위임을 처리하면, 이 속성이 변경되는 상태를 관찰할 수 있다.

import kotlin.properties.Delegates

fun main() {
    var observed = false
    var max: Int by Delegates.observable(0) { _, old, new -> 
        // 속성, 변경 전, 변경 후 3개 매개변수
        println("Old value: $old,New value: $new")
        observed = true // 변경 상태 변경
    }

    println("max: $max")
    println("observed: $observed")

    max = 10
    println("max: $max")
    println("observed: $observed")
    /*
    max: 0
    observed: false
    Old value: 0,New value: 10
    max: 10
    observed: true
    */

}

특정 조건이 일치할 때만 속성 위임 변경

속성 위임을 vetoable 메서드로 처리할 경우는 조건이 만족할 때만 속성 값을 변경한다.

import kotlin.properties.Delegates

fun main() {
    var vetoableField: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
        println("vetoableField: $oldValue -> $newValue")
        newValue >= 0
    }

    println(vetoableField)
    // 0

    vetoableField = 1
    // vetoableField: 0 -> 1

    println(vetoableField)
    // 1

    vetoableField = -1
    // vetoableField: 1 -> -1

    println(vetoableField)
    // 1 => 값이 변경되지 않음
}

속성과 지역변수 지연 초기화

속성 위임을 사용하는 지연처리는 val속성과 val 변수에서 가능하다.

val a: Int by lazy {0} // 초기화를 늦게 하기 위해 lazy 사용

class LazyVar {
    val lazya : String by lazy {"초기값"} // 클래스 내의 val 지연 처리
    lateinit var lateb : String // 클래스 내의 var 지연 처리
}

fun main() {
    println(a) // 초기화 되지 않았으므로 0 출력
    val b = LazyVar() // 객체 생성

    println(b.lazya) // 초기화 되지 않았으므로 "초기값" 출력

//    println(b.lateb) // lateinit으로 선언된 변수는 초기화 되지 않으면 에러 발생
    b.lateb = "지연 초기화"
    println(b.lateb) // "지연 초기화" 출력

    b.lateb = "값 갱신"
    println(b.lateb) // "값 갱신" 출력
}

맵 자료형을 사용한 속성 위임 처리

속성 위임을 map객체에 저장하고, 이 값을 초기화 값으로 활용할 수 있다.

class Person(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
}

fun main() {
    val person = Person(mapOf(
        "name" to "John Doe",
        "age" to 25
    ))
    // name = John Doe, age = 25
    println("name = ${person.name}, age = ${person.age}")
}

2.3 클래스를 만들어 속성 위임 처리

코틀린에서 기본으로 제공하는 속성은 게터와 세터를 구성하는 별도의 클래스로 정의해 by 예약어에 객체를 생성해서 처리하는 구조이다.

클래스를 정의해서 속성 위임하는 클래스 정의

class Delegate{
    private var value:String? = null
    fun getValue_():String{
        return value?:"초기값"
    }

    fun setValue_(value:String){
        this.value = value
        println("속성 위임 갱신 ")
    }
}

class Bar {
    val del = Delegate()
    var p :String
        get() = del.getValue_()
        set(value) = del.setValue_(value)
}

fun main() {
    val bar = Bar()
    println(bar.p) // 초기값
    bar.p = "Hello" // 속성 위임 갱신
    println(bar.p) // Hello

}

 

 

반응형