Kotlin에서의 Delegation Pattern(1)
Delegation Pattern
이름 그대로 해석해보자면, 위임 패턴으로 객체지향 프로그래밍 디자인 패턴중 하나로, 어떤 기능을 자신이 처리하지 않고 다른 객체에 위임을 시켜 그 객체가 일을 처리하도록 만들어주는 패턴입니다.
상속과 구성
Delegation pattern을 알기 위해선 먼저 상속(Inheritance)과 구성(Composition, 이하 컴포지션)에 대해서 알아두면 좋은데, 상속을 사용하는 이유는 1. 코드를 재사용 하기 위해서, 2. 변화에 대한 유연성 및 확장성이 증가함을 기대해서, 3. 개발 시간을 단축하기 위해서 입니다.
그런데 상속은 지나치게 상위 클래스에 대해 하위 클래스가 의존성을 띄고 있다는 점, 그래서 객체의 유연성이 매우 떨어진다는 점에서 컴포지션을 추천합니다.
컴포지션은 기존 클래스가 새로운 클래스의 구성요소로 쓰이는 방식으로, 새로운 클래스 안에 private 필드로 기존 클래스의 인스턴스를 참조하는 방식인데, 쉽게 말해 메소드를 호출하는 방식으로 동작하기 때문에 내부의 구조를 바꿀 필요가 없고, 기존 클래스에 영향을 주지 않으며, 내부의 구조를 변경시에 기존 클래스만 바꾸면 기존 클래스를 호출하는 함수의 동작까지 같이 바뀌게 됩니다.
그럼 컴포지션만 쓰면 되나?
하지만 이러한 상속과 컴포지션을 한쪽만 사용해야 한다! 이러한 뜻은 아닙니다. 상황에 맞게 필요한 요소를 채택하여 사용해야한다는 뜻입니다.
설명하기에 아주 유명한 is-a 관계와 has-a 관계를 통해 사용을 구분할 수 있습니다.
is-a 관계에서는 클래스 및 객체는 상속관계에서 밀접한 관계로 부모 또는 부모 클래스가 수정시 코드가 손상될 위험이 있고, 반대로 말하면 이는 계층 구조에서 안정적인 기반을 뜻합니다.
is-a의 가장 대표적인 예시로는 사람은 인간이다, 고양이는 동물이다 등이 있습니다. 상속의 경우 ~이다, 즉 is 관계가 확실하다면 사용합니다.
has-a 관계에서는 느슨한 결합이 장점으로 작용되고, 명세에 따른 변경이 있다면 구성 요소를 쉽게 변경해줄 수 있습니다. 그러나 유연함을 장점으로 가지고 있다면, 낮은 결합도를 단점으로 가지고 있다는 것을 명심하며 코딩해나가야 합니다.
has-a의 가장 대표적인 예시로는 자동차는 배터리를 가지고 있다, 사람은 심장을 가지고있다. 등이 있습니다. 하나의 객체가 다른 객체를 (부분적으로) 가지고 있을 때 has-a를 사용합니다.
Delegation Pattern은 Composition
서론이 길었습니다만, Delegation Pattern은 결국 Composition을 이용해 만듭니다.
이용 방법은 위에서 설명했 듯 하위 함수에서 상위 함수의 객체를 가지고 있고, 그 객체를 통해 메소드를 호출하는 방식입니다. Java에서 이용하는 방식을 통한 예시로 확인해보겠습니다!
class Rectangle(val width: Int, val height: Int) {
fun area() = width * height
}
class Window(val bounds: Rectangle) {
// Delegation
fun area() = bounds.area()
}
이 코드에서는 bounds라는 객체를 통해 area의 생성을 위임합니다.
코틀린에서의 Delegation Pattern
코틀린에서는 by 키워드를 단 하나를 통해서 만들 수 있으며, 이를 통해 Boilerplate code를 줄일 수 있게됩니다.
위의 예제를 코틀린 방식으로 만들 경우, 더 간편하게 만들 수 있습니다.
interface ClosedShape {
fun area(): Int
}
class Rectangle(val width: Int, val height: Int) : ClosedShape {
override fun area() = width * height
}
class Window(private val bounds: ClosedShape) : ClosedShape by bounds
이렇게 만들어주면, Delegation Pattern을 이용 가능합니다. 실제로 위의 코드를 디컴파일 해보면
public final class Window implements ClosedShape {
private final ClosedShape bounds;
public Window (@NotNull ClosedShape bounds) {
Intrinsics.checkNotNullParameter(bounds, "bounds");
super();
this.bounds = bounds;
}
public int area() {
return this.bounds.area();
}
}
Window 클래스가 위와 같이 나오게됩니다. ClosedShape 형으로 private 객체가 선언되고, area() 메소드는 객체의 메소드를 호출하는 식으로 생성되게 됩니다.
예시
위의 코드에서 인터페이스의 리턴 값이 Double형으로 바뀌게될 경우 인터페이스를 상속하는 모든 클래스들의 area()를 Double로 수정해주어야 합니다.
하지만 by 키워드를 통해 위임을 받은 Window 클래스는 위임을 받아 this.bounds.area()로 메소드를 호출하기 때문에 이러한 수정에도 자유로울 수 있습니다.
이 다음에는 Delegated Properties에 대해서 알아보겠습니다.
출처 및 참고
https://en.wikipedia.org/wiki/Delegation_pattern
https://kotlinlang.org/docs/delegation.html
https://codechacha.com/ko/kotlin-deligation-using-by/
https://woowacourse.github.io/tecoble/post/2020-05-18-inheritance-vs-composition/
https://minusi.tistory.com/entry/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5%EC%A0%81-%EA%B4%80%EC%A0%90%EC%97%90%EC%84%9C%EC%9D%98-has-a%EC%99%80-is-a-%EC%B0%A8%EC%9D%B4%EC%A0%90