Java Design Patterns - 자바 디자인 패턴

개요

이번 포스트에서는 자바 디자인 패턴 [^1]에 대해서 학습해보기로 한다. GoF의 디자인패턴의 서론부분을 요약한 것이며, 앞으로는 하나하나의 패턴들을 분석하여 정리해보려 한다.

Java Design Pattern

  • STEP 1. 디자인 패턴이란?
  • STEP 2. 디자인 패턴의 분류법
  • STEP 3. 디자인 패턴을 이용하여 문제를 푸는 법
  • STEP 4. 디자인 패턴을 고르는 방법

STEP 1. 디자인 패턴이란?

본 챕터에서는 디자인 패턴의 바이블로 불리우는 “GoF의 디자인 패턴”의 책 서론을 요약하여 다룬다.

먼저, 디자인 패턴이란? 어떤 상황의 문제에 대한 해법이다.

건축가이자 패턴의 아버지, 크리스토퍼 알렉산더는 디자인 패턴을 이렇게 이야기했다.

각 디자인 패턴은 기존 환경 내에서 반복적으로 일어나는 문제들을 설명한 후, 그 문제들에 대한 해법의 핵심을 설명해 줍니다. 똑같은 방법으로 두 번 하지 않고, 이 해법을 100만 번 이상 재사용할 수 있도록 말이죠.

이 부분은 소프트웨어 개발에서도 동일하다. 건축과 달리 객체와 인터페이스를 사용한다는 것만 차이일뿐이다.

일반적으로, 패턴에는 다음의 4가지 요소가 반드시 들어가있다.

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

책에 서술된 바로는 또 다시 디자인패턴을 나름대로 정의를 하였는데 바로, ”특정한 전후 관계에서 일반적인 설계 문제를 해결하기 위해 상호교류하는 수정 가능한 객체와 클래스들에 대한 설명”이라 정의한다.

STEP 2. 디자인패턴의 분류법

GoF의 디자인패턴에는 23가지의 디자인 패턴들이 존재하며, 이것을 디자인 패턴 카탈로그(Design Pattern Catalogue)라고 명명하고 있다.

따라서 23가지의 다양한 패턴들은 저마다 추상화 수준이 천차만별이므로 조직화를 시켜서 디자인 패턴을 분류하였다.

디자인 패턴을 분류하는 방법 아래와 같은 2가지 방법으로 분류된다.

  1. 목적 (Purpose)
  2. 범위 (Scope)

첫번째 목적은 디자인 패턴은 생성(Creational), 구조(Structural), 행동(Behavioral) 중의 한 가지의 목적을 갖는다는 전제하로 분류가 된다. 두번째 범위는 패턴을 적용하는 대상이 클래스인지 객체인지를 구분하는 것이다.

즉, 생성패턴에도 클래스 생성패턴과 객체 생성패턴이 있다는 점이다. 하지만 두 패턴의 목적은 동일하다.

생성패턴의 목적은 객체의 생성 과정에 관여하는 것이다. 생성 “클래스” 패턴은 객체를 생성하는 책임의 일부를 서브클래스가 담당하도록 넘기는 것이며, 생성 “객체” 패턴은 이를 다른 객체에 위임하는 것이다.

구조패턴의 목적은 클래스나 객체의 합성이다. 구조 “클래스” 패턴은 상속을 이용해서 클래스를 복합하며, 구조 “객체” 패턴은 객체를 합성하는 방법을 정의한다.

행동패턴의 목적은 클래스나 객체들이 상호작용하는 방법과 책임의 분산이다. 행동 “클래스” 패턴은 상속을 이용해서 알고리즘과 제어 흐름을 기술하며, 행동 “객체” 패턴은 하나의 작업을 수행하기 위해 객체 집합이 어떻게 협력하는지를 기술한다.

분류된 디자인 패턴들을 표로 정리를 해보면 아래와 같이 볼 수가 있다.

  생성 구조 행동  
클래스 팩토리 메소드(Factory Method) 적응자(Adapter) 해석자(Interpreter)  
      템플릿 메소드(Template Mehtod)  
