15장. 디자인 패턴과 프레임워크

  • 디자인 패턴

    1. 소프트웨어에서 반복적으로 발생하는 문제에 대해 반복적으로 적용할 수 있는 해결 방법
    2. 다양한 변경을 다루거나 협력을 일관성 있게 만들기 위해 재사용할 수 있는 설계의 묶음
    3. 디자인 패턴의 목적 : 설계를 재사용하는 것
  • 프레임워크

    1. 설계와 코드를 재사용하기 위한 것
    2. 어플리케이션의 아키텍쳐를 구현 코드의 형태로 제공
    3. 각 애플리케이션의 요구에 따라 적절하게 커스터마이징할 수 있는 확장 포인트를 제공
    4. 일관성 있는 협력을 제공하는 확장 가능한 코드

1. 디자인 패턴과 설계 재사용

소프트웨어 패턴

패턴의 핵심적인 특징

  1. 패턴은 반복적으로 발생하는 문제와 해법의 쌍으로 정의된다.
  2. 패턴을 사용함으로써 이미 알려진 문제와 이에 대한 해법을 문서로 정리할 수 있으며, 이 지식을 다른 사람과 의사소통할 수 있다.
  3. 패턴은 추상적인 원칙과 실제 코드 작성 사이의 간극을 메워주며 실질적인 코드 작성을 돕는다.
  4. 패턴은 실무에서 탄생했다.

패턴이 지닌 가장 큰 가치는 실무 지식을 효과적으로 요약하고 전달할 수 있다는 점이다.

패턴은 홀로 존재하지 않는다. 특정 패턴 내에 포함된 컴포넌트와 컴포넌트 간의 관계는 더 작은 패턴에 의해 서술될 수 있으며, 패턴들을 포함하는 더 큰 패턴 내에 통합될 수 있다.

패턴은 연관된 패턴들의 집합들이 모여 하나의 **패턴 언어(Pattern Language)**를 구성한다.

패턴 언어는 연관된 패턴 카테고리뿐만 아니라 패턴의 생성 규칙과 함께 패턴 언어에 속한 다른 패턴과의 관계 및 협력 규칙을 포함한다.

POSA1이라는 책에서는 패턴 언어라는 용어가 지닌 제약 조건을 완화하기 위해 패턴 시스템(Pattern System)이라는 특수한 용어의 사용을 제안하기도 했으나 현재 두 용어는 거의 동일한 의미로 사용되고 있다.

패턴 분류

패턴은 범위나 적용 단계에 따라

  • 아키텍처 패턴(Architecture Pattern)
  • 분석 패턴(Analysis Pattern)
  • 디자인 패턴(Design Pattern)
  • 이디엄(Idiom)

총 4가지로 분류된다.

4가지 중에서 가장 널리 알려진 것은 디자인 패턴이다.

아키텍처 패턴

  • 디자인 패턴의 상위에 위치
  • 소프트웨어의 전체적인 구조를 결정
  • 구체적인 소프트웨어 아키텍처를 위한 템플릿을 제공
  • 프로그래밍 언어나 프로그래밍 패러다임에 독립적

디자인 패턴

  • 협력하는 컴포넌트들 사이에서 반복적으로 발생하는 구조를 서술함
  • 중간 규모의 패턴
  • 특정한 설계 문제를 해결하는 것을 목적으로 함
  • 프로그래밍 언어나 프로그래밍 패러다임에 독립적

이디엄

  • 디자인 패턴의 하위에 위치
  • 특정 프로그래밍 언어에만 국한된 하위 레벨 패턴
  • 언어의 기능을 사용해 컴포넌트 혹은 컴포넌트 간의 특정 측면을 구현하는 방법을 서술함

분석 패턴

  • 도메인 내의 개념적인 문제를 해결하는데 초점을 맞춤
  • 업무 모델링 시에 발견되는 공통적인 구조를 표현하는 개념들의 집합

패턴과 책임-주도 설계

패턴은 공통으로 사용할 수 있는 역할, 책임, 협력의 템플릿이다. 패턴은 반복적으로 발생하는 문제를 해결하기 위해 사용할 수 있는 공통적인 역할과 책임, 협력의 훌륭한 예제를 제공한다.

  • STRATEGY 패턴 : 다양한 알고리즘을 동적으로 교체할 수 있는 역할과 책임의 집합을 제공함
  • BRIDGE 패턴 : 추상화의 조합으로 인한 클래스의 폭발적인 증가 문제를 해결하기 위해 역할과 책임을 추상화와 구현의 두 개의 커다란 집합으로 분해함으로써 설계를 확장 가능하게 만듬
  • OBSERVER 패턴 : 유연한 통지 메커니즘을 구축하기 위해 객체 간의 결합도를 낮출 수 있는 역할과 책임의 집합을 제공한다.
  • COMPOSITE 패턴 : 클라이언트가 개별 객체와 복합 객체를 동일하게 취급 가능하게 함

