디자인 패턴과 프레임워크

애플리케이션을 설계하다 보면 어떤 요구사항을 해결하기 위해 과거에 경험했던 유사한 해결 방법을 다시 사용하는 경우가 있다. 이처럼 소프트웨어 설계에서 반복적으로 발생하는 문제에 대해 반복적으로 적용할 수 있는 해결 방법을 [디자인 패턴]이라고 한다. 디자인 패턴의 목적은 설계를 재사용하는 것이다. 디자인 패턴은 다양한 변경을 다루기 위해 반복적으로 재사용 할 수 있는 설계의 묶음이다. 일단 디자인 패턴을 익히고 나면 변경의 방향과 주기를 이해하는 것만으로도 필요한 역할과 책임, 역할들의 협력 방식을 순간적으로 떠올릴 수 있게 된다.

디자인 패턴설계를 재사용하는 것이라면 프레임워크설게와 코드를 함께 재사용하기 위한 것이다. 프레임워크는 애플리케이션의 아키텍처를 구현 코드의 형태로 제공한다. 프레임워크가 제공하는 아키텍처가 요구사항에 적합하다면 다양한 환경에서 테스트를 거친 견고한 구현 코드를 쉽고 빠르게 재사용 할 수 있다. 프레임워크는 각 애플리케이션 요구에 따라 적절하게 커스터마이징할 수 있는 확장 포인트를 제공한다.

디자인 패턴과 프레임워크 모두 일관성 있는 협력과 관련이 있다. 디자인 패턴특정한 변경을 일관성 있게 다룰 수 있는 협력 템플릿을 제공한다. 디자인 패턴이 협력을 일관성 있게 만들기 위해 재사용할 수 있는 설계의 묶음이라면, 프레임워크는 일관성 있는 협력을 제공하는 확장 가능한 코드라고 할 수 있다. 결론적으로 디자인 패턴과 프레임워크 모두 협력을 일관성 있게 만들기 위한 방법이다.

디자인 패턴과 설계 재사용

소프트웨어 패턴

GOF의 디자인 패턴에 의해 패턴이 대중화된 이후 소프트웨어와 관련된 패턴을 다루는 수많은 저작물이 쏟아져 나왔으며 그에 비례해서 패턴의 정의 역시 다양하고 풍부해져 왔다. 패턴이라는 거대한 숲 속에서 길을 잃지 않기 위해서는 패턴의 정의보다는 패턴이라는 요어 자체가 풍기는 미묘한 뉘앙스를 이해하는 것이 중요하다.

패턴이란 무엇인가를 논의할 때면 반복적으로 언급되는 몇 가지 핵심적인 특징이 있다.

  • 패턴은 반복적으로 발생하는 문제와 해법의 쌍으로 정의된다.

  • 패턴을 사용함으로써 이미 알려진 문제와 이에 대한 해법을 문서로 정리할 수 있으며, 이 지식을 다른 사람과 의사소통 할 수 있다.

  • 패턴은 추상적인 원칙과 실제 코드 작성 사이의 간극을 메워주며 실질적인 코드 작성을 돕는다.

  • 패턴의 요점은 패턴이 실무에서 탄생했다는 점이다.

패턴은 한 컨텍스트에서 유용한 동시에 다른 컨텍스트에서도 유용한 ‘아이디어’다. 일반적으로 패턴으로 인정하기 위한 조건으로 3의 규칙을 언급한다. 이 규칙에 따르면 최소 세 가지의 서로 다른 시스템에 특별한 문제 없이 적용할 수 있고 유용한 경우에만 패턴으로 간주할 수 있다.

패턴이 지닌 가장 큰 가치는 경험을 통해 축적된 실무 지식을 효과적으로 요약하고 전달할 수 있다는 점이다. 패턴은 경험의 산물이다. 책상 위에서 탄생한 이론이나 원리와 달리 패턴은 치열한 실무 현장의 역학관계 속에서 검증되고 입증된 자산이다. 따라서 실무 경험이 적은 초보자라고 하더라도 패턴을 익히고 반복적으로 적용하는 과정 속에서 유연하고 품질 높은 소프트웨어를 개발하는 방법을 익힐 수 있게 된다.

