[Kotlin] Delegate 패턴에 대해 알아보자

MingMaeng·2022년 4월 13일
5

코틀린

목록 보기
1/1
post-thumbnail

자신의 와이드 오픈 찬스도 친히 위임(Deleagate)하는 모습 또한 릅신의 위엄이겠죠

해당 포스팅은 제 티스토리 블로그에 포스팅한 내용과 동일합니다.
더 많은 포스팅을 보고 싶으시다면 아래 링크로 들어가시면 됩니다 :)
밍맹의 생각날 때 적는 블로그

서론

먼 옛날부터 사람들은 코드를 좀 더 아름답게 짜기 위해서 여러 가지 시도를 했습니다.
객체지향 원칙에 맞게끔 항상 고민하고 끊임없는 설계를 구상해왔죠.

그러면서 좀 더 유연하고 유지보수가 쉬운 설계 방식을 연구했고,
기존 개발에 있어서 발생하는 일반적인 문제들(반복 코드 제거, 수정 교체 용이 등등..)에 대해 솔루션을 구상하기 시작했습니다.

그렇게 해서 나온 게 바로 개발 설계를 좀 더 빠르고 잘할 수 있도록 도와주는 솔루션의 집약체 디자인 패턴입니다.

Delegate Pattern이란?

디자인 패턴 중 하나인 Delegate Pattern은 어떠한 객체가 기능을 수행할 때 해당 기능을 다른 객체에게 위임하는 패턴입니다.

동일한 기능에 대해 여러번 구현하는 것은 귀찮고 피곤한 일입니다.
그렇다면 한번만 구현을 해놓고 이를 다른 곳에 위임시키면 좀 더 쉽고 빠르게 기능을 구현할 수 있겠죠?

상속과의 비교

Delegate Pattern은 상속과 많이 연관 지어서 이야기를 하는 것 같습니다.
'그냥 클래스 상속받아서 필요할 때 사용하는게 더 낫지 않냐?'라는 목소리도 많이 나오죠.

상속을 하게 된다면 부모 클래스의 구현이 자식 클래스에게 드러나게 됩니다. 특정 기능을 다른 객체에게 맡기기 위해서 부모 클래스에서는 해당 메서드를 private로 설정할 수 없기 때문입니다. 그렇다면 캡슐화를 파괴한다고 볼 수 있을 것입니다.

또한 부모 클래스 구현에 있어서 어떠한 변화가 일어난다면 크든 작든 자식 클래스에도 영향을 끼치게 됩니다. 결국 잘못된 상속은 객체의 유연성이 떨어뜨리는 결과를 불러옵니다.

이러한 문제는 추상클래스를 상속받으면 자식 클래스에서 해당 메서드를 정의하여 사용하기 때문에 유연성과 재사용성을 해결할 수 있습니다.

그렇다면 왜 Delgate Pattern을 사용하려고 할까요?

Delgate Pattern은 상속과 다르게 객체 안에 새로운 기능 혹은 객체를 구성하는 composition을 기반으로 구성됩니다.

composition 객체는 각 클래스의 캡슐화를 유지할 수 있을 뿐더러 해당 각 클래스는 한 가지 일에 집중할 수 있기 때문입니다.

// 일에 관한 interface
interface Work {
    fun programming()
    fun meeting()
}
// 청소에 관한 interface
interface Cleaning {
    fun washing()
    fun shower()
}
// 상속을 이용한다면 클래스를 만들때마다 일일이 override 진행
class WorkerA(): Work, Cleaning {
    override fun programming() {
    	...
    }
    override fun meeting() {
    	...
    }
    override fun washing() {
    	...
    }
    override fun shower() {
    	...
    }
}

일반적인 Delegate 패턴

위에서 말했듯이 Delegate 패턴의 핵심은 특정 기능을 다른 객체에 위임하는 것입니다.
예시를 보여드리기 위해 블로그 포스팅에서 가장 보편적으로 다루는 집계약 예시로 보겠습니다.

개발자 K씨는 최근 이직을 하게 되었는데 회사가 집이랑 멀리 떨어져 있어서
어쩔 수 없이 회사 근처로 자취방을 구하게 됩니다.
근데 부동산 계약을 해야하는데 너무나도 고려할게 많고 복잡해서
이 부분을 부동산중개인에게 맡기기로 정했는데...

먼저 부동산 중개인을 통해 부동산 계약을 진행하려는 Customer 클래스와 부동산 중개인 Broker 클래스가 존재한다고 했을 때

Customer 클래스는 부동산과 관련된 여러 권한들이 있을 것입니다. 가장 대표적인 예로 본인의 인증이 들어간 도장이나 서명이라고 하겠습니다.

부동산 중개인은 개발자 K의 권한을 위임받아서 여러 부동산을 알아보고 계약을 진행할 것입니다. 이때 개발자 K의 대리인 역할을 하기 위해 도장이나 서명은 개발자 K 씨의 것을 사용하도록 만들어보겠습니다.

// 부동산과 관련된 권한들
interface Authority {
    fun stampSeal() // 도장 찍기
     ... // 기타 등등
}

// 부동산에 대해서 내가 행사할 수 있는 권리가 있어!
class Customer(private val name: String) : Authority {
    // Authority 인터페이스 구현
    override fun stampSeal() {
        println("${name} 도장 쾅")
    }
}