객체 추상 팩토리(Abstract Factory) 적응자(Adapter) 책임 연쇄(Chain of Responsibility)  
  빌더(Builder) 가교(Bridge) 명령(Command)  
  원형(Prototype) 복합체(Composite) 해석자(Interpreter)  
  단일체(Singleton) 장식자(Decorator) 중재자(Mediator)  
    퍼사드(Facade) 메멘토(Memento)  
    플라이급(Flyweight) 감시자(Observer)  
    프록시(Proxy) 상태(State)  
      전략(Strategy)  
      방문자(Visitor)  

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

“GoF의 디자인패턴”에서는 아래와 같이 7가지 문제에 대하여 디자인패턴을 이용하여 푸는 방법을 제시한다.

  1. 적당한 객체 찾기
  2. 객체의 크기 결정
  3. 객체 인터페이스의 명세
  4. 객체 구현 명세하기
  5. 재사용을 실현 가능한 것으로
  6. 런타임 및 컴파일 타임의 구조를 관계 짓기
  7. 변화에 대비한 설계

1. 적당한 객체 찾기

객체는 데이터와 이 데이터에 연산을 하는 프로시저(procerure)를 함께 묶은 단위이다. 즉, 객체 = 데이터 + 프로시저라 볼 수가 있다. 프로시저를 일반적으로 메소드(method) 또는 연산(operation) 이라 한다. 객체는 요청(request) 혹은 메시지(message) 를 사용자에게 받으면 연산을 수행한다.

요청객체가 연산을 실행하게 하는 유일한 방법이다. 연산객체의 내부 데이터의 상태를 변경하는 유일한 방법이다.

이러한 접근의 제약 사항으로 객체의 내부의 상태는 캡슐화(Encapsulate) 된다고 한다. 즉, 객체 외부에서는 객체의 내부 데이터에 직접 접근할 수가 없고, 객체의 내부 데이터 표현 방법(데이터 타입 등)을 알 수 없다.

객체지향 설계에서 가장 어려운 부분은 시스템을 구성할 객체의 분할을 결정하는 것이다. 이러한 분할을 결정하는데 객체지향 설계 방법론들은 서로 다른 방법으로 접근한다. 이를테면, 실세계의 모델링을 통해서 설계하는 방법이나 문제 기술서를 작성 후 동사와 명사를 추출하여 바꾸는 방법 등이 있다.

그러나, 객체지향 설계는 실세계와 대응 관계를 갖지 못할 때가 많다. 객체의 대부분은 분석 모델에서 만들어진 것인데 분석 모델의 객체는 실세계들의 객체지만, 설계 모델의 객체에서는 배열, 리스트처럼 구현에 가까운 클래스들도 존재한다. 또한, 실세계를 그대로 반영하는 모델링만 강조하면 현재의 실세계는 반영하지만 미래의 실세계를 반영하지 못하는 경우가 존재하므로 설계 단계 동안 만들어야 하는 새로운 추상화는 설계의 유연성을 증진하기 위해 중요하다.

디자인 패턴은 덜 명확한 추상적 개념과 이것을 잡아낸 객체를 알아보는데 도움을 준다. 예를 들면, 전략 패턴은 상호교환이 가능한 알고리즘군을 어떻게 구현할지 설명하고, 상태 패턴은 대상들의 각 상태를 객체로 표현한다. 이 상태 객체는 분석 및 설계 단계에서 찾기 힘들다.

2. 객체의 크기 결정

우리가 응용프로그램을 만든다할 때 하드웨어 하나하나를 객체를 모두 표현할 수도, 응용프로그램 전체를 하나의 객체를 만들 수도 있다.

그렇다면 적당한 객체의 규모는 어떻게 결정하는가? 이러한 문제도 디자인패턴에서 답을 얻을 수가 있다.

예를 들면, 퍼사드 패턴은 서브시스템을 어떻게 객체로 표현할지 설명하며, 플라이급 패턴은 규모는 작지만 개수는 많은 객체를 다루는 방법을 설명한다.

3. 객체 인터페이스의 명세