패턴은 지식 전달과 커뮤니케이션의 수단으로 활용할 수 있기 때문에 패턴에서 가장 중요한 요소는 패턴의 이름이다. 패턴의 이름은 커뮤니티가 공유할 수 있는 중요한 어휘집을 제공한다. 잘 알려진 이름을 사용함으로써 “인터페이스를 하나 추가하고 이 인터페이스를 구체화 하는 클래스를 만든 후 객체의 생성자나 setter 메서드에 할당해서 런타임 시에 알고리즘을 바꿀 수 있게 하자” 는 장황한 대화가 STRATEGY 패턴을 적용하자는 단순한 대화로 바뀐다. 패턴의 이름은 높은 수준의 대화를 가능하게 하는 원천이다.

패턴의 범위는 소프트웨어 개발과 직접적인 연관성을 가진 분석, 설계, 구현 영역만으로 한정되는 것은 아니다. 다양한 크기의 프로젝트 조직을 구성하는 방법, 프로젝트 일정을 추정하는 방법, 스토리 카드나 백로그를 통해 요구사항을 관리하는 방법과 같이 반복적인 규칙을 발견할 수 있는 모든 영역이 패턴의 대상이 될 수 있다.

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

패턴 분류

패턴을 분류하는 가장 일반적인 방법은 패턴의 범위나 적용 단계에 따라 아키텍처 패턴, 분석 패턴, 디자인 패턴, 이디엄의 4가지로 분류하는 것이다. 4가지 중에서 가장 널리 알려진 것은 디자인 패턴이다. 디자인 패턴은 특정 정황 내에서 일반적인 설계 문제를 해결하며, 협력하는 컴포넌트들 사이에서 반복적으로 발생하는 구조를 서술한다. 디자인 패턴은 중간 규모의 패턴으로, 특정한 설계 문제를 해결하는 것을 목적으로 하며, 프로그래밍 언어나 프로그래밍 패러다임에 독립적이다.

패턴과 책임-주도 설계

객체지향 설계에서 가장 중요한 일은 올바른 책임을 올바른 객체에게 할당하고 객체 간의 유연한 협력 관계를 구축하는 일이다. 책임과 협력의 윤곽은 캡슐화, 크기, 의존성, 유연성, 성능, 확장 가능성, 재사용성 등의 다양한 요소들의 트레이드오프를 통해 결정된다. 가끔씩 책임과 협력을 결정하는 작업이 손쉽게 진행될 때도 있지만 대부분의 경우에는 훌륭한 품질의 설계를 얻기 위해 많은 시간과 노력을 들여야만 한다. 어떤 책임이 필요한가? 이 책임을 어떤 객체에게 할당해야 하는가? 유연하고 확장 가능한 협력 관계를 구축하기 위해서는 객체와 객체 간에 어떤 의존성이 존재해야 하는가?

패턴공통으로 사용할 수 있는 역할, 책임, 협력의 템플릿이다. 또한 패턴반복적으로 발생하는 문제를 해결하기 위해 사용할 수 있는 공통적인 역할과 책임, 협력의 훌륭한 예제를 제공한다. 예를 들면 STRATEGY 패턴은 다양한 알고리즘을 동적으로 교체할 수 있는 역할과 책임의 집합을 제공한다. BRIDGE 패턴 은 추상화의 조합으로 인한 클래스의 폭발적인 증가 문제를 해결하기 위해 역할과 책임을 추상화와 구현의 두 개의 커다란 집합으로 분해함으로써 설계를 확장 가능하게 한다. OBSERVER 패턴은 유연한 통지 메커니즘을 구축하기 위해 객체 간의 결합도를 낮출 수 있는 역할과 책임의 집합을 제공한다.

