
원활한 이해를 위해, 아래 사항의 선행 지식이 필요합니다.
- 상속과 조합
- 결합도
In software engineering, the
delegation patternis an object-oriented design pattern that allows object composition to achieve the same code reuse as inheritance.
조합을 활용하며 상속과 같은 코드 재사용을 달성할 수 있도록 하는 객체지향 디자인 패턴입니다. 여기서 Delegation은 위임이라는 의미를 가지는데, 어떤 일을 직접 수행하지 않고 다른 대상이 대신 수행하도록 맡기는 것을 의미합니다. 이 의미를 토대로 Delegation Pattern을 이해해보도록 합시다.
기존 코드를 재사용하는 대표적인 방식들로는 상속과 조합이 있습니다.
"상속보다 조합을 사용하라"는 객체지향을 접해본 사람이라면 한 번쯤 들어봤을 정도로 유명한 원칙입니다. 일반적으로는 is-a 관계를 장담할 수 있는 것이 아니라면 독립적인 두 객체 간 has-a 관계를 형성하는 조합을 활용하는 것이 권장됩니다. 유지보수 측면에서 상속이 위험한 대표적인 이유는 다음과 같습니다.
상속 계층이 깊어지며 경우의 수가 기하급수적으로 증가
👉 상위 클래스를 변경할 때 하위 클래스들이 예상치 못한 동작을 할 위험을 파악하기 어려움