패턴의 구성 요소인 Component, Composite, Leaf는 클래스가 아니라 협력에 참여하는 객체들의 역할이다. Component는 역할이기 때문에 Component가 제공하는 오퍼레이션을 구현하는 어떤 객체라도 Component의 역할을 수행할 수 있다.

1

패턴을 구성하는 요소가 클래스가 아니라 역할이라는 사실은 패턴 템플릿을 구현할 수 있는 다양한 방법이 존재한다는 사실을 암시한다. 역할은 동일한 오퍼레이션에 대해 응답할 수 있는 책임의 집합을 암시하기 때문에 아래와 같이 하나의 객체가 세 가지 역할 모두를 수행하더라도 문제가 없다.

2

반대로 다수의 클래스가 동일한 역할을 구현할 수도 있다.

아래의 그림은 8장에서 살펴본 중복 할인 정책의 구조를 다이어그램으로 표현한 것이다. 기본 구조는 COMPOSITE 패턴을 따른다.

3

위의 두 이미지 모두 올바른 COMPOSITE 패턴이다. 이것은 패턴을 적용하기 위해서는 패턴에서 제시하는 구조를 그대로 표현하는 것이 아니라 패턴의 기본 구조로부터 출발해서 현재의 요구에 맞게 구조를 수정해야 한다는 것을 의미한다. COMPOSITE 패턴은 협력하는 객체의 수 또한 캡슐화해준다. 그러므로 객체의 수가 변경되어도 Movie에 영향을 미치지 않는다.

캡슐화와 디자인 패턴

영화 예매 시스템에서 Movie가 DiscountPolicy 상속 계층을 합성 관계로 유지해야 하는 이유에 대해서 장황하게 설명했지만 사실 이 설계는 STRATEGY 패턴을 적용한 예다.

영화에 적용될 할인 정책의 종류는 Movie가 참조하는 DiscountPolicy의 서브클래스가 무엇이냐에 따라 결정된다. STRATEGY 패턴을 이용하면 Movie와 DiscountPolicy 사이의 결합도를 낮게 유지할 수 있기 때문에 런타임에 알고리즘을 변경할 수 있다.

4

물론 변경을 캡슐화하는 방법이 합성만 있는것은 아니다. 상속을 이용할 수도 있다.

아래의 그림은 위와 동일하지만 상속을 사용한 예다. 이처럼 알고리즘을 캡슐화하기 위해 합성 관계가 아닌 상속 관계를 사용하는 것을 TEMPLATE METHOD 패턴이라고 부른다.

5

추상 클래스나 인터페이스를 사용해 변경을 캡슐화하는 합성과 달리 상속을 사용할 경우에는 추상메서드를 이용해 변경을 캡슐화해야 한다. 자식 클래스들이 추상 메서드를 오버라이딩해서 변하는 부분을 구현한다는 것이 중요하다. 이것은 TEMPLATE METHOD 패턴의 전형적인 구현 방법이다.

이처럼 TEMPLATE METHOD 패턴은 부모 클래스의 알고리즘의 기본 구조를 정의하고 구체적인 단계는 자식 클래스에서 정의하게 함으로써 변경을 캡슐화할 수 있는 디자인 패턴이다. 다만 합성보다는 결합도가 높은 상속을 사용했기 때문에 STRATEGY 패턴처럼 런타임에 객체의 알고리즘을 변경하는 것은 불가능하다.

하지만 알고리즘 교체와 같은 요구사항이 없다면 STRATEGY 패턴보다 복잡도를 낮출 수 있다.

아래의 핸드폰 과금 시스템 설계는 DECORATOR 패턴을 기반으로 한다. 이 패턴은 객체의 행동을 동적으로 추가할 수 있게 해주는 패턴으로서 기본적으로 객체의 행동을 결합하기 위해 객체 합성을 사용한다. DECORATOR 패턴은 선택적인 행동의 개수와 순서에 대한 변경을 캡슐화 할 수 있다.

6

패턴은 출발점이다.