여기서 언급한 패턴들의 세부적인 내용이 중요한 것이 아니다. 중요한 것은 패턴을 따르면 특정한 상황에 적용할 수 있는 설계를 쉽고 빠르게 떠올릴 수 있다는 사실이다. 특정한 상황에 적용 가능한 패턴을 잘 알고 있다면 책임 주도 설계의 절차를 하나하나 따르지 않고도 시스템 안에 구현할 객체들의 역할과 책임, 협력 관계를 빠르고 손쉽게 구성할 수 있다.

패턴의 구성 요소는 클래스가 아니라 '역할'이다. 예를 들어, 클라이언트가 개별 객체와 복합 객체를 동일하게 취급할 수 있는 COMPOSITE 패턴을 살펴보자. 아래 그림은 COMPSITE 패턴의 일반적인 구조를 표현한 것이다. 패턴의 구성 요소인 Component, Composite, Leaf는 클래스가 아니라 협력에 참여하는 객체들의 역할이다. Component가 역할이기 때문에 Component가 제공하는 오퍼레이션을 구현하는 어떤 객체라도 Component의 역할을 수행할 수 있다.

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

반대로 다수의 클래스가 동일한 역할을 구현할 수도 있다. 아래 그림은 8장에서 살펴본 중복 할인 정책의 구조를 다이어그램으로 표현한 것이다. 중복 할인 설계의 기본 구조는 COMPOSITE 패턴을 따른다. OverlappedDiscountPolicyComposite의 역할을 수행하고 AmountDiscountPolicyPercentDiscountPolicyLeaf의 역할을 수행한다. 여기서는 서로 다른 두 클래스인 AmountDiscountPolicyPercentDiscountPolicy가 동일한 Leaf의 역할을 수행한다는 점을 주목하자.

디자인 패턴의 구성요소가 클래스와 메서드가 아니라 역할과 책임이라는 사실을 이해하는 것이 중요하다. 어떤 구현 코드가 어떤 디자인 패턴을 따른다고 이야기할 때는 역할, 책임, 협력의 관점에서 유사성을 공유한다는 것이지 특정한 구현 방식을 강제하는 것이 아니라는 점을 이해하는 것 역시 중요하다. 디자인 패턴은 단지 역할과 책임, 협력의 패턴을 제안할 뿐 구체적인 구현 방법에 대해서는 제한을 두지 않는다.

위 그림 모두 올바른 COMPOSITE 패턴이다. 두 가지 모두 COMPOSITE 패턴에서 제공하는 기본적인 역할과 책임, 협력 관계를 준수한다. 이것은 패턴을 적용하기 위해서는 패턴에서 제시하는 구조를 그대로 표현하는 것이 아니라 패턴의 기본 구조로부터 출발해서 현재의 요구에 맞게 구조를 수정해야 한다는 것을 의미한다.

캡슐화와 디자인 패턴

몇 가지 이례적인 경우를 제외하면 널리 알려진 대부분의 디자인 패턴은 협력을 일관성 있고 유연하게 만드는 것을 목적으로 한다. 따라서 디자인 패턴은 특정한 변경을 캡슐화하기 위한 독자적인 방법을 정의하고 있다.

영화 예매 시스템에서 MovieDiscountPolicy 상속 계층을 합성 관계로 유지해야 하는 다양한 설계 원칙과 이유에 대해 장황하게 설명했지만 사실 이 설계는 STRATEGY 패턴을 적용한 예다. STRATEGY 패턴의 목적은 알고리즘의 변경을 캡슐화하는 것이고 이를 구현하기 위해 객체 합성을 이용한다.

아래 그림은 STRATEGY 패턴을 적용한 영화 예매 시스템 설계를 표현한 것이다. 영화에 적용될 할인 정책의 종류는 Movie가 참조하는 DiscountPolicy서브클래스가 무엇이냐에 따라 결정된다. 그리고 STRATEGY 패턴을 이용하면 MovieDiscountPolicy 사이의 결합도를 낮게 유지할 수 있기 때문에 런타임에 알고리즘을 변경할 수 있다.