상속의 문제점으로 클래스 폭발(Class Explosion)이 있습니다. 위와 같은 모습의 상속 구조가 있으며, 이 계층이 추후 확장될 수도 있다고 가정해 보겠습니다. 수많은 계층이 존재하는 상태에서 1번 클래스에 변경 사항이 발생한다면, 연쇄적으로 하위 클래스에도 그 영향이 반영될 수 있기에 고려해야 할 경우의 수가 기하급수적으로 늘어날 것입니다. 이렇듯 복잡한 상속관계가 형성되면, 코드의 예측 가능성이 떨어지게 됩니다.
결합도 측면에서 볼 때도, 상속으로 엮인 클래스들끼리는 서로의 함수와 프로퍼티에 필요 이상으로 과도하게 접근할 수 있게 됩니다. 이는 클래스 간의 의존성을 높이고 캡슐화를 약화시켜, 코드의 유지보수성과 재사용성을 저하시킬 수 있습니다. 이것 말고도 다른 여러 이유들로, 상속은 신중하게 사용해야 한다는 말이 많이 나오고 있습니다.
대안으로 조합을 활용한다면, 코드의 유연성을 크게 향상시킬 수 있습니다. 기능을 독립적인 객체로 분리하고, 필요한 기능만을 가진 객체를 다른 객체의 내부에 포함시킵니다. 이를 통해 특정 변경사항이 불필요하게 다른 부분으로 전파되는 문제를 효과적으로 방지할 수 있습니다.
조합을 이해하고 있다면, 사실상 위임도 이해한 것이라고 볼 수 있습니다.
Delegation Pattern은 조합(Composition)을 기반으로 하여, 상속보다 유연한 코드 재사용 효과를 낼 수 있는 방법
조합과 위임을 적용한 코드를 비교해보며 자세히 알아보겠습니다.
기존의 Engine에 새로운 기능을 가진 Car와 Boat를 만들기 위해, 상속을 활용한 코드입니다. Car에는 drive()와 park() 기능을, Boat에는 sail()과 dock() 기능을 추가하는 과정에서 Engine의 메서드를 재사용할 수 있습니다.
open class Engine {
fun start() {
println("Engine starting")
}
fun stop() {
println("Engine stopping")
}
}
class Car : Engine() {
fun drive() {
start()
println("Car is moving")
}
}
class Boat : Engine() {
fun sail() {
start()
println("Boat is sailing")
}
}
상속이 잠재적으로 일으킬 수 있는 문제점이 많기에, 조합을 사용하여 해결해보도록 하겠습니다.
먼저 Engine을 상속 불가능하도록 변경합니다.(open class -> class)
class Engine {
fun start() {
println("Engine starting.")
}
fun stop() {
println("Engine stopping.")
}
}
Car와 Boat가 내부에 Engine 인스턴스를 가지도록 하여, Car/Boat는 Engine이다라는 관계를 Car/Boat가 Engine을 가진다 라는 관계로 변경합니다.
class Car(private val engine: Engine) {
fun drive() {
engine.start()
println("Car is moving.")
}
fun park() {
engine.stop()
println("Car is parked.")
}
}
class Boat(private val engine: Engine) {
fun sail() {
engine.start()
println("Boat is sailing.")
}
fun dock() {
engine.stop()
println("Boat is docked.")
}
}
기존 조합 코드에 위임 패턴을 적용해보겠습니다.
위임 패턴에는 크게 두 가지 역할이 존재합니다.
Receiver: 수신 객체
👉 대신 해줘~ (다른 객체에게 위임을 '요청하는' 객체)
Delegate: 위임 객체
👉 대신 해줄게~ (위임을 '요청 받는' 객체)
위임을 적용하기 위해 반드시 공통 인터페이스를 구현할 필요는 없지만, 공통된 역할을 수행할 때는 인터페이스를 활용하는 것이 일반적이고 유용합니다. 위임 객체의 구현을 변경하더라도 코드의 일관성을 유지할 수 있는 등의 이점이 있기 때문입니다.
조합을 사용했을 때의 코드를 살펴보면, Engine뿐만 아니라 Car, Boat도 궁극적으로는 start와 stop이라는 역할을 수행합니다. 따라서 일관성을 위하여 Startable 인터페이스를 만들고, Engine과 Car, Boat가 모두 해당 인터페이스의 start(), stop()을 구현하도록 하겠습니다.
interface Startable {
fun start()
fun stop()
}
Startable을 구현한 Engine의 모습입니다.
class Engine : Startable {
override fun start() {
println("Engine starting")
}
override fun stop() {
println("Engine stopping")
}
}
아래 코드는 Startable을 구현한 Car와 Boat입니다. Car, Boat(Receiver)가 Engine(Delegate)에게 자신들의 행동을 위임한 모습입니다.
수신 객체(Receiver) : Car, Boat
start(), stop()을 요청받지만, 직접 처리하지 않고 Engine에게 위임!
엔진을 이용해 자동차를 움직이고 멈추는 역할
위임 객체(Delegate) : Engine
start(), stop() 요청을 실제로 수행!
Car가 동작을 위임하면, 실제 엔진을 켜고 끄는 작업을 처리
수신 객체인 Car와 Boat는 같은 기능(start, stop)을 굳이 다시 구현하지 않고 넘겨받은 Engine에 위임하여 engine의 메서드 사용을 통해 코드 재사용을 달성하고 있습니다. 이것이 바로 위임을 통한 코드 재사용 원리인 것입니다.
class Car(private val engine: Engine) : Startable {
override fun start() {
engine.start()
}
override fun stop() {
engine.stop()
}
fun drive() {
start()
println("Car is moving")
}
fun park() {
stop()
println("Car is parked")
}
}
class Boat(private val engine: Engine) : Startable {
override fun start() {
engine.start()
}
override fun stop() {
engine.stop()
}
fun sail() {
start()
println("Boat is sailing")
}
fun dock() {
stop()
println("Boat is docked")
}
}
조합의 코드와 비교해보았을 때, 추가적인 추상화가 이뤄지고 메서드가 추가된 것 이외에는 크게 다른 점이 없어보입니다. 사실 위임을 위해 조합을 사용하는 만큼, 구현 방식은 사실상 같습니다. 단지 어느 관점으로 코드를 작성하느냐에 따라 두 개념이 구분될 수 있다는 것을 인지해야 합니다.
조합 : '부분이 전체를 구성한다'는 관점으로, 여러 객체의 기능을 조합하여 새로운 기능을 구현하는 것에 중점위임 : 객체가 다른 객체에게 자신의 책임을 넘겨서 작업을 맡긴다는 관점한편, 위의 코드는 더 개선해볼 수 있습니다. start(), stop()을 구현하는 과정에서 보일러 플레이트 코드가 발생하였는데, 이어질 내용에서 이 문제를 해결해보겠습니다.
Kotlin에서는 언어적인 차원에서 Delegation Pattern을 보일러 플레이트 코드 없이 적용할 수 있도록 하는 문법을 지원하고 있습니다. Kotlin을 사용할 때, 위임 과정에서의 불편함을 줄이기 위해 사용할 수 있는 것이 by 키워드입니다.
위임 객체(Delegate)와 수신 객체(Receiver)가 같은 타입을(공통된 상위 추상 클래스나 인터페이스) 구현한다면,
by키워드를 활용하여 보일러플레이트 코드를 줄일 수 있다!
기존 : + 인터페이스/추상클래스(A) 형태에 by + (A)를 구현한 인스턴스(B) 형태를 추가적으로 활용하면 됩니다. 아래의 코드는 앞에서 살펴보았던 코드와 같은 동작을 합니다.
by 키워드가 위임 과정에서 Car, Boat와 Engine이 공통적으로 가지고 있는 Startable 메서드(start, stop)를 직접 구현해야 하는 불편함을 줄여주는 역할을 해주고 있습니다.
class Car(private val engine: Startable) : Startable by engine {
fun drive() {
start()
println("Car is moving")
}
fun park() {
stop()
println("Car is parked")
}
}
class Boat(private val engine: Startable) : Startable by engine {
fun sail() {
start()
println("Boat is sailing")
}
fun dock() {
stop()
println("Boat is docked")
}
}
해당 코드를 Kotlin 바이트 코드로 변환 후 다시 자바로 변환하면, 다음과 같은 결과를 확인할 수 있습니다. 이전에 작성했던 코드의 모습과 유사하게, 직접 Engine 프로퍼티의 함수들을 호출하여 Startable의 메서드를 구현하는 방식으로 위임을 진행하고 있습니다.
public final class Car implements Startable {
private final Engine engine;
public final void drive() {
this.start();
String var1 = "Car is moving";
System.out.println(var1);
}
public final void park() {
this.stop();
String var1 = "Car is parked";
System.out.println(var1);
}
public Car(@NotNull Engine engine) {
Intrinsics.checkNotNullParameter(engine, "engine");
super();
this.engine = engine;
}
public void start() {
this.engine.start();
}
public void stop() {
this.engine.stop();
}
}
당연히 필요한 경우에는 오버라이드도 가능합니다. 아래와 같이 start()의 동작을 바꾸고 싶어 오버라이드를 진행하면, drive() 실행 결과 역시 달라지게 될 것입니다. 한편, 넘겨받은 engine의 start()에는 변화가 생기지 않는다는 것에 유의해야 합니다.
class Engine : Startable {
override fun start() {
println("Engine starting")
}
override fun stop() {
println("Engine stopping")
}
}
class Car(private val engine: Engine) : Startable by engine {
override fun start() {
println("Engine new start!")
}
fun drive() {
start() // ***"Engine new start!"***
engine.start() // ***"Engine starting"***
println("Car is moving")
}
fun park() {
stop()
println("Car is parked")
}
}
이러한 방식으로 클래스 차원에서 위임을 적용하는 것을 Class Delegation이라고 합니다.
사용할 때는 상속으로 설계했을 때와 유사하지만, 조합으로 얻을 수 있는 효과를 얻으면서 Engine을 활용할 수 있게 되었습니다.
참고로 Kotlin에서는 프로퍼티 차원에서도 Delegation 기능을 제공하는데, 이는 Property Delegation이라고 합니다. 해당 개념은 추후 다른 포스트에서 다루도록 하겠습니다.
Delegation Pattern - Wikipedia
Delegation - Kotlin Docs
Delegated Properties - Kotlin Docs