본문 바로가기
Android & Kotlin/Kotlin

[Kotlin] 클래스 위임 : by / 일급 객체 / 데코레이터, 위임 패턴

by 말린밴댕이_공부 2024. 5. 7.
반응형

대규모 객체지향 시스템을 설계할 때 시스템을 취약하게 만드는 문제는 보통 구현 상속에 의해 발생합니다.

 

하위 클래스가 상위클래스의 일부를 오버라이드하면서 세부 구현사항에 의존하게 되고 계속 개발을 하게 되면 상위 클래스의 구현이 바뀌거나 상위 클래스에 새로운 메서드가 추가 되는 문제가 발생합니다.

 

이를 바탕으로 코틀린은 자바와달리 기본적으로 클래스를 final로 취급하여 상속을 염두에 두어 open 변경자로 클래스를 확장한다.

 

상속을 허용하지 않는 클래스에 새로운 동작을 추가해야 할 때가 있는데 이를 사용하는 일반적인 방법은 데코레이터(Decorator)패턴과 객체의 동작을 다른 객체에게 위임하는 메터니즘인 위임(Delegate Pattern)패턴 이다.


데코레이터(Decorator) 패턴

객체의 동작을 확장하거나 수정하지 않으면서 객체에 새로운 동작을 추가하는 패턴

위임(Delegate) 패턴

+객체의 동작을 다른 객체에게 위임하는 메커니즘인 하나의 클래스가 다른 클래스에게 일부 동작을 위임하여 코드 재사용을 하게 되는 위임(Delegate)패턴이다.

*위임 : 코드 재사용을 위해 일부 동작을 다른 객체에게 위임


 

기존 기능이 그대로 필요한 부분은 데코레이터 메서드가 기존 클래스의 메서드에게 요청을 전달하지만 이런 접근 방법의 단점은 오버라이드 되는 메서드들이 아주 많다는 것이다.

 

아래는 customCollection에 대해서 Colletion의 단순한 인터페이스를 구현하면서 아무 동작을 변경하지 않는 데코레이터를 만들 때 조차 복잡한 코드이다.

class CustomCollection<T> : Collection<T> {

    private val innerList = arrayListOf<T>()

    override val size: Int
        get() = innerList.size

    override fun contains(element: T): Boolean = innerList.contains(element)
    override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
    override fun isEmpty(): Boolean = innerList.isEmpty()
    override fun iterator(): Iterator<T> = innerList.iterator()

    fun add(element : T){innerList.add(element)}
    // 다른 메서드들 ....
}

 

코틀린에서는 **1급 객체(First Class Citizen)**를 지원하므로 인터페이스를 구현할 때 by 키워드를 통해 위임을 명시할 수 있다.

(1급 객체는 맨 하단에 상세 설명.)

 

class CustomCollection<T>(innerLst : Collection<T> = ArrayList<T>())
    : Collection<T> by innerLst{
    
    private val innerList = ArrayList(innerLst)
    
    // No Override!
    
    fun add(element : T) {innerList.add(element) }
    // 다른 메서드들 ....
}

모든 메서드 정의가 사라졌다..!

 

이렇게 일부 필요한 동작에 대한 메서드만 override를 하면 컴파일러가 생성한 메서드 대신 오버라이드한 메서드를 쓰이게 된다는 것이다.

 

innerList라는 외부 컬렉션을 받아 Collection<t> 인터페이스 메서드를 위임하여 처리하며 fun add와 같이 기존 클래스를 확장하여 새로운 동작을 추가하는 데코레이터 패턴의 예시를 간단히 보았다.

 

안드로이드에서의 위임 예시

ViewModel을 UI에 연결을 할때 뷰모델에 관한 참조를 만드는 간단한 예시로 알 수 있습니다.

class MyPageViewModel @Inject constructor(
		//~~
) : ViewModel() {

//위임을 통한 MyPageViewModel초기화
private val viewModel : MyPageViewModel by viewModels()

//생성자를 사용하여 뷰모델 초기화
private val viewModel = MyPageViewModel()

그냥 생성자를 통해 초기화하면 기기에서 생명주기가 변할 때 뷰모델의 참조의 상태를 손실하게 되어 소멸된후 다시 생성되는 문제가 발생합니다.

 

이를 위임을 통해 viewModel 객체의 책임을 viewModels라는 별도의 클래스에 위임을 하여 viewModels에 의해 내부적으로 처리를 하는 방식으로 위임 예시를 한번 더 상기를 할 수 있다.

 

 

 

 

일급 객체(First Class Citizen)

:다른 객체들에 일반적으로 적용 가능한 모든 연산을 지원하는 객체로 세가지의 조건을 충족하면 1급 객체로 볼 수 있다.

  1. 변수나 데이터에 할당 가능
  2. 함수(객체)의 인자로 전달 가능

3.함수(객체)의 반환값으로 전달 가능

// 1. 변수나 데이터 할당
val varFunction: () -> Unit = { println("변수나 데이터 할당") }
val dataFunction = listOf(
    { println("변수나 데이터 할당 1") },
    { println("변수나 데이터 할당 2") },
    { println("변수나 데이터 할당3") }
)

// 2. 함수(객체)의 인자 전달
fun argumentFunc(function: () -> Unit) {
    function.invoke()
}

// 3. 함수(객체)의 반환값 전달
fun returnFunction(): () -> Unit {
    return { println("일급시민 반환값") }
}

fun main() {
    //1번
    val varFunc = varFunction
    val dataFunc = dataFunction
    varFunc.invoke()
    dataFunc.forEach { it.invoke() }

    //2번
    argumentFunc(varFunction)

    //3번
    returnFunction().invoke()
}

 

반응형

댓글