객체가 선언한 모드 연산은 연산의 이름, 매개변수로 받아들이는 객체들, 연산의 반환 값을 명세하며 이것을 연산의 시그니처(Signature) 라 한다.

인터페이스는 객체가 정의하는 연산의 모든 시그니처들을 일컫는 말로 객체의 인터페이스는 객체가 받아서 처리할 수 있는 연산의 집합이다.

타입(Type) 은 특정 인터페이스를 나타낼 때 사용하는 이름이다. 객체가 “Math” 타입을 갖는다는 것은 “Math”인터페이스에 정의한 연산들을 모두 처리할 수 있다는 것을 의미한다.

인터페이스가 다른 인터페이스를 부분집합으로 포함할 때가 있는데 포함되는 인터페이스를 서브타입(Subtype) 이라 하며, 포함하는 인터페이스를 슈퍼타입(Supertype) 이라한다.

객체는 인터페이스로 자신을 드러내며, 외부에서 객체를 알 수 있는 방법은 인터페이스 밖에 없기때문에 인터페이스를 통해서만 처리를 요청한다. 즉, 기능을 사용할 수 있는 통로가 되는 것이다.

객체의 인터페이스는 구현에 대한 정보는 알려주지 않는다. 따라서, 서로 다른 객체는 인터페이스에 정의한 요청의 구현 방법을 자유롭게 선택이 가능한데 이를 통해서 동일한 인터페이스를 갖는 두 객체라도 완전히 다른 구현을 갖는다는 뜻이다.

이러한 특성으로 동일한 요청이라도 처리하는 객체들이 다르면, 구현을 어떻게했는지에 따라서 다른 결과가 나온다. 이러한 것을 런타임(프로그램 실행 중에)에 연결 짓는 것을 동적 바인딩(Dynamic Binding) 이라 한다.

이러한 동적 바인딩을 통해서 프로그램이 기대하는 객체를 동일한 인터페이스를 갖는 다른 객체로 대체할 수 있게하는데 이것을 바로 다형성(Polymorphism) 이라 한다.

객체지향 시스템은 다형성을 통하여 객체 간의 결합도를 없애며, 프로그램 실행 중에는 서로 간의 관련성을 다양화할 수 있게 해준다.

디자인 패턴은 인터페이스에 정의해야 하는 중요 요소가 무엇이고, 어떤 종류의 데이터르 주고 받아야하는지 식별하여 인터페이스를 정의하도록 도와준다. 예를 들면, 메멘토 패턴은 객체의 내부 상태를 어떻게 저장하고 캡슐화해야 하는지 정의함으로써 객체가 나중에 그 상태로 복구할 수 있는 방법을 알려주며, 이 패턴에서는 객체에 두 개의 인터페이스를 정의하도록 규정한다. 또한, 장식자 패턴이나 프록시 패턴은 인터페이스 간의 관련성을 정의하는데 장식되고 중재되는 객체와 동일한 인터페이스를 갖도록 장식자 객체와 프록시 객체의 인터페이스를 요청한다.

4. 객체 구현 명세하기

UML을 이용하여 클래스와 객체에 대한 정보를 명세한다. 이 부분은 대부분 UML에 관련된 내용이였으므로 클래스 다이어그램에 대한 구체적인 설명이 있는 UML: 클래스 다이어그램과 소스코드 매핑로 대체하겠다.

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

이 챕터에서는 디자인 패턴이 이전 장에서 배운 개념을 이용하여 유연하고 재사용 가능한 소프트웨어 개발에 적용하는지를 파악할 수가 있다.

“GoF의 디자인패턴”에서는 아래의 내용을 포함하고 있다.

  1. 상속 대 합성
  2. 위임

1. 상속 대 합성 객체지향 시스템에서 기능의 재사용을 위해 구사되는 가장 대표적인 기법은 클래스 상속, 객체 합성이다.

클래스 상속은 다른 부모 클래스에서 상속받아 한 클래스의 구현을 정의하는 것으로 서브클래싱에 의한 재사용을 화이트박스 재사용(white-box reuse)라 한다. 화이트박스라는 말이 쓰인 것은 부모 클래스의 내부가 서브클래스에 공개 되기때문에 내부를 볼 수 있다는 의미에서 사용되었다.

