이 책은 크게 두 부분으로 나뉩니다. 첫 번째 부분에서는 디자인 패턴이 무엇인지와 디자인 패턴이 어떻게 객체지향 소프트웨어 설계에 도움을 주는 지 설명합니다. 두 번째 부분은 목록, 즉 카탈로그로 정리 된 실제 디자인 패턴 23개에 대한 설명입니다. 이 글에서는 책의 앞부분에 대해 요약하려고 합니다.

전문가들은 초보자들처럼 모든 문제를 처음 기초 단계에서부터 해결하려고 하지 않습니다. 대신, 전에 사용했던 해결책을 다시 사용해 봅니다. 디자인 패턴 이란 방식을 통해 소프트웨어 설계에서 얻은 세세한 경험들을 기록해 놓도록 하는 것으로 말이죠.


1. 디자인 패턴이란?

일반적으로 하나의 패턴에는 다음의 네 가지 요소가 반드시 들어 있습니다.

  1. 패턴 이름은 한 두 단어로 설계 문제와 해법을 서술합니다.
  2. 문제는 언제 패턴을 사용하는가를 서술하며 해결할 문제와 그 배경을 설명합니다.
  3. 해법은 설계를 구성하는 요소들과 그 요소들 간의 관계, 책임 그리고 협력 관계를 서술합니다.
  4. 결과는 디자인 패턴을 적용해서 얻는 결과와 장단점을 서술합니다.

2. 디자인 패턴을 이용하여 문제를 푸는 방법

클래스 상속 대 인터페이스 상속

객체의 클래스는 그 객체가 어떻게 구현되느냐를 정의합니다. 클래스는 객체의 내부 상태와 그 객체의 연산에 대한 구현 방법을 정의합니다. 반면, 객체의 타입은 그 객체의 인터페이스, 즉 그 객체가 응답할 수 있는 요청의 집합을 정의합니다. 하나의 객체가 여러 타입을 가질 수 있고 서로 다른 클래스의 객체들이 동일한 타입을 가질 수 있습니다. 즉, 객체의 구현은 다를지라도 인터페이스는 같을 수 있다는 의미입니다.

구현에 따르지 않고, 인터페이스에 따르는 프로그래밍

클래스 상속은 기본적으로 부모 클래스에서 정의한 구현을 재사용하여 응용프로그램의 기능성을 확장하려는 메커니즘입니다. 그러나 구현의 재사용이 전부는 아닙니다. 상속이 가진 다른 기능들 중에는 동일한 인터페이스를 갖는 객체군을 정의하는 것이 있는데, 매우 중요한 특징입니다. 객체군을 정의하는 것잉 중요한 이유는 그것으로 다형성을 끌어낼 수 있기 때문입니다.

추상 클래스를 정의하고 인터페이스 개념으로 객체를 다룰 때 얻을 수 있는 두 가지 이점은 다음과 같습니다.

  1. 사용자가 원하는 인터페이스를 그 객체가 만족하고 있는 한, 사용자는 그들이 사용하는 특정 객체 타입에 대해 알아야 할 필요는 없습니다.
  2. 사용자는이 객체들을 구현하는 클래스를 알 필요가 없고, 단지 인터페이스를 정의하는 추상 클래스가 무엇인지만 알면 됩니다.

재사용을 실현 가능한 것으로

객체지향 시스템에서 기능의 재사용을 위해 구사하는 가장 대표적인 기법은 클래스 상속, 그리고 객체합성입니다. 서브클래싱에 의한 재사용을 화이트박스 재사용이라고 합니다. 상속을 받으면 부모 클래스의 내부가 서브 클래스에 공개되기 때문에 화이트박스인 셈입니다. 반대로 객체 합성을 통해 재사용을 구현하는 방식을 블랙박스 재사용이라고 합니다.

클래스 상속보다 객체 합성을 더 선호하는 이유는 각 클래스의 캡슐화를 유지할 수 있고, 각 클래스의 한 가지 작업에 집중할 수 있기 때문입니다.

위임