물론 변경을 캡슐화 하는 방법이 합성만 있는 것은 아니다. 상속을 이용할 수도 있다. 아래 그림은 Movie의 인터페이스 측면에서는 위 그림과 동일하지만 변경을 캡슐화 하기 위해 상속을 사용한 예다. 여기서 변하지 않는 부분은 Movie이고 변하는 부분은 AmountDiscountPolicyPercentDiscountPolicy다. 그리고 변경하지 않는 부분은 부모 클래스, 변하는 부분은 자식 클래스로 분리함으로써 변경을 캡슐화한다. 이처럼 알고리즘을 캡슐화하기 위해 합성 관계가 아닌 상속 관계를 사용하는 것을 TEMPLATE METHOD 패턴이라고 한다.

추상 클래스나 인터페이스를 사용해 변경을 캡슐화하는 합성과 달리 상속을 사용할 경우에는 추상 메서드를 이용해 변경을 캡슐화해야 한다. 위 그림에서 calculateDiscountAmount 메서드가 바로 서브클래스의 변경을 캡슐화하기 위해 사용되는 추상 메서드다. 부모 클래스의 calculateFee 메서드 안에서 추상 메서드인 calculateDiscountAmount를 호출하고 자식 클래스들이 이 메서드를 오버라이딩해서 변하는 부분을 구현한다는 것이 중요하다. 이것은 TEMPLATE METHOD 패턴의 전형적인 구현 방법이다.

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

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

대부분의 디자인 패턴의 목적은 특정한 변경을 캡슐화함으로써 유연하고 일관성 있는 협력을 설계할 수 있는 경험을 공유하는 것이다. 디자인 패턴에서 중요한 것은 디자인 패턴의 구현 방법이나 구조가 아니다. 어떤 디자인 패턴이 어떤 변경을 캡슐화 하는지를 이해하는 것이 중요하다. 그리고 각 디자인 패턴이 변경을 캡슐화 하기 위해 어떤 방법을 사용하는지를 이해하는 것이 더 중요하다.

OverlappedDiscountPolicy의 예를 통해 살펴본 COMPOSITE 패턴은 개별 객체와 복합 객체라는 객체의 수와 관련된 변경을 캡슐화 하는 것이 목적이다. Movie는 자신과 협력해야 하는 DiscountPolicy 인스턴스가 단일 객체인지 복합 객체인지를 알 필요가 없다. 다시 말해서 협력하는 객체의 수를 변경하더라도 Movie에 영향을 미치지 않는다.

패턴은 출발점이다.

패턴은 출발점이지 목적지가 아니다. 많은 전문가들은 널리 요구되는 유연성이나 공통적으로 발견되는 특정한 설계 이슈를 해결하기 위해 적절한 디자인 패턴을 이용해 설계를 시작한다. 그러나 패턴은 설계의 목표가 되어서는 안된다. 패턴은 단지 목표로 하는 설계에 이를 수 있는 방향을 제시하는 나침반에 불과하다. 디자인 패턴이 현재의 요구사항이나 적용 기술, 프레임워크에 적합하지 않다면 패턴을 그대로 따르지 말고 목적에 맞게 패턴을 수정하라.

패턴을 사용하면서 부딪히게 되는 대부분의 문제는 패턴을 맹목적으로 사용할 때 발생한다. 대부분의 패턴 입문자가 빠지기 쉬운 함정은 패턴을 적용하는 컨텍스트의 적절성은 무시한 채 패턴의 구조에만 초점을 맞추는 것이다. 망치를 들면 모든 것이 못으로 보인다는 격언처럼 패턴을 익힌 후에는 모든 설계 문제를 패턴으로 해결하려고 시도하기 쉽다. 이를 패턴 만능주의라고 부른다.

해결하려는 문제가 아니라 패턴이 제시하는 구조를 맹목적으로 따르는 것은 불필요하게 복잡하고, 난해하며, 유지보수하기 어려운 시스템을 낳는다. 따라서 부적절한 상황에서 부적절하게 사용된 패턴으로 인해 소프트웨어의 엔트로피가 증가하는 부작용을 낳기 쉽다. 패턴을 남용하지 않기 위해서는 다양한 트레이드오프 관계속에서 패턴을 적용하고 사용해 본 경험이 필요하다.