패턴은 출발점이지 목적지가 아니다. 패턴은 단지 목표로 하는 설계에 이를 수 있는 방향을 제시하는 나침반에 불과하다. 디저인 패턴이 현재의 요구사항이나 적용 기술, 프레임워크에 적합하지 않다면 패턴을 그대로 따르지 말고 목적에 맞게 수정하라.

패턴을 가장 효과적으로 적용하는 방법은 패턴을 지향하거나 패턴을 목표로 리팩터링을 하는것이다.

패턴이 적용된 최종 결과를 이해하는 것보다는 패턴을 목표로 리팩터링하는 이유를 이해하는 것이 훨씬 가치 있으며, 훌륭한 소프트웨어 설계가 발전해 온 과정을 공부하는 것이 훌륭한 설계 자체를 공부하는 것보다 훨씬 중요하다고 이야기한다.

2. 프레임워크와 코드 재사용

코드 재사용 대 설계 재사용

재사용 관점에서 설계 재사용보다 더 좋은 방법은 코드 재사용이다.

가장 이상적인 형태의 재사용 방법은 설계 재사용과 코드 재사용을 적절한 수준으로 조합하는 것이다.

설계를 재사용하면서도 유사한 코드를 반복적으로 구현하는 문제를 피할 수 있는 방법은 없을까? 이 질문에 대한 대답이 바로 프레임워크다.

프레임워크란 구조적인 측면에서는 '추상 클래스나 인터페이스를 정의하고 인스턴스 사이의 상호작용을 통해 시스템 전체 혹은 일부를 구현해 놓은 재사용 가능한 설계'이고, 코드와 설계의 재사용이라는 측면에서는 '애플리케이션 개발자가 현재의 요구사항에 맞게 커스터마이징할 수 있는 애플리케이션의 골격'을 의미한다.

프레임워크는 코드를 재사용함으로써 설계 아이디어를 재사용한다. 또한 구현된 추상 클래스와 인터페이스 집합뿐만 아니라 다양한 종류의 컴포넌트도 함께 제공한다.

비록 프레임워크가 즉시 업무에 투입할 수 있는 구체적인 서브클래스를 포함하고 있기는 하지만 프레임워크는 코드의 재사용보다는 설계 자체의 재사용을 중요시한다.

상위 정책과 하위 정책으로 패키지 분리하기

아래의 그림은 핸드폰 과금 시스템에서 추상화에 해당하는 부분을 짙은 색으로 표시한 것이다. 그림에서 알 수 있는 것처럼 구체적인 클래스들은 RatePolicy, AdditionalRatePolicy, FeeCondition에 의존하지만 추상화들은 구체 클래스에 의존하지 않는다는 것을 알 수 있다. 이 설계는 9장에서 살펴본 의존성 역전 원칙에 기반하고 있는 것이다.

7

상위 정책은 상대적으로 변경에 안정적이지만 세부 사항은 자주 변경된다.

그리고 상위 정책이 세부사항에 비해 재사용될 가능성이 높다.

기본정책과 부가정책을 조합하는 규칙은 모든 요금 계산 시에 재사용돼야 하는 협력 패턴이다.

그에 비해 시간대별 방식으로 요금을 계싼하거나 세금을 부과하는 것은 특수한 경우에만 사용되는 기본 정책과 부가 정책에 한 예라고 할 수 있다.

요점은 상위 정책이 세부 사항보다 더 다양한 상황에 재사용될 수 있어야 한다는 것이다.

하지만 상위 정책이 세부 사항에 의존하게 되면 세부 사항도 항상 함께 존재해야 하기 때문에 상위 정책의 재사용성이 낮아진다. 이를 해결할 수 있는 가장 좋은 방법은 의존성 역전 원칙에 맞게 상위 정책과 세부 사항 모두 추상화에 의존하게 만드는 것이다.

의존성 역전 원칙의 관점에서 세부 사항은 변경을 의미한다. 프레임워크는 여러 애플리케이션에 걸쳐 재사용 가능해야 하기 때문에 변하는 것과 변하지 않는 것들을 서로 다른 주기로 배포할 수 있도록 별도의 '배포 단위'로 분리해야 한다.

아래의 그림은 상위 정책을 구현하는 패키지와 세부 사항을 구현하는 클래스들을 서로 다른 패키지로 분리한 것이다.

8