객체 합성은 클래스 상속에 대한 대안이다. 다른 객체를 여러 개 붙여서 새로운 기능 혹은 객체를 구성하는 것으로써 이런 스타일의 재사용을 블랙박스 재사용(black-box reuse)라고 한다. 객체의 내부는 공개되지 않고 인터페이스를 통해서만 재사용되기 때문이다.

상속과 합성 비교

상속

장점

컴파일 시점에 정적으로 정의되고, 프로그래밍 언어가 직접 지원하므로 그대로 사용하면 된다.

단점

  1. 런타임에 상속받는 부모 클래스의 구현 변경 불가능. (상속이 컴파일 시점에 적용되므로)
  2. 부모가 서브클래스의 물리적 표현의 최소 부분만을 정의 -> 서브클래스는 정의된 물리적 표현을 전부 또는 일부 상속받는다. (서브클래스는 부모 클래스의 구현에 종속될 수밖에 없으므로, 부모 클래스 구현에 변경이 생기면 서브클래스도 변경해야된다.) -> 해결법 : 추상클래스 사용

객체 합성

장점 : 한 객체가 다른 객체에 대한 참조자를 얻는 방식으로 런타임에 동적으로 정의된다. 따라서, 객체는 인터페이스에 맞춰 구현되므로 구현 사이의 종속성이 낮아진다.

객체 합성을 통하면, 각 클래스의 캡슐화를 유지할 수 있고, 각 클래스는 한 가지 작업에 집중할 수 있다. 따라서 객체 합성이 클래스 합성보다 더 나은 방법이다. 라고 책에서는 말한다.

단점 : 가능한 구성요소의 집합이 실제로 사용할 수 있을 만큼 충분하지 않기 때문에, 기존 구성요소의 조합을 통한 재사용만으로 목적을 달성할 수 있는 경우는 드물다.

2. 위임(Delegation)

두 객체가 하나의 요청을 처리한다. 자세히 설명하면, 수신 객체가 연산의 처리를 위임자(Delegate)에게 보낸다.

더 자세히 설명하면, 어떤 기능을 구현할 때 그 책임을 다른 객체로 떠넘기거나 그 객체의 기능을 빌려 사용하는 것을 말하며, 특정 객체의 기능을 사용하기 위해 다른 객체의 기능을 호출하는 것이다.

이 부분은 나중에 전략패턴을 다루면서 자세히 다뤄보도록 하겠다.


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

객체의 관계 중에는 집합(Aggregation)과 인지관계(Acquaintance) 가 있다.

집합은 한 객체가 다른 객체를 소유하거나 그것에 책임을 진다는 뜻이다. 인지는 한 객체가 다른 객체에 대해 알고 있음을 의미한다. 이를 연관(Association) 혹은 사용(using) 관계라고도 한다.

인지 관계는 집합 관계보다 관련성이 약해서 객체들 사이의 결합도가 약하다.

인지 관계와 집합 관계는 언어의 처리 방식이 아닌 사용 목적에 따라 결정해야 한다. 이러한 차이를 컴파일 시점에서 발견하기는 힘들지만 중요한 의미를 갖는데 집합 관계는 인지 관계와 비교하여 강력한 영속성의 개념을 갖는다. 즉, 자동차에 바퀴가 있다는 것은 영속적 사실이라는 점과 같다. 반면, 인지 관계는 자주 바뀌는데 사람과 회사의 관계를 보면 근무한다는 관련성이 있을 수도 혹은 없을 수도 있다.

따라서 소스 코드에서 그것을 쉽게 식별하기 어려우므로, 설계자가 런타임 구조를 만들 때 객체와 타입 사이의 관계를 세심하게 설계해야한다. 복합체 패턴이나 장식자 패턴은 복잡한 실행 구조를 구축하는 데 유용하며, 감시자 패턴은 런타임 구조를 만드는데 사용한다.

7. 변화에 대비한 설계