GoF의 디자인 패턴에서 저자들은 초심자와 전문가의 차이점으로 어떤 문제를 해결하기 위해 과거의 경험을 활용할 수 있는 능력을 보유했는지 여부를 들고 이싸. 그러나 전문가와 초심자의 또 다른 차이점은 전문가는 다양한 실무 경험을 통해 어떤 컨텍스트에서 어떤 패턴을 적용해야 하는지, 그리고 이보다 더 중요한 것으로 어떤 패턴을 적용해서는 안되는지에 대한 감각을 익히고 있다는 것이다.

패턴을 처음 입문한 사람들은 패턴의 강력함에 매료된 나머지 아무리 사소한 설계라도 패턴을 적용해보려고 시도한다. 그러나 명확한 트레이드오프 없이 패턴을 남용하면 설계가 불필요하게 복잡해지게 된다.

정당한 이유 없이 사용된 모든 패턴은 설계를 복잡하게 만드는 장애물이다. 패턴은 복잡성의 가치가 단순성을 넘어설 때만 정당화되어야 한다. 패턴을 적용할 때는 항상 설계를 좀 더 단순하고 명확하게 만들 수 있는 방법이 없는지를 고민해야 한다. 또한 코드를 공유하는 모든 사람들이 적용된 패턴을 알고있어야 한다. 패터내을 알고있는 사람들은 코드를 쉽게 이해할 수 있지만 그렇지 못한 사람들은 복잡한 구조로 인해 코드를 쉽게 이해할 수 없게 된다.

조슈아 케리에브스키는 패턴을 가장 효과적으로 적용하는 방법을 패턴을 지향하거나 패턴을 목표로 리팩터링하는 것이라고 이야기 한다. 그는 패턴이 적용된 최종 결과를 이해하는 것보다는 패턴을 목표로 리팩터링하는 이유를 이해하는 것이 훨씬 가치가 있으며, 훌륭한 소프트웨어 설계가 발전해온 과정을 공부하는 것이 훌륭한 설계 자체를 공부하는 것보다 훨씬 중요하다고 조언한다.

패턴은 출발점이다. 패턴은 공통적인 문제에 적절한 해법을 제공하지만 공통적인 해법이 우리가 직면한 문제에 적합하지 않을 수 있다. 문제를 분석하고 창의력을 발휘함으로써 패턴을 현재의 문제에 적합하도록 적절하게 수정하라. 비록 패턴이 현재의 문제에 딱 들어맞지 않는다고 해도 참조할 수 있는 모범적인 역할과 책임의 집합을 알고 있는 것은 큰 도움이 될 것이다.

프레임워크와 코드 재사용

코드 재사용 VS 설계 재사용

디자인 패턴은 프로그래밍 언어에 독립적으로 재사용 가능한 설계 아이디어를 제공하는 것을 목적으로 한다. 따라서 언어에 종속적인 구현 코드를 정의하지 않기 때문에 디자인 패턴을 적용하기 위해서는 설계 아이디어를 프로그래밍 언어의 특성에 맞춰 가공해야 하고 매번 구현 코드를 재작성해야 한다는 단점이 있다.

재사용 관점에서 설계 재사용보다 더 좋은 방법은 코드 재사용이다. 오랜 시간 동안 개발자들은 부품을 조립해서 제품을 만드는 것처럼 별도의 프로그래밍 없이 기존 컴포넌트를 조립해서 애플리케이션을 구축하는 방법을 추구해왔다. 아쉽게도 컴포넌트 기반의 재사용 방법이라는 아이디어 자체는 이상적이지만 실제로 적용하는 과정에서 현실적이지 않다는 사실이 드러났다.