// 저는 중개인 입니다. 제가 대신 손님의 집을 알아봐드릴게요
class Broker(private val customer: Authority) : Authority {
    // 도장은 손님의 권한을 위임받아 사용하겠습니다 허허
    ovveride fun stampSeal() {
        customer.stampSeal()
    }
}

fun main() {
    //개발자K씨
    val developerK = Customer("개발자K씨")
    //부동산중개인G씨
    val brokerG = Broker(developerK)

    bokerG.stampSeal() // 개발자K씨에게 권한을 위임받아서 도장찍겠습니다 ㅎㅎ
}

손님 객체인 Customer와 중개인 객체인 Broker 클래스 모두 부동산과 관련된 권한 interface인 Authority를 상속받고 있습니다.

이때 Broker 클래스는 Authority의 함수를 Customer 클래스로 위임하는 모습을 볼 수 있습니다.

Broker 클래스의 오버라이드 함수를 보시면 customer에서 선언한 함수를 사용하는 것을 보실 수 있습니다.
Broker 클래스 또한 Authority를 상속 받고 있기 때문에 결국 오버라이드 메서드가 생길 수밖에 없고 interface 내 메서드가 많아지면 많아질수록 보일러 플레이트 코드 또한 늘어날 것입니다.

by 키워드를 이용한 Delegate 패턴

kotlin은 이러한 보일러플레이트 코드를 보완해주기 위해 by라는 키워드를 통해 깔끔하게 위임을 시켜줄 수 있습니다.

// 부동산과 관련된 권한들
interface Authority {
    fun stampSeal() // 도장 찍기
    ... // 기타 등등
}

// 부동산에 대해서 내가 행사할 수 있는 권리가 있어!
class Customer(private val name: String) : Authority {
    // Authority 인터페이스 구현
    override fun stampSeal() {
        println("${name} 도장 쾅")
    }
}

// 저는 중개인 입니다. 제가 대신 손님의 집을 알아봐드릴게요
class Broker(private val customer: Authority) : Authority by customer {
	// by 키워드를 통해 customer 파라미터에게 권한을 위임
}

fun main() {
    //개발자K씨
    val developerK = Customer("개발자K씨")
    //부동산중개인G씨
    val brokerG = Broker(developerK)

    bokerG.stampSeal() // 개발자K씨에게 권한을 위임받아서 도장찍겠습니다 ㅎㅎ
}

Broker 클래스 부분에 by 키워드를 이용하여 Authority에 대한 권한을 customer 파라미터에게 위임하는 코드입니다.

Broker 클래스 내부에서 일일히 오버라이드 하지 않고 바로 위임을 할 수 있다는 점에서 보일러 플레이트 코드를 크게 감소하는 효과를 볼 수 있습니다.

변수에서 바로 Delegate 패턴 사용

변수 선언 단계에서 바로 by 키워드를 이용해 클래스를 선언하기 위해서는 getValue 연산자 함수를 오버로딩하여 사용해주셔야 합니다.

interface Authority {
	fun stampSeal()
}

class Customer(private val name: String) : Authority {
	// Authority 인터페이스 구현
    override fun stampSeal() {
        println("${name} 도장 쾅")
    }
    
    // getValue() 연산자 오버로딩
    operator fun getValue(nothing: Nothing?, property: KProperty<*>): Broker {
        return Broker(this)
    }
}

class Broker(private val customer: Authority) : Authority by customer {}

fun main() {
    // 개발자K씨의 권한을 위임받은 중개인G씨
    val brokerG: Broker by Customer("K")

    bokerG.stampSeal() // 개발자K씨에게 권한을 위임받아서 도장찍겠습니다 ㅎㅎ
}

마치며

위임 패턴은 상속을 대체할 수 있는 강력한 패턴입니다.

클래스의 위임을 통해서 요구 사항 변경에 있어서 전면적인 코드 수정을 방지할 수 있다는 점에서
모듈을 보다 더 유연하게 구성할 수 있고 캡슐화와 다형성을 지킬 수 있습니다.

가장 중요한 것은 위임패턴을 잘 사용하기 위해선 인터페이스 정의 단계부터 신중하게 작성해야 추후에 고생하지 않는 것 같습니다 ㅎㅎ

여러 가지 디자인 패턴들을 공부하면서 개인적으로 하나로 귀결되는 결론이 있다면 그것은
어떤 디자인 패턴이던 설계던지 간에 결국은 프로그램 설계를 하는 데 있어서 가장 중요한 것은 내가 만들려는 프로그램에 적절한 방식을 찾는 것이라고 생각합니다.
(디자인 패턴은 그런 방식을 결정하기 위한 일종의 도움수단이라고 저는 생각합니다)

무작정 이 패턴이 좋아보여서, 남들이 사용해서 쓰는 것이 아닌 현재 내가 진행하는 프로젝트나 프로그램에 적절한 방식인지 따져보고 적용하는 것이 좋은 개발자로서의 능력 중 하나이지 않나 싶습니다.

참고 사이트

https://kotlinworld.com/85
https://codechacha.com/ko/kotlin-deligation-using-by/
https://june0122.github.io/2021/08/21/design-pattern-delegate/

profile
오늘보다 더 성장한 내일의 나를 만드려는 안드로이드 개발자입니다.

0개의 댓글