위임(delegation)은 합성을 상속만큼 강력하게 만드는 방법입니다. 위임이 갖는 단점은, 객체 합성을 통해 소프트웨어 설계의 유연성을 보장하는 방법과 동일하게 동적인데다가 고도로 매개변수화된 소프트웨어는 정적인 소프트웨어 구조보다 이해하기가 더 어렵다는 것입니다. 이런 위임이 만들어 내는 복잡함보다 단순화의 효과를 더 크게 할 수 있다면 그 설계는 사용하기 좋은 설계입니다.

상속 대 매개변수화된 타입

기능의 재사용에 이용할 수 있는 다른 방법이 매개변수화된 타입(parameterized type) 입니다. Ada와 Eiffel에서는 제네릭(generic)이라고 하며, C++에서는 템플릿이라고 합니다. 매개변수화된 타입은 객체지향 시스템에서 행동을 복합할 수 있는 세 번째 방법입니다. 첫 번째가 클래스 상속이었고, 두 번째가 객체 합성이었습니다. 이 세 가지 기법에는 중요한 차이가 있습니다. 객체 합성은 런타임에 행동을 변경할 수 있지만, 행동이 위임되기 때문에 비효율적일 수 잇습니다. 상속이 연산에 대한 기본 행동을 부모 클래스가 제공하고 이를 서브클래스에서 재정의하도록 하는 것이라면, 매개변수화된 타입은 클래스가 사용하는 타입을 변경하게 하는 것입니다. 상속도 배개변수화된 타입이라고 볼 수 있지만, 런타임에 변경이 일어나지는 않습니다.

런타임 및 컴파일 타임의 구조를 관계짓기

객체 관계 중에는 집합(aggregation)인지(acquaintance) 라는 것이 있습니다. 집합은 한 객체가 다른 객체를 소유하거나 그것에 책임을 진다는 뜻입니다. 객체 인지는 한 객체가 다른 객체에 대해 알고 있음을 의미합니다. 이를 “연관(association)” 관계 또는 “사용(using)” 관계라고도 합니다. 집합 관계와 인지 관계는 구현상으로 동일할 때가 잦기 때문에, 언어의 처리 방식이 아닌 사용 목적에 따라 결정해야 합니다. 집합 관계는 인지 관계보다는 강력한 영속성의 개념을 갖습니다. 즉, 자전거에 바퀴가 있어야 한다는 것은 불변의 영속적 사실입니다. 이에 반해, 인지 관계는 자주 바뀌게 됩니다.

변화에 대비한 설계