로버트 L. 글래스컴포넌트 기반의 재사용과 관련된 논쟁의 핵심은 소프트웨어 다양성이라고 불리는 주제와 관련이 있따고 생각했다. 그의 주장에 따르면 만약 여러 프로젝트나 도메인 사이에 비슷한 문제가 충분히 많이 존재한다면 컴포넌트 기반의 접근법이 효과가 있을 수 있겠지만 애플리케이션과 도메인의 다양성으로 인해 두 가지 문제가 아주 비슷한 경우는 거의 없다고 한다. 따라서 가장 기본이 되는 아주 적은 부분만이 일반화될 수 있을 것이다. 결국 다양한 도메인에 재사용 가능한 컴포넌트라는 개념은 비현실적이라고 할 수 있다.

가장 이상적인 형태의 재사용 방법은 설계 재사용코드 재사용을 적절한 수준으로 조합하는 것이다. 코드 재사용만을 강조하는 컴포넌트는 실패했다. 추상적인 수준에서의 설계 재사용을 강조하는 디자인 패턴은 재사용을 위해 매번 유사한 코드를 작성해야만 한다. 설계를 재사용하면서도 유사한 코드를 반복적으로 구현하는 문제를 피할 수 있는 방법은 없을까? 이 질문에 대한 객체지향 커뮤니티의 대답이 바로 프레임워크다.

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

프레임워크코드를 재사용함으로써 설계 아이디어를 재사용한다. 프레임워크는 애플리케이션의 아키텍처를 제공하며 문제 해결에 필요한 설계 결정과 이에 필요한 기반 코드를 함께 포함한다. 또한 애플리케이션을 확장할 수 있도록 부분적으로 구현된 추상 클래스와 인터페이스 집합뿐만 아니라 추가적인 작업 없이도 재사용 가능한 다양한 종류의 컴포넌트도 함께 제공한다.

프레임워크는 애플리케이션에 대한 아키텍처를 제공한다. 즉, 프레임워크는 클래스와 객체들의 분할, 전체 구조, 클래스와 객체들 간의 상호작용, 객체와 클래스 조합 방법, 제어 흐름에 대해 미리 정의한다. 프레임워크는 설계의 가변성을 미리 정의해 뒀기 때문에 애플리케이션 설계자나 구현자는 애플리케이션에 종속된 부분에 대해서만 설계하면 된다. 프레임워크는 애플리케이션 영역에 걸쳐 공통의 클래스들을 정의해서 일반적인 설계 결정을 미리 내려둔다. 비록 프레임워크가 즉시 업무에 투입할 수 있는 구체적인 서브클래스를 포함하고 있기는 하지만 프레임워크는 코드의 재사용보다는 설계 자체의 재사용을 중요시한다.

상위 정채고가 하위 정책으로 패키지 분리하기

프레임워크의 핵심은 추상 클래스나 인터페이스와 같은 ‘추상화’라고 할 수 있다. 그렇다면 추상 클래스와 인터페이스가 가지는 어떤 특성이 프레임워크의 재사용성을 향상시키는 것일까? 이 질문의 답은 일관성 있는 협력이라는 주제와 관련이 있다.

추상 클래스와 인터페이스는 일관성 있는 협력을 만드는 핵심 재료라는 것을 기억하자. 협력을 일관성있고 유연하게 만들기 위해서는 추상화를 이용해 변경을 캡슐화해야 한다. 그리고 협력을 구현하는 코드 안의 의존성은 가급적이면 추상 클래스나 인터페이스와 같은 추상화를 향하도록 작성해야 한다.

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

객체지향 이전의 구조적인 설계와 같은 전통적인 소프트웨어 개발 방법의 경우 상위 레벨 모듈이 하위 레벨 모듈에, 그리고 상위 정책이 구체적인 세부사항에 의존하도록 소프트웨어를 구성한다. 하지만 상위 정책은 상대적으로 변경에 안정적이고 세부사항은 자주 변경된다. 핸드폰 과금 시스템에서 상위 정책은 요금제가 기본 정책과 부가 정책으로 구성되고, 이 정책들이 다양한 순서로 조합될 수 있다는 점이다. 그에 비해 세부사항은 시간대별 방식, 요일별 방식, 기간별 방식과 같은 세부적인 정책의 종류다. 만약 변하지 않는 상위 정책이 자주 변하는 세부사항에 의존한다면 변경에 대한 파급 효과로 인해 상위 정책이 불안정해질 것이다.