변화에 잘 대응하기 위한 소프트웨어를 설계하기 위해서는 소프트웨어를 운영하는 동안 앞으로 일어날 변화를 어떻게 수용할 것인가를 미리 고려해야 한다.

디자인 패턴은 어떤 구체적인 원인으로 앞으로 시스템을 변경해야 한다는 것을 미리 보여줌으로써 이런 수고를 덜어준다.

아래는 디자인 패턴을 써서 재설계를 할 수밖에 없게 하는 이유이다.

  1. 특정 클래스에서 객체 생성
  2. 특정 연산에 대한 의존성
  3. 하드웨어와 소프트웨어 플랫폼에 대한 의존성
  4. 객체의 표현이나 구현에 대한 의존성
  5. 알고리즘 의존성
  6. 높은 결합도
  7. 서브클래싱을 통한 기능 확장
  8. 클래스 변경이 편하지 못한 점

특정 클래스에서 객체 생성 : 객체를 생성할 때 클래스 이름을 명시하면 어떤 특정 인터페이스가 아닌 특정 구현에 종속이 된다.

상위 문제를 해결 할 수 있는 디자인 패턴 : 추상 팩토리, 팩토리 메소드, 원형

특정 연산에 대한 의존성 : 특정한 연산을 사용하면, 요청을 만족하는 한 가지 방법에만 종속된다.

상위 문제를 해결 할 수 있는 디자인 패턴 : 책임 연쇄, 명령

하드웨어와 소프트웨어 플랫폼에 대한 의존성 : 특정 플랫폼에 종속된 소프트웨어는 다른 플랫폼에 이식하기가 어려울뿐아니라 본래 플랫폼에서의 버전 변경이 까다롭다.

상위 문제를 해결 할 수 있는 디자인 패턴 : 추상 팩토리, 가교

객체의 표현이나 구현에 대한 의존성 : 사용자가 객체의 표현법, 저장법, 구현법 등… 을 알고 있으면, 객체를 변경할 때 사용자도 함께 변경해야되므로 정보은닉이 필요하다.

상위 문제를 해결 할 수 있는 디자인 패턴 : 추상 팩토리, 가교, 메멘토, 프록시

알고리즘 의존성 : 알고리즘에 종속된 객체라면 알고리즘이 변할 때마다 객체도 변경해야한다.

상위 문제를 해결 할 수 있는 디자인 패턴 : 빌더, 반복자, 전략, 템플릿 메소드, 방문자

높은 결합도 : 높은 결합도를 갖는 클래스들은 독립적으로 재사용하기 어렵다.

상위 문제를 해결 할 수 있는 디자인 패턴 : 추상 팩토리, 가교, 책임 연쇄, 명령, 퍼사드, 중재자, 감시자

서브클래싱을 통한 기능 확장 : 서브클래싱으로 객체를 재정의를 하면, 새로운 클래스마다 매번 초기화, 소멸 등에 대한 구현 오버헤드가 증가하게 된다. 또한, 서브클래스를 정의하려면 최상위 클래스부터 자신의 직계 부모클래스까지에 대한 정보를 모두 이해하고 있어야된다. 단순히 확장만으로 서브클래스를 만든다면, 서브클래싱은 클래스의 수를 엄청나게 증가시킨다.

상위 문제를 해결 할 수 있는 디자인 패턴 : 가교, 책임 연쇄, 장식자, 감시자, 전략

클래스 변경이 편하지 못한 점 : 어떤 변경을 하면, 기존 서브클래스의 다수를 수정하거나 소스 코드가 필요한데 없다고 가정하면 클래스를 변경하는 작업이 어려움을 알 수가 있다.

상위 문제를 해결 할 수 있는 디자인 패턴 : 적응자, 장식자, 방문자

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

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

결론

서론만 정리했을뿐인데 양이 어마어마하게 많이나온다.

다음 장부터는 GoF책에 패턴 카탈로그에 있는 패턴들을 하나하나씩 분석하면서 공부를 해보려 한다.

REFERENCE

  1. GoF의 디자인 패턴 개정판

[^1] : Software design pattern - Wikipedia

댓글남기기