정의
Delegate
란 단어를 직역하면 위임이라는 뜻을 가지고 있다.
현실에서 내가 대리인에게 결정 권한을 위임한다고 하면, 대리인은 우리 결정 권한을 가지게 된다. 이처럼 대리인은 내가 아님에도 불구하고, 결정 권한이라는 행동에 대해 나와 똑같이 행동할 수 있게 되는 것이다.
Kotlin에서 의미하는 Delegate Pattern 또한 같은 의미이다. Kotlin의 Delegate Pattern은 한 객체에의 함수(행동) 혹은 Property(속성)의 구현을 다른 객체에 위임한다.
예를 들어 객체 1
과 객체 2
가 있다고 할 때, 객체 1
의 행동을 객체 2
에 위임한다고 하면, 객체 2
는 객체 1
의 함수와 Property를 위임받는다.
예시
부동산 게약을 위한 인터페이스가 있다고 가정했을 때
interface 부동산계약 {
fun 계약하기()
}
부동산 계약을 진행하는 본인은, 부동산계약 인터페이스를 구현해 계약이 가능해진다.
class 본인(): 부동산계약 {
override fun 계약하기() { println("계약되었습니다") }
}
하지만, 중개인은 본인에게 위임받지 않는 이상 부동산 계약이 불가능하다. 이러한 상황에서 위임을 사용하는 것이다.
Kotlin에서는 위임을 위한 키워드를 by
를 사용한다
val me = 본인()
class 부동산중개인(): 부동산계약 by me // 나의 계약하기 fun이 부동산 중개인에게 위임됨
fun main() {
부동산중개인().계약하기() // 계약되었습니다.
}
이를 통해, 부동산 중개인은 계약하기 수행이 가능해진다.
한 객체에서 구현된 메소드가 다른 메소드에 구현된 메소드와 같다면, 여러 번 구현하는 것보다, 1번 구현한 후에 재사용하는 것이 보일러플레이트 코드를 줄여줄 것이다.
val me = 본인()
class 부동산중개인() : 부동산계약 by me
class 배우자() : 부동산계약 by me
fun main() {
부동산중개인.계약하기() // 계약되었습니다.
배우자().계약하기()
}
필요성
상속과 구성 (Inheritance와 Composition)
Delegate Pattern의 필요성을 알아보기 전에, 상속과 구성에 대한 개념부터 알아보고자 한다.
상속은 is - a
관계라고 한다. 예를 들어, 동물이라는 클래스가 강아지라는 클래스의 부모 클래스라면, 강아지 is 동물
관계가 성립하는 것이다. 이런 경우엔, 클래스의 변수와 메소드를 상속받아 새로 구현해줄 필요가 없다.
이러한 상속관계는 편리하지만, 의존성이 생겨버리기 때문에 잘 생각하고 사용해야 한다. 상속을 하게 된다면, 부모 클래스의 구현이 자식 클래스에 드러나게 된다. 특정 기능을 다른 객체에 맡기기 위해 부모 클래스에서는 해당 메소드를 private으로 설정할 수 없기 때문이며, 이는 곧 캡슐화를 파괴한다고 볼 수 있을 것이다.
(이처럼 서브 클래싱
(다른 부모 클래스에서 상속받아 한 클래스의 구현을 정의하는것) 에 의한 재사용을 화이트박스 재사용
이라고 한다)
또한, 부모 클래스 구현에 있어 어떠한 변화가 일어나면, 자식 클래스에도 영향을 미치게 된다. 결국, 잘못된 상속은 객체의 유연성을 떨어트리는 결과를 불러오고, 유지보수나 추가적인 개발에 있어 다양한 문제가 발생하며, 코드의 유연성이 떨어질 수 있다.
따라서, 이러한 해결법으로 Composition
(또는 Aggregation
) 관계로 구현하는 것을 권장하고 있다.
Composition
(또는 Aggregation
)은 상속이 아닌, 클래스 안에 객체를 소유하고 있는 관계를 뜻하며 흔히 has - a
관계라고도 한다. 클래스 상속에 대한 대안으로, 다른 객체를 여러개 붙여 새로운 기능 혹은 객체를 구성하는 것이다.
객체를 합성하기 위해서는 합성에 들어가는 객체들의 인터페이스를 명확하게 정의해두어야 한다.
이런 스타일의 재사용을 블랙박스 재사용
이라고 한다. (객체의 내부는 공개되지 않고, 인터페이스를 통해서만 재사용되기 때문)
Delegate Pattern
은 Composition
을 이용하는 일반적인 패턴이다.
Composition
객체의 함수가 많아지면, 보일러플레이트 코드를 많이 작성해야 할 수 있는데 Kotlin은 by
키워드로 적은 코드로 적용할 수 있도록 지원하고 있다.
효과적인 Delegate Pattern 사용의 예시
interface Human {
fun name(): String
fun countury(): String
}
class Student : Human {
override fun name() = "홍길동"
override fun countury() = "한국"
}
class Information(student: Human) : Human {
private val human: Human = student
override fun name(): String = human.name()
override fun countury(): String = human.countury()
}
fun main() {
val human: Human = Student()
val information = Information(human)
println("이름::" + information.name())
println("나라::" + information.countury())
}
위의 경우, Human
이라는 Interface
롸, Student
, Information
이라는 Class
가 있다. Student
, Information
모두, Human
이라는 Interface
를 상속받고 있다.
여기서 Information
은 Student
Class를 상속받지 않고, human
을 가지고 있다. 그리고, human.name()
과 같이 human
을 호출하고 있다.
Information
Class는 Student
의 기능을 내부 변수 human
에 위임하였다.
하지만 이 구조에서는 보일러 플레이트 코드가 존재하는데, 바로 information.name()
, information.countury()
이다. 만약 Human
인터페이스의 메서드가 100개라면 100개에 대한 메서드를 모두 작성해야한다. 여기서 by
를 사용한다면 이러한 보일러플레이트 코드를 줄일 수 있다.
interface Human {
fun name(): String
fun countury(): String
}
class Student : Human {
override fun name() = "홍길동"
override fun countury() = "한국"
}
class Information(student: Human) : Human by student
fun main() {
val human: Human = Student()
val information = Information(human)
println("이름::" + information.name())
println("나라::" + information.countury())
}
여기서 보면, Student
는 상속받은 Interface
를 구현하였는데, Information
는 상속받은 Interface
를 구현은 했지만 스스로 한게 하나도 없다. 쉽게 말해서, “객체를 생성하는 너가 다해라, 니거 가져다가 쓰겠다”는 것이다. 그렇기 때문에 delegate
라고 부르는 것이다.
한마디로, by
키워드는, “다 위임합니다. 알아서 하세요”라는 의미인 것이다. 따라서 위에서 3줄이었던 Information
코드가 1줄로 표현된 것이다.
Delegate Pattern의 의의
Delegate Pattern
은 보일러플레이트 코드를 줄여준다.- 관리 포인트를 일원화시켜, 코드 유지보수를 용이하게 해준다. 한곳에서 구현하면, 다른 곳에서 위임받아 사용할 수 있기 때문이다.
클래스 선언부의 by 키워드
클래스 선언부에는, interface
가 implement되거나, 다른 클래스가 상속될 수 있다.
Delegate
패턴은 인터페이스 구현부를 다른 클래스에 위임하는 것이기 때문에, 인터페이스 구현에만 사용될 수 있다.
따라서, Kotlin의 Delegate 패턴을 도와주는 by
키워드는 다음의 제한을 가진다.
by
키워드의 제한
by
키워드는interface
가 implement되는 경우에만 사용될 수 있다.- 클래스 상속에는
by
키워드 사용이 불가능하다 - 변수(
val
,var
)와 메소드(fun
)에 대한 위임만이 가능하다.
interface가 implement 되는 과정
interface 부동산계약 {
fun 계약하기()
}
class 본인(): 부동산계약 {
override fun 계약하기() {
println("집이 계약되었습니다")
}
}
메소드 위임
본인
의 부동산 계약
내부의 계약하기
행위를 부동산 중개인
클래스에 위임하고 싶은 경우
interface 부동산계약 {
fun 계약하기()
}
class 본인(): 부동산계약 {
override fun 계약하기() {
println("집이 계약되었습니다")
}
}
val me = 본인()
class 부동산중개인() : 부동산 계약 by me
부동산 중개인
은 본인
의 계약하기
동작(fun
)을 위임받는다
Property 위임
val/var <property name>: <Type> by <expression>
Property도 위임가능한데, by
키워드를 사용하면, Property가 위임된 delegate
에 의해서 컨트롤된다는 의미로 해석할 수 있다. delegated property는 위와 같은 문법으로 사용되는데 by
키워드가 핵심이다.
interface 부동산계약 {
val 계약자이름: String
fun 계약하기()
}
class 본인(val 이름: String): 부동산계약 {
override val 계약자이름 = 이름
override fun 계약하기() {
println("${계약자이름} 집이 계약되었습니다")
}
}
val me = 본인("dev_cho")
class 부동산중개인() : 부동산 계약 by me
fun main() {
부동산중개인().계약하기() // dev_cho 집이 계약되었습니다.
}
입력받은 변수로부터 위임 받기
위의 예시처럼 외부의 me
변수를 쓰지않고, 다음과 같이 입력받은 변수로부터 위임을 받을수도 있다.
class 부동산중개인(위임인 : 부동산계약) : 부동산 by 위임인
입력받은 변수로부터 위임 받는 방식으로 코드를 짜게되면, 코드 유연성이 올라가게 된다. 따라서, 이미 선언된 변수로부터 위임받는 방식 보다는 입력받은 변수로부터 위임받는 방식으로 위임을 사용하는 것이 좋다.
변수에서의 by 키워드
인스턴스가 변수에 위임
interface 부동산계약 {
val 계약자이름: String
fun 계약하기()
}
class 본인(val 이름: String): 부동산계약 {
override val 계약자이름 = 이름
override fun 계약하기() {
println("${계약자이름} 집이 계약되었습니다")
}
override fun getValue(nothing: Nothing?, property: KProperty<*>): 부동산계약 {
return 본인(이름)
}
}
val 부동산중개인: 부동산계약 by 본인("dev_cho")
변수 인터페이스
를 구현하는 변수
가 객체로부터 생성된 인스턴스로부터 위임을 받을 때는 operator fun
getValue
가 호출된다. 따라서, operation fun
getValue
를 구현해야만, 객체의 인스턴스가 인터페이스를 구현하는 변수에 위임을 할 수 있게 된다.
by
키워드를 사용함으로서, 컴파일할 때 이 property에 해당하는 get()
이나 set()
함수는 by
에 의해서 위임된, expression의 getValue()
와 setValue()
함수로 delegate되기 때문이다.
클래스에서의 by 와 변수에서의 by 예시
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씨에게 권한을 위임받아서 도장찍겠습니다 ㅎㅎ
}
+ 코드 작성의 편리함
by
키워드를 사용함으로서, 코드 작성을 편하게 해주는 면도 있다.
예를 들어, type
이 MutableState
타입이어야하는데, 현재 Receipt
타입인경우 MutableState
타입으로 Wrapping시켜주어야 했다.
그러나 이때 by
키워드를 사용한다면, getter
와 setter
을 mutableStateOf()
에 위임하므로, 타입 문제도 같이 해결되게 된다.
receipt
에 Int
타입인 값을 "="으로 대입해준 순간 mutableStateOf<Receipt?>
의 setter
가 동작하게 된 것이다.
Android 리팩토링 적용: BaseActivity를 지양하기 위한Delegate를 사용
Delegate Pattern의 필요성
사실 BaseActivity
는 안드로이드 프로젝트를 진행했었다면, 1번 쯤은 상속받아서 사용해봤을 것이라고 생각할 것이다. 그 이유는, 이러한 상속을 통해, 보일러플레이트 코드를 줄이고, 자식 activity 들과 Logic을 공유할 수 있는 대표적인 방법이기 때문이다. 그러나, 이러한 방법에는 다음과 같은 한계점이 존재한다
BaseActivity의 한계점
- 상속은
Java/Kotlin
에서 여러개의 클래스가 가능하도록 지원되지 않는다. - 여러개의
BaseActivity
를extend
할 수 없기 때문에, 모든 Logic은 하나의 Class에서 구현되어야 한다. 이는단일책임원칙
에 위배되는 행위이다. - 관련이 없는
Activity
들에 까지 Logic이 작동되도록할 수 있고, 유지보수가 점차 어려워진다.
BaseActivity의 한계를 보여주는 예시
모든 Activity
들에서 네트워크와 배터리 상태를 모니터하는 앱이 있다라고 가정한다.
모든 곳에 같은 Logic을 구현하는 대신, BaseActivity
를 구현해서 이를 모든 Activity
에 적용시켜보고자 한다
BaseActivity
open class BaseActivity : AppCompatActivity() {
fun observeBatteryChanges() {
TODO("Not yet implemented")
}
fun observeNetworkChanges() {
TODO("Not yet implemented")
}
}
LogicActivity
class LoginActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
observeBatteryChanges()
observeNetworkChanges()
}
}
MainActivity
class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
observeBatteryChanges()
observeNetworkChanges()
}
}
만약 여기서, 로그인 이후에 모든 사용자의 행동을 tracking하고자 하는 경우, 우리는 새로운 메소드를 BaseActivity
에 추가할것이다.
open class BaseActivity : AppCompatActivity() {
fun observeBatteryChanges() {
TODO("Not yet implemented")
}
fun observeNetworkChanges() {
TODO("Not yet implemented")
}
fun trackUserAction(action: String) {
TODO("Not yet implemented")
}
}
이건 간단한 추가 같아보이지만 몇가지 문제점이 있다.
- 우리는, 로그인 이후의 사용자 행동에 대한 Tracking에 대해서만 관심이 있다. 그러나, 위처럼
BaseActivity
에 집어넣는 순간,BaseActivity
를 상속받는 모든Activity
들이 해당 메소드에 access할 수 있게 된다. BaseActivity
내부 코드베이스는 더이상 단일 책임을 가지지않게 되며, 번거로워진다.- 코드를 처음 접하는 사람은
BaseActivity
의 기능을 한번에 알기 어렵다.
따라서 앞서 말한것 처럼, Delegation Pattern
을 적용하면 이를 어느정도 보완할 수 있다. 위의 코드를 delgate pattern
을 사용해 리팩토링해보면 아래와 같다
For Hardware monior:
interface IHardwareMonitor {
fun observeBatteryChanges()
fun observeNetworkChanges()
}
class HardwareMonitorImpl : IHardwareMonitor {
override fun observeBatteryChanges() {
TODO("Not yet implemented")
}
override fun observeNetworkChanges() {
TODO("Not yet implemented")
}
}
For User Action Tracking:
interface ITrackingService {
fun trackUserAction(action: String)
}
class UserTrackingImpl : ITrackingService {
override fun trackUserAction(action: String) {
TODO("Not yet implemented")
}
}
이제 Activity
가 HardwareMonitor
와 Tracking Service
를 모두 사용하는 경우 IHardwareMonitor
및 ITrackingService
구현할 수 있다.
class MainActivity : ComponentActivity(),
IHardwareMonitor by HardwareMonitorImpl(), // delegation
ITrackingService by UserTrackingImpl() { // delegation
override fun onCreate(savedInstanceState: Bundle?) {
observeBatteryChanges()
observeNetworkChanges()
}
override fun onPause() {
super.onPause()
trackUserAction("Activity paused")
}
}
만약 Activity
가 IHardwareMonitor
class만 사용하는 경우 다음과 같이 plug 할 수 있다.
class LoginActivity : ComponentActivity(),
IHardwareMonitor by HardwareMonitorImpl() { // delegation
override fun onCreate(savedInstanceState: Bundle?) {
observeBatteryChanges()
observeNetworkChanges()
}
}
이러한 리팩토링 과정에서 delegate pattern의 장점이 극명하게 드러나게 되었다.
바로, Activity에 Plug and Play처럼 feature를 무제한으로 추가할 수 있다는 것이다. 이것은, Activity를 Clean하게 만들어 줄 뿐만 아니라, long-term scalability를 향상시켜준다는데에 의의 가 있다.
따라서, Delegation이 상속에 대한 대안이 될 수 있다. 상속
은 부모와 자식 object간에 밀접한 관계가 있는 경우 좋은 전략이지만, object을 매우 밀접하게 연결한다. 때때로, delegation
이 클래스간에 관계를 express하는데 좀 더 유연한 전략이 될 수 있다.
해당 예시로 알아본 사용 상황
- 클래스 간에 메소드의 연결을 줄이기 위해 (To reduce the coupling of methods to their class)
- 동일하게 작동하지만, 미래에는 상황이 바뀔수도 있는 요소에 대해 (Components that behabe identically but realize that this situation can change in the future)
해당 예시로 알아본 장점
- 다른 기능 집합간의 분리 (Separates the different sets of functionality)
- 런타임 modification의 유연성 (The Flexivility of Run-time Modification)
해당 예시로 알아본 단점(Drawback)
- 상속 구현만큼 간단하지 않다 (Not as straightforward as implementing an inheritance)
참고
[Delegate Pattern] 1. Delegate Pattern이란?
Delegate Pattern - kotlin (by를 사용해서 쉽게)
[kotlin]Delegate Pattern을 알아보자
Kotlin By 키워드에 대한 이해 # Property Delegate Pattern
[디자인 패턴] 위임 패턴(Delegate Pattern)
Kotlin - by로 Delegate Pattern 쉽게 구현하기
Delegation Pattern: An effective way of replacing Android's BaseActivity with native Kotlin support
'IT > Kotlin' 카테고리의 다른 글
[Kotlin/Coroutine] 4. Coroutine의 구성요소: Coroutine의 생성과 활용 (0) | 2023.04.22 |
---|---|
[Kotlin/Coroutine] 3. launch와 async의 Job과 Deferred (0) | 2023.04.07 |
[Kotlin/Coroutine] 2. Kotlin에서의 Coroutine: launch와 async (0) | 2023.03.24 |
[Kotlin/Coroutine] 1. Coroutine이란 무엇인가 (0) | 2023.03.16 |
[Kotlin] Companion Object (+ Object, Object Declaration) (0) | 2023.01.24 |