그리고 상위 정책이 세부 사항에 비해 재사용될 가능성이 높다. 기본 정책과 부가 정책을 조합하는 규칙은 모든 요금 계산 시에 재사용돼야 하는 협력 패턴이다. 그에 비해 시간대별 방식으로 요금을 계산하거나 세금을 부과하는 것은 특수한 경우에만 사용되는 기본 정책과 부가 정책의 한 예라고 할 수 있다.

요점은 상위 정책이 세부 사항보다 더 다양한 상황에서 재사용 될 수 있어야 한다는 것이다. 하지만 상위 정책이 세부 사항에 의존하게 되면 상위 정책이 필요한 모든 경우에 세부 사항도 항상 함께 존재해야 하기 때문에 상위 정책의 재사용성이 낮아진다. 이 문제를 해결할 수 있는 가장 좋은 방법은 의존성 역전 원칙에 맞게 상위 정책과 세부 사항 모두 추상화에 의존하게 만드는 것이다.

이를 위한 첫걸음은 변하는 부분과 변하지 않는 부분을 별도의 패키지로 분리하는 것이다. 아래 그림은 상위 정책을 구현하는 패키지와 세부 사항을 구현하는 클래스들을 서로 다른 패키지로 분리한 것이다. 물론 실제 애플리케이션에서는 패키지 단위의 결합도와 응집도를 고려해서 패키지 단위를 좀 더 세분화하겠지만 여기서는 설명을 위해 단지 두 개의 패키지만 이용해 구조를 단순화했다. 실제로는 다양한 패키지들이 중첩돼 있다고 생각하기 바란다.

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

이제 상위 정책을 구현하고 있는 패키지가 세부 사항을 구현하고 있는 패키지로부터 완벽하게 분리됐다. 다시 말해 상위 정책을 구현하고 있는 패키지를 다른 애플리케이션에 재사용할 수 있는 것이다. 이것은 컨텍스트 독립성의 패키지 버전이다.

좀 더 나아가 상위 정책을 구현하고 있는 패키지가 충분히 안정적이고 성숙했따면 하위 정책 패키지로부터 완벽히 분리해서 별도의 배포 단위로 만들 수 있다. 상위 정책 패키지하위 정책 패키지를 물리적으로 완전히 분리하고 나면 상위 정책 패키지를 여러 애플리케이션에서 재사용할 수 있는 기반이 마련된 것이다. 다시 말해 재사용 가능한 요금 계산 로직을 구현한 프레임워크가 만들어진 것이다.

이 예제를 통해 일관성 있는 협력프레임워크 사이의 관계를 이해했을 것이다. 프레임워크는 여러 애플리케이션에 걸쳐 일관성 있는 협력을 구현할 수 있게 해준다. 그리고 일관성 있는 협력이 제공하는 다양한 장점들은 프레임워크에 대해서도 여전히 적용된다. 우리는 동일한 프레임워크를 사용하는 여러 애플리케이션에 걸쳐 일관성 있게 코드를 설계하고 구현할 수 있다. 동일한 프레임워크를 사용하는 애플리케이션은 구현 방식에 일관성이 있기 때문에 이해하기도 쉽다. 추가적으로 설계와 함께 코드 역시 재사용할 수 있다.

제어 역전 원리

상위 정책을 재사용한다는 것은 결국 도메인에 존재하는 핵심 개념들 사이의 ‘협력 관계를 재사용’한다는 것을 의미한다. 객체지향 설계의 재사용성개별 클래스가 아니라 객체들 사이의 공통적인 협력 흐름으로부터 나온다. 그리고 그 뒤에는 항상 의존성 역전 원리라는 강력한 지원군이 존재한다. 의존성 역전 원리는 전통적인 설계 방법과 객체지향을 구분하는 가장 핵심적인 원리다. 의존성 역전 원리에 따라 구축되지 않은 시스템은 협력 흐름을 재사용할 수도 없으며 변경에 유연하게 대처할 수도 없다.