디자인 패턴을 써서 재설계를 할 수 밖에 없게 하는 흔한 이유 몇개를 정리해 보았습니다.

  1. 특정 클래스에서 객체 생성

    객체를 생성할 때 클래스 이름을 명시하면 어떤 특정 인터페이스가 아닌 어떤 특정 구현에 종속됩니다. 이런 종속은 앞으로의 변화를 수용하지 못합니다. 이를 방지하려면 객체를 직접 생성해서는 안 됩니다. (추상 팩토리, 팩토리 메서드, 원형)

  2. 특정 연산에 대한 의존성

    특정한 연산을 사용하면, 요청을 만족하는 한 가지 방법에만 매이게 됩니다. 요청의 처리 방법을 직접 코딩하는 방식을 피하면, 컴파일 시점과 런타임 모두를 만족하면서 요청 처리 방법을 쉽게 변경할 수 있습니다. (책임 연쇄, 명령)

  3. 하드웨어와 소프트웨어 플랫폼에 대한 의존성

    기존에 존재하는 시스템 인터페이스와 응용프로그램 인터페이스는 소프트웨어 및 하드웨어 플랫폼마다 모두 다릅니다. 특정 플랫폼에 종속된 소프트웨어는 다른 플랫폼에 이식하기도 어렵고요. 또한 본래의 플랫폼에서도 버전의 변경을 따라가기 어려울 수도 있습니다. 이런 플랫폼 종속성을 제거하는 것은 시스템 설계에 있어 매우 중요합니다. (추상 팩토리, 가교)

  4. 객체의 표현이나 구현에 대한 의존성

    사용자가 객체의 표현 방법, 저장 방법, 구현 방법, 존재의 위치에 대한 모든 방법을 알고 있다면 객체를 변경할 때 사용자도 함께 변경해야 합니다. 이런 정보를 사용자에게 감춤으로써 변화의 파급을 막을 수 있습니다. (추상 팩토리, 가교, 메멘토, 프록시)

  5. 알고리즘 의존성

    알고리즘 자체를 확장할 수도, 최적화할 수도, 다른 것으로 대체할 수도 있는데, 알고리즘에 종속된 객체라면 알고리즘이 변할 때마다 객체도 변경해야 합니다. 그러므로 변경이 가능한 알고리즘은 분리해 내는 것이 바람직합니다. (빌더, 반복자, 전략, 템플릿 메서드, 방문자)

  6. 높은 결합도

    높은 결합도를 갖는 클래스들은 독립적으로 재사용하기 어렵습니다. 높은 결합도를 갖게 되면 하나의 커다란 시스템이 되어 버립니다. 이렇게 되면 클래스 하나를 수정하기 위해서 전체를 이해해야 하고 다른 많은 클래스도 변경해야 합니다. 또한 시스템은 배우기도 힘들고, 이식은 커녕 유지 보수하기조차도 어려운 공룡이 되어 버립니다. 추상 클래스 수준에서 결합도를 정의한다거나 계층화시키는 방법으로 디자인 패턴은 낮은 결합도의 시스템을 만들도록 합니다. (추상 팩토리, 가교, 책임 연쇄, 명령, 퍼사드, 중재자, 감시자)

  7. 서브클래싱을 통한 기능 확장

    서브클래싱으로 객체를 재정의하는 것은 쉬운 일이 아닙니다. 새로운 클래스마다 매번 반드시 해야 하는 초기화, 소멸 등에 대한 구현 오버헤드를 늘 지게 됩니다. 서브클래스를 정의하려면, 최상위 클래스부터 자신의 직속 부모 클래스까지 모든 것을 이해하고 있어야 합니다. 일반적으로 객체 합성과 위임은 행동 조합을 위한 상속보다 훨씬 유연한 방법입니다. 한편 객체 합성을 많이 사용한 시스템은 이해하기가 어려워집니다. 많은 디자인 패턴에서는 그냥 서브클래스를 정의하고 다른 인스턴스와 새로 정의한 클래스의 인스턴스를 합성해서 기능을 재정의하는 방법을 도입합니다. (가교, 책임 연쇄, 장식자, 감시자, 전략)

  8. 클래스 변경이 편하지 못한 점

    가끔 클래스를 변경하는 작업이 그렇게 단순하지 않을 때가 많습니다. 소스 코드가 필요한데 없다고 가정해 봅시다. 또한 어떤 변경을 하면 기존 서브클래스의 다수를 수정해야 한다고 가정합시다. 디자인 패턴은 이런 환경에서 클래스를 수정하는 방법을 제시합니다. (적응자, 장식자, 방문자)


3. 디자인 패턴을 고르는 방법

  1. 패턴이 어떻게 문제를 해결하는지 파악합시다.
  2. 패턴의 의도 부분을 봅시다.
  3. 패턴들 간의 관련성을 파악합시다.
  4. 비슷한 목적의 패턴들을 모아서 공부합시다.
  5. 재설계의 원인을 파악합시다.
  6. 설계에서 가변성을 가져야 하는 부분이 무엇인지 파악합시다.

참고

에릭 감마, 리처드 헬름, 랄프 존슨, 존 블리시디스. GoF의 디자인 패턴. 프로텍미디어 2015


GoF의 디자인 패턴 (Summary) 시리즈입니다.

  1. GoF의 디자인 패턴 (Summary) - 1
  2. GoF의 디자인 패턴 (Summary) - 2. 추상 팩토리(Abstract Factory)
  3. GoF의 디자인 패턴 (Summary) - 3. 팩토리 메서드(Factory Method)
  4. GoF의 디자인 패턴 (Summary) - 4. 단일체(Singleton)
  5. GoF의 디자인 패턴 (Summary) - 5. 적응자(Adapter)
  6. GoF의 디자인 패턴 (Summary) - 6. 복합체(Composite)
  7. GoF의 디자인 패턴 (Summary) - 7. 장식자(Decorator)
  8. GoF의 디자인 패턴 (Summary) - 8. 퍼사드(Facade)
  9. GoF의 디자인 패턴 (Summary) - 9. 감시자(Observer)
  10. GoF의 디자인 패턴 (Summary) - 10. 전략(Strategy)
  11. GoF의 디자인 패턴 (Summary) - 11 템플릿 메서드(Template Method)