728x90
반응형
✅ 위임 패턴(Delegation Pattern)이란?
- 위임 패턴(Delegation Pattern)은 객체 지향 기반의 디자인 패턴으로 상속처럼 코드의 재사용성을 향상 시켜주기 위한 패턴이다.
- 객체 A가 특정 기능을 직접 처리하지 않고 다른 객체 B에게 위임하여 처리하도록 하는 방식이다.
📌 위임을 사용해야 하는 이유
- 프로그래밍을 하다보면 하위 클래스에서 상위 클래스를 상속하여 프로퍼티를 오버라이드 하는 경우가 많다.
- 이러한 오버라이딩 상황에서 상위 클래스의 내용이 변경될 경우 상위 클래스를 참조하고 있는 하위 클래스의 내용들이 틀어지게 되며 이는 곧 에러를 발생시키게 된다.
- 그 뿐만 아니라 상속은 다음과 같은 문제가 있다.
- 상위 클래스에 대한 하위 클래스의 의존도가 높아져 상위 클래스의 내용 변경 시 하위 클래스에 영향이 가게 된다.
- 불필요한 상위 클래스의 메소드까지 구현해야 한다.
- 상속의 깊이가 깊어질수록 기능 파악이 어려워진다.
- 이러한 종속성, 의존성 문제를 해결하기 위해 위임 패턴(Delegation Pattern)이 상속의 대안으로 증명되어 왔다.
- 따라서 코틀린에서는 이러한 문제를 해결하고자 위임 패턴을 지원하게 되었고 위임 사용을 권장하고 있다.
✅ 상속(Inheritance)과 위임(Delegation)
- 상속과 위임 모두 객체지향 프로그래밍에서 사용되는 디자인 패턴으로, 두 방식 모두 클래스를 다른 클래스로부터 확장한다.
- 차이점은 상속은 해당 클래스에 귀속되는 것이기 때문에 클래스가 다른 클래스들 사이에서 선택할 권한을 주지 않는다.
- 하지만 위임의 경우 객체 자신이 처리해야 할 일을 다른 클래스 인스턴스에게 위임할 수 있는 것으로 상속보다 유연하게 활용할 수 있다.
- 따라서 여러 객체지향 모델에서 상속보다 위임을 사용할 것을 권장하고 있다.
- 상속과 위임 중 어떤 것을 선택해야 할 지 고민이 될 때는 아래 규칙을 생각해보면 된다.
- 클래스의 객체가 다른 클래스의 객체가 들어갈 자리에 쓰여야 한다. => 상속
- 클래스의 객체가 단순히 다른 클래스의 객체를 사용만 해야 한다 => 위임
📌 상속(Inheritance)
- 상속은 is a 관계를 가지며 부모 클래스의 속성들을 자식 클래스가 상속 받아서 사용할 수 있게 해준다.
- Dog is animal
- 부모 클래스에서 작성된 속성들을 물려 받기 때문에 자식 클래스에서는 해당 속성들을 정의하지 않고 사용 가능하다.
- 불필요한 코드 중복을 방지할 수 있다.
class Animal {
open fun do() {
// ...
}
}
class Dog : Animal {
// ...
}
val dog = Dog()
dog.do() // 부모 클래스에 정의된 do 함수 사용 가능
- 위 코드와 같이 중복 코드 작성 없이 부모 클래스의 속성을 사용할 수 있다.
- 하지만 Dog(Child) 클래스는 Animal(Parent) 클래스에 종속되기 때문에 Animal(Parent) 클래스에 변경이 있을 경우 Dog(Child) 클래스에도 그에 대한 대응을 해줘야 한다.
📌 위임(Delegation)
- 위임은 has a 관계를 가지며 A 클래스 내에 위임 관계를 가지고 있는 B 클래스의 인스턴스를 가지고 있는 구조이다.
- Manager has worker
- 다음 예제는 일을 할 작업자(Worker)와 그들을 관리하는 관리자(Manager)가 있다.
interface Worker {
fun work()
fun takeVacation()
}
class Worker_1 : Worker {
override fun work() = println("First Worker")
override fun takeVacation() = println("First Worker Take Vacation")
}
class Worker_2 : Worker {
override fun work() = println("Second Worker")
override fun takeVacation() = println("Second Worker Take Vacation")
}
class Manager
- Manager 클래스는 아직 아무런 정의가 되어 있지 않다.
- 이 Manager 클래스에 로직을 넣기 위한 디자인을 해야 하는데 먼저 상속을 사용하여 디자인을 하면 다음과 같다.
open class Worker_1 : Worker {
override fun work() = println("First Worker")
override fun takeVacation() = println("First Worker Take Vacation")
}
class Manager : Worker_1()
fun main() {
val manager = Manager()
manager.work() // First Worker
}
- Manager 클래스가 Worker_1을 상속하기 때문에 work()를 사용할 수 있다.
- 따라서 위 코드의 main 함수를 실행할 경우 "First Worker" 라는 문자열을 출력하게 된다.
- 마치 Manager를 통해 Worker_1에게 일을 시킨 것 처과 같이 작동한다.
- 하지만 이는 Manager 클래스가 Worekr_1 클래스에 종속되어 버리고 오로지 Worker_1 만을 위한 Manager가 되어버린다.
- 이것 뿐만 아니라 Manager가 특정 Worker에 대해서 설정한 적이 없지만 상속이 그렇게 만들어버리게 된다.
- 원래 우리가 의도했던 바믄 Manager의 인스턴스가 모든 Worker 인스턴스에게 일을 위임하게 만드는 것이다.
- 그렇다면 Manager 인스턴스가 Worker 인스턴스에게 일을 위임하는 방법은 무엇일까?
- 다음 예제는 Java에서 Manager가 Worker에게 위임을 사용하는 방식을 코틀린으로 나타낸 것이다.
class Manager(val worker: Worker) {
fun work() = worker.work()
fun takeVacation = worker.work() // Manager가 쉬어도 Worker는 일해야한다
}
fun main() {
val manager = Manager(Worker_1())
manager.work() // First Worker
- 위 예제를 보면 Manager 인스턴스를 만든 후 Worker_1 인스턴스를 생성자로 전달했다.
- 이런 디자인이 상속을 사용하는 것보다 좋은 점은 Manager가 Worker_1 클래스에 강하게 묶이지 않아 언제든지 새로운 Worker에 대한 인터페이스를 구현이 가능하다는 점이다.
- 이를 다르게 말해 Manager의 인스턴스는 Worker 인터페이스를 구현하는 클래스의 인스턴스에게 위임할 수 있다는 뜻이다.
- 이러한 장점 덕분에 상속보다 위임이 더 적합한 경우가 많이 있다.
- 하지만 단점으로는 많은 언어에서 위임(Delegation)을 언어 차원에서 지원하지는 않는다.
- 그렇기 때문에 메소드가 추가되는 등의 변경 사항이 발생할 경우 A와 B 클래스 모두 수정되어야 하기 때문에 객체 지향 설계 SOLID 원칙 중 OCP(개방 - 폐쇄 원칙)을 위반하게 된다.
- 하지만 이러한 문제를 해결하기 위해 코틀린에서는 언어적 차원에서 위임(Delegation)을 지원한다.
- 코틀린에서는 by 키워드를 통해 위임을 구현할 수 있다.
📌 by 키워드를 사용한 위임(Delegation)
- 바로 전 예제에서 Manager 클래스에 Worker 인터페이스로 요청을 위임하는 Delegation을 구현했다.
- Java에서는 저런 코딩만이 유일한 방법이지만 코틀린에서는 개발자가 직접 손대지 않고도 컴파일러에게 코드를 요청할 수 있다.
- 다음 예제를 통해 코틀린의 가장 간단한 위임을 살펴보자.
class Manager() : Worker by Worker_1
- 위 코드의 Manager는 어떤 메소드도 따로 작성해주지 않았다.
- Manager는 Worker_1을 이용해 Worker 인터페이스를 구현하고 있다.
- 코틀린 컴퓨ㅏ일러는 Worker에 속하는 Manager 클래스의 메소드를 바이트 코드 수준에서 구현하고, by 키워드 뒤에 나오는 Worker_1 클래스의 인스턴스로 호출을 요청한다.
- by 키워드가 컴파일 시간에 이전 예제에서 구현했단 위임을 대신 해준다.
- 클래스 정의 수준에서 사용되는 코틀린의 by 키워드의 왼쪽에는 인터페이스가, 오른쪽엔 해당 인터페이스를 구현한 클래스가 필요하다.
val manager = Manager()
manager.work() // First Worker
- 위의 코드를 보면 언뜻 보기에 상속과 비슷하게 보인다.
- 하지만 여기엔 주요 차이점이 몇 가지 있다.
- 첫 번째로, Manager 클래스는 Worker 인터페이스를 구현하긴 하지만 Worker_1 클래스를 상속받지 않는다.
- 상속을 이용한 구현에서 우리는 Manager의 인스턴스를 Worker_1 타입의 변수에 저장할 수 있었다.
- 하지만 이제 그런 상황은 더 이상 이루어지지 않으며, 아래와 같은 상황에서 오류가 발생한다.
val worker1: Worker_1 = manager // Error : type mismatch
- 두 번째로, 상속을 사용한 솔루션에서 work() 같은 메소드를 위임하기 위해 그대로 호출하는 보일러 플레이트 코드가 Manager 클래스에서는 작성되지 않았다.
- 대신 by 뒤에 오는 베이스 클래스로 요청을 넘겼을 뿐이다.
- 사실상 우리가 manager.work() 를 호출할 때, 우리는 Manager 클래스의 보이지 않는 메소드인 work()를 호출하는 겨이다.
- 이 함수는 코틀린 컴파일러에 의해 합성되었고 델리게이션에게 호출을 요청한다.
- 그래서 위 케이스에서는 클래스 선언 시 주어진 Worker_1의 인스턴스에게 요청하게 된다.
참고 : https://velog.io/@dudgns0507/Kotlin-Inheritance-vs-Delagation-feat.-Kotlin-Delegation-Delagation-Pattern / https://kotlinlang.org/docs/delegation.html / https://kotlinlang.org/docs/inheritance.html / https://itandhumanities.tistory.com/86 / https://0391kjy.tistory.com/61 / https://m1nzi.tistory.com/10 / https://typealias.com/start/kotlin-delegation/ / https://readystory.tistory.com/202
728x90
반응형