시스템이 진화하는 방향에는 항상 의존성 역전 원리를 따르는 설계가 존재해야 한다. 만약 요구사항이 빠르게 진화하는 코드에서 의존성 역전 원리가 지켜지지 않고 있다면 그곳에는 변경을 적절하게 수용할 수 없는 하향식의 절차적인 코드가 존재할 수밖에 없다.

로버트 마틴은 훌륭한 객체지향 설계는 의존성이 역전된 설계라는 점을 강조한다.

사실, 좋은 객체지향 설계의 증명이 바로 의존성 역전이다. 프로그램이 어떤 언어로 작성됐는가는 상관없다. 프로그램의 의존성이 역전돼 있따면, 이것은 객체지향 설계를 갖는 것이다. 그 의존성이 역전돼 있지 않다면, 절자척인 설계를 갖는 것이다. 의존성 역전의 원칙은 객체지향 기술에서 당연하게 요구되는 많은 이점 뒤에 있는 하위 수준에서의 기본 메커니즘이다. 재사용 가능한 프레임워크를 만들기 위해서는 이것의 적절한 응용이 필수적이다. 이 원칙은 또한 변경에 탄력적인 코드를 작성하는 데 있어 결정적으로 중요하다. 추상화와 구체적인 사항이 서로 고립돼 있기 때문에 이 코드는 유지보수하기가 훨씬 쉽다.

의존성 역전 원리프레임워크의 가장 기본적인 설계 메커니즘이다. 의존성 역전의 의존성의 방향 뿐 아니라 제어 흐름의 주체 역시 역전시킨다. 앞서 설명한 것처럼 상위 정책이 구체적인 세부사항에 의존하는 전통적인 구조에서는 상위 정책의 코드가 하부의 구체적인 코드를 호출한다. 즉, 애플리케이션의 코드가 재사용 가능한 라이브러리나 툴킷의 코드를 호출한다.

그러나 의존성을 역전시킨 객체지향 구조에서는 반대로 프레임워크가 애플리케이션에 속하는 서브 클래스의 메서드를 호출한다. 따라서 프레임워크를 사용할 경우 개별 애플리케이션에서 프레임워크로 제어 흐름의 주체가 이동한다. 즉, 의존성을 역전시키면 제어 흐름의 주체 역시 역전된다. 이를 제어 역전 원리, 또는 할리우드 원리라고 한다.

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

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

여기서 협력을 제어하는 것은 프레임워크라는 것에 주목하라ㅏ. 우리는 프레임워크가 적절한 시점에 실행할 것으로 예상되는 코드를 작성할 뿐이다. 과거의 좋았던 시절에는 우리가 직접 라이브러리의 코드를 호출했지만 객체지향의 시대에는 그저 프레임워크가 호출하는 코드를 작성해야만 한다. 제어가 우리에게서 프레임워크로 넘어간 것이다. 다시 말해서 제어가 역전된 것이다.

우리의 코드는 수동적인 존재다. 프레임워크가 우리의 코드를 호출해줄 때까지 그저 넋 놓고 기다리고 있을 수밖에 없다. 할리우드에서 캐스팅 담당자가 오디션을 보러 온 배우에게 먼저 연락하지 마세요 저희가 연락 드리겠습니다.라고 말하는 것처럼 프레임워크는 자신을 찾지 말라고 이야기 한다.

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

만약 프레임워크를 처음 사용한다면 제어 흐름이 손가락 사이로 스멀스멀 빠져너가는 듯한 느낌에 불안해질 수도 있다. 그러나 이러한 제어의 역전이 프레임워크의 핵심 개념인 동시에 코드의 재사용을 가능하게 하는 힘이라는 사실을 이해해야 한다.

참고 및 출처

  • 오브젝트