지난 포스팅에서는 객체, 인터페이스, 클래스, 상속의 개념에 대해서 살펴 보았다(상단 링크 참고). 이번 포스팅에서는 디자인 패턴이 어떻게 이런 개념들을 유연하고 재사용 가능한 소프트웨어에 적용 시키는지 살펴보자.
재사용 가능한 소프트웨어
객체지향 시스템에서 기능의 재상용을 위한 가장 대표적인 방법은 상속과 객체 합성이다. 앞선 포스팅에서 상속에 대해서 알아봤으나, 객체 합성은 생소한 단어일 것이다. 객체 합성은 클래스 상속에 대한 대안으로, 새로운 기능성을 위해서 객체들을 합성하는 것이다. 객체를 합성하기위해서는 합성할 객체들의 인터페이스를 명확하게 정의해야 한다. 즉, 객체의 내부는 공개되지 않고 인터페이스를 통해서만 재사용 된다. 이 때문에 다른 클래스를 이용해서 한 클래서의 구현을 정의하는, 즉 서브클래싱에 의한 재사용을 하는 상속을 화이트박스 재사용(whitebox reuse)라고 하고, 인터페이스를 통한 재사용을 객체 합성을 블랙박스 재사용(blackbox reuse)라고 한다.
상속 vs 객체합성
상속의 특징
- 상속은 컴파일 시점에 정적으로 정의되고 그로그래밍 언어가 직접 지원하는 대로 사용하면 된다.
- 상속을 통해서 재사용하는 부모 클래스의 구현을 쉽게 수정할 수도 있는데, 서브클래스는 모든 오퍼레이션이 아닌 일부만도 재정의 할 수 있다.
상속의 단점
- 상속은 컴파일 시점에 결정되는 사항이기 때문에 런타임 시에 상속받은 부모 클래스의 구현을 변경할 수 없다.
- 부모 클래스는 서브 클래스의 물리적 표현의 최소 부분만을 정의하고 있어, 상속을 통해서 서브클래스는 부모 클래스가 정의한 물리적 표현들을 전부 또는 일부 상속받는다. 상속은 부모 클래스의 구현이 서브클래스에 다 드러나기 때문에 캡슐화를 파괴한다고도 한다. 이 때문에 서브클래스는 부모 클래스의 구현에 종속되며, 부모 클래스의 구현에 변경이 생기면 서브 클래스도 변경해야 한다.
객체 합성의 특징
- 합성은 객체가 다른 객체에 대한 참조자를 얻는 방석으로 런타임 시에 동적으로 이루어 진다.
- 합성은 객체가 다른 객체의 인터페이스만을 바라보게 한다. 이로써 객체의 인터페이스 정의에 보다 많은 주의를 기울여야 한다.
- 객체는 인터페이스를 통해서만 접근하므로 캡슐화를 유지할 수 있다.
- 객체가 동일한 타입을 갖는다면 다른 객체로 런타임 시에 대체가 가능하다.
- 객체의 구현은 인터페이스에 맞추어 이루어지므로 구현 간의 종속성은 확실히 줄어든다.
객체 합성이 클래스 합성보다 더 나은 방법이다.
클래스 상속보다 객체 합성을 더 선호하는 것은 각 클래스의 캡슐화를 유지할 수 있기 때문이다. 클래스와 클래스 계층은 소규모로 유지하면서, 큰 규모의 계층도가 만들어지는 것을 막아준다. 합성에 의한 설계가 이루어지면 클래스의 수는 적어지고 객체의 수는 좀 더 많아질 수 있지만, 시스템의 행위는 클래스에 정의된 정적인 내용보다는 합성에 의한 런타임 시의 상호 관련성에 따라 달리질 수 있다.
그러나 기존 컴포넌트의 조합을 통한 재사용만으로 목적을 달성하기는 어려울 수 있다. 상속에 의한 재사용은 기존의 클래스들과 조합해서 새로운 컴포넌트를 쉽게 만들 수 있도록 해주기 때문에 상속과 합성은 함께 실행되어야 완벽한 재사용이 가능하다.
위임
위임(Delegation)은 합성을 상속만큼 강력하게 만드는 방법이다. 위임에서는 두 객체가 하나의 요청을 처리한다. 수신 객체가 오퍼레이션의 처리를 위임자(Delegate)에게 보낸다. 이는 서브클래스가 부모 클래스에게 요청을 전달하는 것과 유사한 방식이다. 위임과 동일한 효과를 얻으려면 수신 객체는 대리자에게 자신을 파라미터로 전달해서 위임된 오퍼레이션이 수신자를 참조하게 한다.
위임의 단점은 객체 합성을 통해 소프트웨어 설계의 유연성을 보장하는 방법과 동일하게 동적이며, 고도로 파라미터화 된 스프트웨어는 정적인 스프트웨어 구조보다 이해하기가 더 어렵다는 것이다(러닝 커브가 높다는 뜻. 디자인패턴은 안높냐!!). 그 이유는 클래스에 상호작용이 다 정의되어 있는 것이 아니라 런타임 시의 객체에 따라서 그 결과가 다르기 때문이다. 또한 런 타임 시에 비효율적일 수 있다. 설계는 이런 위임이 만들어 내는 복잡함보다 단순화의 효과를 더 크게 할 수 있다면, 그것은 사용하기 좋은 설계 기법이다. 그러나 이러한 유용성은 상황에 따라 다르고 얼마나 많은 경험을 갖고 있는가에 좌우되므로 위임은 고도로 표준화 된 패턴에서 사용하는 것이 최상일 것이다.
상속 vs 파라미터화 된 타입
기능을 재사용 할 수 있는 다른 방법에는 파라미터화 된 타입(parameterized type)이 있다. 이 기법은 타입을 정의할 때 타입이 사용하는 모든 타입을 다 지정하지 않은 채 정의한다. 정의하지 않는 타입을 우리는 사용하는 시점에 파라미터로 제공한다.
** generic type을 생각하면 쉬울 것 같다. 아래는 C#으로 된 예제이다.
public class GenericList<T>
{
public void Add(T input) { }
}
class TestGenericList
{
private class ExampleClass { }
static void Main()
{
// Declare a list of type int.
GenericList<int> list1 = new GenericList<int>();
list1.Add(1);
// Declare a list of type string.
GenericList<string> list2 = new GenericList<string>();
list2.Add("");
// Declare a list of type ExampleClass.
GenericList<ExampleClass> list3 = new GenericList<ExampleClass>();
list3.Add(new ExampleClass());
}
}
지금까지의 내용을 정리해보자면, 객체지향 시스템에서 시스템을 합성할 수 있는 방법은 클래스 상속, 객체 합성, 그리고 파리미터화 된 타입이다. 많은 설계는 이 세 가지 기법 중 어느 하나로 구현하고 있다. 원소들을 비교하기 위한 정렬 루틴을 설계하는 세 가지 방법을 비교해 보자.
- 서브클래스에 의해 오퍼레이션을 구현하는 방법 : 상속
- 정렬 루틴으로 전달된 객체: 합성
- 클래스의 아규먼트로 원소를 비교할 수 있는 함수 이름을 명시: 파라미터화
이 세 가지 기법에는 중요한 차이가 있다. 객체 합성은 런타임 시에 행위를 변경할 수 있지만, 행위가 위임된다는 효율성이 있다. 상속은 오퍼레이션에 대한 기본 행위를 부모 클래스가 제공하고, 이를 서브클래스에서 재정의하도록 하는 것이라면, 파라미터화 된 타입은 클래스가 사용하는 타입을 변경하게 하는 것이다. 상속도 파라미터화된 타입이라고 볼 수 있지만, 런타임 시에 변경이 일어나지 않는다. 어떤 방법이 최적의 방법인가는 설계와 구현 제약 사항에 따라 달라질 수 있다.
GoF의 디자인패턴 참고
'Development > 디자인패턴' 카테고리의 다른 글
디자인 패턴을 이용하는 방법 (5) (0) | 2022.04.21 |
---|---|
디자인 패턴을 이용하는 방법 (4) (0) | 2022.04.20 |
디자인 패턴을 이용하는 방법 (2) (0) | 2022.04.18 |
디자인 패턴을 이용하는 방법 (1) (0) | 2022.04.14 |
디자인패턴의 조직화, 관계도 (0) | 2022.04.13 |