중요한 것은 패키지 사이의 의존성 방향이다. 의존성 역전 원리에 따라 추상화에만 의존하도록 의존성의 방향을 조정하고 추상화를 경계로 패키지를 분리했기 세부사항을 구현한 패키지는 항상 상위 정책을 구현한 패키지에 의존해야 한다.

좀 더 나아가 상위 정책을 구현하고 있는 패키지가 충분히 안정적이고 성숙했다면 하위 정책 패키지로부터 완벽히 분리해서 별도의 배포 단위로 만들 수 있다.

9

이로써 상위 정책 패키지를 여러 애플리케이션에서 재사용 할 수 있는 기반이 마련됐다.

다시 말해 재사용 가능한 요금 계산 로직을 구현한 프레임워크가 만들어진 것이다.

제어 역전 원리

상위 정책을 재사용한다는 것은 결국 도메인에 존재하는 핵심 개념들 사이의 협력 관계를 재사용한다는 것을 의미한다.

객체 지향 설계의 재사용성은 개별 클래스가 아니라 객체들 사이의 공통적인 협력 흐름으로부터 나온다. 의존성 역전 원리는 전통적인 설계 방법과 객체지향을 구분하는 가장 핵심적인 원리다. 의존성 역전 원리에 따라 구축되지 않은 시스템은 협력 흐름을 재사용할 수도 없으며 변경에 유연하게 대처할 수도 없다.

의존성 역전 원리는 프레임워크의 가장 기본적인 설계 메커니즘이다. 의존성 역전은 의존성의 방향 뿐만 아니라 제어 흐름의 주체 역시 역전시킨다.

전통적인 구조에서는 상위 정책의 코드가 하부의 구체적인 코드를 호출한다.

하지만 의존성을 역전시킨 객체지향 구조에서는 상위 정책 대신 프레임워크가 애플리케이션에 속하는 서브클래스의 메서드를 호출한다.

따라서 프레임워크를 사용할 경우 애플리케이션에서 프레임워크로 제어 흐름의 주체가 이동한다.

이를 제어 역전(Inversion of Control) 원리, 또는 할리우드(Hollywood) 원리라고 한다.

아래의 그림은 핸드폰 과금 시스템의 프레임워크의 요소들을 이용해 기본 정책의 협력을 나타낸 것이다. 전체적인 협력 흐름은 프레임워크에 정의돼 있다. 특정 기본 정책을 구현하는 개발자는 FeeCondition을 대체할 서브타입만 개발하면 프레임워크에 정의된 플로우에 따라 요금이 계산된다.

10

프레임워크에서는 일반적인 해결책만 제공하고, 달라질 수 있는 특정한 동작은 비워둔다. 그리고 이렇게 완성되지 않은 채로 남겨진 동작을 훅(hook)이라고 부른다. 재정의된 훅은 제어 역전 원리에 따라 프레임워크가 원하는 시점에 호출된다.

여기서 협력을 제어하는 것은 프레임워크라는 것에 주목하라. 우리는 프레임워크가 적절한 시점에 실행할 것으로 예상되는 코드를 작성할 뿐이다.

객체지향의 시대에는 우리는 그저 프레임워크가 호출하는 코드를 작성해야 한다. 제어가 우리에게서 프레임워크로 넘어가 버린 것이다. 다시 말해서 제어가 역전된 것이다.

설계 수준의 재사용은 애플리케이션과 기반이 되는 소프트웨어 간에 제어를 바꾸게 한다. 라이브러리를 사용하여 애플리케이션을 작성하면 애플리케이션이 필요한 라이브러리의 코드를 호출한다. 즉, 애플리케이션 자체가 언제 어떤 라이브러리를 사용할 것인지 스스로 제어한다. 그러니 프레임워크를 재사용할 때는 프레임워크가 제공하는 메인 프로그램을 재사용하고 이 메인 프로그램이 호출하는 코드를 개발자가 작성해야 한다. 따라서 언제 자신이 작성한 코드가 호출될 것인지를 스스로 제어할 수 없다. 제어 주체가 자신이 아닌 프레임워크로 넘어간 것이다. 즉, 제어가 역전된 것이다. 개발자는 이미 특정 이름과 호출 방식이 결정된 오퍼레이션을 작성해야 하지만 결정해야 하는 설계 개념은 줄어들고 구체적인 오퍼레이션의 구현만 남게 된다.

이러한 제어의 역전이 프레임워크의 핵심 개념인 동시에 코드의 재사용을 가능하게 하는 힘이라는 사실을 이해해야 한다.

Last Updated: