디자인 패턴 중 Delegate Pattern 이라는 것이 있다. 코틀린의 by
키워드는 해당 디자인 패턴 구현을 쉽게 할 수 있도록 도와주는 키워드이다. 그럼, Delegate 패턴에 대해 간략히 살펴보자. Delegate 의 사전적 정의는 다음과 같다.
동사 뜻을 보면 '위임하다' 라고 나와있다. 어떤 일을 다른 이에게 떠넘기는 것이다. Delegate Pattern 은 어떤 기능을 자신이 수행하지 않고 다른 객체가 수행하도록 하는 패턴이다. 즉, 기능을 위임시키는 것이다.
이 Delegate Pattern 을 설명할 땐 상속과 구성 (Inheritance & Composition) 의 차이에 대한 이야기가 필연적이다. 상위 클래스의 요소들을 사용할 때 상속 혹은 구성을 사용할 수 있다. 상속은 모든 요소를 물려받기 때문에 변수나 메소드 등을 다시 구현할 필요가 없어 편리하지만, 객체의 유연성이 떨어진다는 치명적인 단점이 있다. 만약 상위 클래스가 변경된다면, 하위 클래스 역시 반드시 고쳐져야 한다.
따라서 유연성을 높이기 위해 , 구성 (Composition) 관계를 통해 상위 클래스를 이용하는 것을 권장한다. 상속이 아닌 객체 소유로써 상위 클래스의 요소를 활용하는 것이다.
💡 상속과 구성 관계 차이
상속 :
is-a
구성 :has-a
Delegate Pattern 은 구성 (Composition) 을 활용한 패턴이다. 구성을 통해 특정 객체를 소유하고, 모든 동작들을 소유하고 있는 객체에게 모두 위임하는 형식이다.
⛔️ 예제 코드는 아래 블로그 글에서 따왔음을 밝힙니다
간략히 Delegate Pattern 사용 예제를 살펴보자. 먼저, IWindow
라는 인터페이스가 있다고 가정해보자.
interface IWindow {
fun getWidth() : Int
fun getHeight() : Int
}
그리고 위 인터페이스를 구현한 TransparentWindow
라는 클래스가 있다.
open class TransparentWindow : IWindow {
override fun getWidth(): Int {
return 100
}
override fun getHeight() : Int{
return 150
}
}
마지막으로 UI
라는 클래스가 있는데, 이는 TransparentWindow
를 상속받진 않으나 IWindow
를 구현했고, Composition 관계로 TransparentWindow
를 받기 위해 mWindow
객체를 갖게 된다. 그리고 Delegate Pattern 을 구현하기 위해 모든 메소드에 대해서 mWindow
이 갖고있는 메소드를 호출함으로써 기능을 위임시키게 된다.
class UI(window: IWindow) : IWindow {
val mWindow: IWindow = window
override fun getWidth(): Int {
return mWindow.getWidth()
}
override fun getHeight(): Int {
return mWindow.getHeight()
}
}
이들을 활용하여 다음과 같이 활용해볼 수 있을 것이다.
fun main() {
val window: IWindow = TransparentWindow()
val ui = UI(window)
System.out.println("Width : ${ui.getWidth()}, height: ${ui.getHeight()}")
}
// Width : 100, height: 150
Delegate Pattern 을 활용하면 상속을 사용하지 않아 유연성을 갖춘 채로 상위 클래스의 요소들을 사용할 수 있으며, 새로운 기능을 자유자재로 덧붙여 사용할 수 있다는 장점이 있다.
그러나 가만 살펴보면, Delegate Pattern 을 사용할 때 모든 메소드에 대해 일일히 Wrapper 메소드를 작성해줘야 함을 알 수 있다. 지금 예제에서는 메소드가 2개이기에 가뿐해보이지만, 만약
IWindow
의 메소드가 계속 늘어난다면 그만큼 보일러 플레이트 코드가 증가할 것이다.
by
키워드의 역할따라서, 코틀린에서는 Delegate Pattern 구현 시 이러한 보일러 플레이트 코드들을 줄이기 위해서 by
키워드를 제공하게 된다. 아래처럼 by
키워드를 사용하게 되면, 컴파일러가 자동으로 Delegate Pattern 코드를 작성해주게 된다. 따라서 일일히 IWindow
의 메소드들을 구현해줄 필요가 없다.
class UI(window: IWindow) : IWindow by window { }
위와 같이 UI
클래스를 수정해도, 동작상 변화가 없는 것을 확인할 수 있다.
그럼, by
키워드가 어떻게 자동으로 Delegate Pattern 을 구현해주는 지 살펴보기 위해 자바로 변환된 코드를 살펴보도록 하자.
public interface IWindow {
int getWidth();
int getHeight();
}
public class TransparentWindow implements IWindow {
public int getWidth() {
return 100;
}
public int getHeight() {
return 150;
}
}
public final class UI implements IWindow {
private final IWindow $$delegate_0;
public UI(@NotNull IWindow window) {
Intrinsics.checkParameterIsNotNull(window, "window");
super();
this.$$delegate_0 = window;
}
public int getHeight() {
return this.$$delegate_0.getHeight();
}
public int getWidth() {
return this.$$delegate_0.getWidth();
}
}
public final class Kotlin20Kt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
IWindow window = (IWindow)(new TransparentWindow());
UI ui = new UI(window);
System.out.println("Width : " + ui.getWidth() + ", height: " + ui.getHeight());
}
}
구현을 보면, 메소드에 인자로 전달받은 TransparentWindow
객체와 동일한 이름의 메소드를 호출하도록 구현되어 있는 것을 확인할 수 있다. 결국은 IWindow
에 대한 메소드들이 모두 구현되어 있다. by
키워드가 이러한 수고를 덜어준 것이다.
아니 by 키워드 검색하는데 익숙한 닉네임이