GOF의 디자인 패턴 1장, 서론의 두번째를 정리해봅니다.
카탈로그 조직화 하기
영역을 기준으로
- 앞에서 말한 디자인 패턴들은 추상화 수준과 배움의 깊이가 다름
- 그렇기에 목적과 범위를 기준으로 조직화함
- 목적
- 생성: 객체들의 생성과정에 관여
- 구조: 클래스나 객체의 합성에 관한 패턴
- 행동: 클래스나 객체들의 상호작용 방법과 책임 분산 방법
- 범위
- 클래스: 클래스와 서브클래스 간의 관련성을 다루는 패턴
- 객체: 객체 관련성을 다루는 패턴
- 런타임에 변경할 수 있음
- 조금더 동적인 성격을 지님
- 대부분은 상속을 이용함
복합적으로 얽힐 경우 어떤 식으로 이해할 수 있는 조금더 구체적인 구분에 대해 알아보자.
- 생성 클래스 패턴
- 생성 객체 패턴
- 구조 클래스 패턴
- 구조 객체 패턴
- 행동 클래스 패턴
- 행동 객체 패턴
- 하나의 작업을 수행하기 위해 객체 집합이 어떻게 협력하는지 기술
패턴간 참조관계를 기준으로
- 이 그림을 아직 이해할 수는 없다.
- 다만 위 그림은 패턴 간의 참조 관계를 기준으로 나타낸 것이다.
- 나중에 알 수 있겠지
디자인 패턴을 이용하여 문제를 푸는 방법
- 디자인 패턴을 사용하면 어떤 문제를 어떻게 해결하기를 지향하는 것일까?
적당한 객체 찾기
기본 용어
- 객체: data + procedure
- procedure: method or operation
- 객체는 요청 혹은 message을 받으면 연산을 수행한다.
- 요청은 객체가 연산을 실행하게 하는 유일한 방법이다.
- 연산은 객체의 내부 데이터의 상태를 변경하는 유일한 방법이다.
- 이 두개의 제약을 적용한 것을 캡슐화되어 있다고 말한다.
객체지향 설계가 어려운 이유와 방법들
- 객체 지향 설계에 있어 가장 어려운 부분은 시스템을 구성한 객체의 분할을 결정하는 일이다.
- 캡슐화, 크기, 종속성, 유연성, 성능, 진화, 재사용성 등을 고려해야 한다.
- 그리고 이것들을 고려하기 위한 판단은 모두 주관적이기 때문에 더더욱 어렵다.
- 예를 들어, 시스템의 협력관계, 책임성을 중심으로 설계하거나, 실세계를 모방한 모델로 만들거나 할 수 있다. 하지만 가장 좋은 방법은 없다.
- 객체 지향 설계는 실세계와 대응 관계를 갖지 못할 때가 많다.
- 흔히 실세계를 모방하는 방법이라 하지만, 저수준으로 갔을 때는 배열, 리스트와 같은 구현에 직접적으로 연관되는 녀석들로 되어 있는 경우가 많다.
- 실세계를 그대로 반영하게 되면 현재의 실세계는 반영가능하나 미래는 불가능하다.
- 이러한 점을 기반으로 우리는 설계 단계 동안에 새로운 추상화된 객체나 인터페이스를 만들어야 한다.
- 이는 결국 설계의 유연성을 증진하기 위한 중요한 노력중 하나다.
- 요약하면, 실세계를 그대로 모방하는 방식으로 OOP를 설계하기 어렵다는 것이다.
- 그 과정에는 설계의 유연성, 객체의 크기등 다양한 변수 때문에 새로운 추상화를 만들어 관리할 필요성이 있기 때문이다.
- 그렇기 때문에 OOP를 단순히 실세계를 모방하기 위한 방법론이라 칭하는 것은 일부만 이해하고 있는것이라 생각할 수 있겠다.
객체의 크기 결정
- 객체의 규모는 어떤 기준으로 정할 수 있을까?
- 디자인 패턴에서는 이 문제에 대한 답을 제시한다.
- 퍼사드: 서브시스템 객체로 표현하는 방법
- 플라이급: 규모는 작으나 개수는 많은 객체를 다루는 방법
- 객체를 작은 규모로 분할하는 방법 등이 있다.
객체 인터페이스의 명세
- Signiture: 연산의 이름, 매개변수로 받는 객체들, 반환 값
- Interface: 객체가 정의하는 연산의 모든 시그니처를 말함
- 즉, 객체가 받아서 처리할 수 있는 연산의 집합을 말함
- Type: 특정 인터페이스를 지칭하는 이름
- SuperType: 다른 인터페이스가 포함하는 인터페이스
- SubType: 다른 인터페이스를 포함하는 인터페이스
- 당연하게도 SuperType으로 SubType들을 관리할 수 있음
- SubType에 구현에 따라 다른 결과가 나올 수 있다.
- 즉, 어떤 요청과 그 요청을 처리할 객체를 런타임에 연결짓는 것을 동적 바인딩이라 한다.
- 이러한 동적 바인딩은 동일한 인터페이스를 가진 다른 객체로 대체할 수 있도록 한다.
- 이러한 성질을 다형성이라 한다.
- 이는 객체지향 시스템의 핵심 개념이다.
- 사용자 정의를 단순화
- 객체간 결합도를 없앰
- 런타임에 관련성을 다양하게 만들어줌
- 디자인 패턴은 인터페이스에 정의해야 하는 요소, 데이터 등을 기반으로 인터페이스 작성을 도와준다.
- 또한 인터페이스간의 관련성도 정의한다.
객체 구현 명세하기
- 그럼 객체는 어떻게 정의해야 할까?
- 그 구현은 class에서 수행한다.
- 클래스 이름
- Line
- 연산 이름
- Line
- 데이터
- 왼쪽 객체가 오른쪽 객체를 생성함
- 점선 채운 삼각형
- 추상 클래스: 모든 서브 클래스 사이의 공통되는 인터페이스를 정의
- 정의한 모든 연산이나 일부 연산의 구현을 서브클래스로 넘김
- 그렇기 때문에 추상 클래스는 인스턴스화 할 수 없다. 모든 연산이 정의되지 않았기 때문
- 이렇게 정의만하고 구현을 하지 않은 연산을 추상 연산이라 한다.
- 그리고 이런 추상 클래스가 아닌 클래스는 구체 클래스라 한다.
- 추상 클래스, 추상 연산의 이름은 italic으로 표기한다.
- Mixed In 클래스: 다른 클래스들에게 선택적인 인터페이스 혹은 기능을 제공하려는 목적을 가진 클래스
- 다중 상속을 사용해야만 한다.
- 왼쪽은 구체 클래스를 상속하고, 오른쪽은 추상 클래스를 상속받았다.
- 그렇기 때문에
MixinOperation()
의 경우에는 필수적으로 구현해야 한다.
- 하지만 다중 상속은 죽음의 다이아몬드와 같은 문제가 있기 때문에, 요즘언어에서는 지원하지 않는다.
클래스 상속 대 인터페이스 상속
- 클래스 상속
- 인터페이스 상속
- 특정 객체가 다른 객체 대신에 사용될 수 있는 경우를 대비하여 만들어짐
구현에 따르지 않고, 인터페이스에 따르는 프로그래밍
구현이 아닌 인터페이스에 따라 프로그래밍하세요.
- 클래스 상속은 부모 클래스의 정의한 구현을 재사용하여 확장하려는 메커니즘이다.
- 즉, 구현의 재사용을 염두에 두었다고 할 수 있다.
- 하지만, 이 뿐만 아니라 다형성을 끌어낼 수 있어야 한다.
- 그렇기 때문에 어떤 변수를 구체 클래스의 인스턴스로 선언하는 일은 피해야 한다.
- 물론 상위 클래스를 상속한 하위 클래스의 경우 상위 클래스로 선언했을 때 다형성으로 동작하기는 한다.
- 하지만 인터페이스 개념으로 다루게 된다면..
- 인터페이스만 만족하면 사용하면 된다. 구체 타입을 알 필요가 없다.
재사용을 실현 가능 한 것으로
객체 합성을 사용하세요.
상속 대 합성
- 기능의 재사용을 위해 구사하는 대표적인 방법은 클래스 상속, 객체 합성이다.
- Subclassing: 클래스 상속 - White box reuse
- Composition: 객체 합성 - Black box reuse
클래스 상속
- 장점
- 컴파일 시점에 정의됨
- 부모 구현을 쉽게 수정가능함
- 또는 일부만 재정의함
- 단점
- 런타임 상속 클래스 구현 변경 불가 (컴파일 시점에 결정되니까)
- 부모 클래스의 구현에 종속적임
- 부모 클래스의 구현이 다 드러나 캡슐화가 깨진다는 의견도 있음
- 이런 구현의 종속성은 서브 클래스 재사용시 문제가 생김
- 새 문제에 맞지 않는다면, 부모 클래스를 재작성해야 함
- 위의 문제에서 벗어나기 위해서는 추상 클래스에서만 상속받는 것이 있겠다.
- 그러면 서브 클래스에서만 구현을 바꾸면 된다.
객체 합성
- 장점
- 인터 페이스만 바라보게 할 수 있어 캡슐화를 유지할 수 있다.
- 대체 역시 가능하다.
- 객체는 인터페이스에 맞춰 구현되어 구현사이 종속성이 줄어든다.
- 단점
위임
- 런타임에 행동의 복합을 가능하게 한다.
- 실선 화살표는 Window가 rectangle instance에 대한 참조를 가지고 있음을 보여준다.
area()
연산을 위임하여 처리했다.
- 단점은, 구조가 이해하기 어려울 수 있다는 점이다.
- 이는 런타임 객체에 따라 그 결과가 다르기 때문이다.
상속 대 매개변수화된 타입
- Generic을 말한다.
- 매개변수로 타입을 정의하지 않는다.
런타임 및 컴파일 타임의 구조를 관계짓기
- 객체지향 프로그램의 실행구조는 코드 구조와 일치하지 않는 경우가 있다.
- 코드 구조는 컴파일 시점에 확정되지만, 런타임에서는 교류하는 객체들에 의해 구조가 달라질 수 있다.
- 객체 관계에서는 집합과 인지라는 것이 있다.
- 집합: 한 객체가 다른 객체를 소유하거나 책임을 짐
- 인지: 한 객체가 다른 객체에 대해 알고 있음
- 왼쪽이 오른쪽을 포함, 오른쪽은 왼쪽을 인지
- 이 두개는 사실 구현상으로 보았을 때 구분하기가 까다로울 수 있다. 결국 상호참조니까
- 그래서 이러한 구조는 사용 목적에 따라 결정해야 한다.
- 집합의 경우, 강력한 영속성을 띈다. 즉 하위 요소가 무조건적으로 포함된다는 의미다.
- 인지의 경우 변경이 잦다. 좀더 동적이다.
변화에 대비한 설계
- 변화에 대비하여 설계하기 위해서는 앞으로 일어날 변화에 대해 생각해보아야 한다.
- 다음과 같은 상황에서 디자인 패턴을 통해 재설계가 필요하게 된다.
- 객체 생성
- 클래스 이름을 명시해서 생성하면 대체가 불가능해진다.
- 특정 구현에 종속적이게 된다.
- 앞으로 구현에 얽매인다.
- 디자인 패턴: 추상 팩토리, 팩토리 메서드, 원형
- 특정 연산에 대한 의존성
- 특정한 연산을 사용하면 요청을 만족하는 한가지 방법에만 매이게 된다.
- 디자인 패턴: 책임 연쇄, 명령
- 하드웨어와 소프트퉤어 플랫폼에 대한 의존성
- 특정 플랫폼에 종속된 소프트웨어는 다른 플랫폼에 이식하기 어렵다.
- 디자인 패턴: 추상 팩토리, 가교
- 객체의 표현이나 구현에 대한 의존성
- 사용자에게 객체 표현 방법, 저장 방법, 구현 방법 등의 정보를 감추어 변화의 정도를 제어한다.
- 디자인 패턴: 추상 팩토리, 가교, 메멘토, 프록시
- 알고리즘 의존성
- 알고리즘 자체를 확장, 대체, 최적화할 수 있는 것에 대응해야 한다.
- 디자인 패턴: 빌더, 반복자, 전략, 템플릿 메서드, 방문자
- 높은 결합도
- 높은 결합도를 가지면 독립적으로 재사용하기 어렵다.
- 하나의 거대한 시스템이 되어버린다.
- 디자인 패턴: 추상 팩토리, 가교, 책임 연쇄, 명령, 퍼사드, 중재자, 감시자
- 서브클래싱을 통한 기능 확장
- 단순히 확장만을 이유로 새로운 서브클래스를 만들면 클래스 수를 엄청나게 증가시킬 수 있다.
- 또, 서브 클래싱하는 클래스마다 매번 해야하는 초기화, 소멸에 대한 구현 오버헤드도 있다.
- 객체 합성과 위임을 사용하여 유연하게 처리하자.
- 다만 객체 합성을 많이 사용하면 이해하기가 어려워진다.
- 디자인 패턴: 가교, 책임 연쇄, 장식자, 감시자, 전략
- 클래스 변경이 편하지 못한 점
디자인 패턴을 고르는 방법
- 패턴이 어떻게 문제를 해결하는지 파악하자.
- 패턴의 의도를 보자.
- 패턴들 간의 관련성을 파악하자.
- 비슷한 목적의 패턴들을 모아서 공부합시다.
- 재설계의 원인을 파악하자.
- 설계에서 가변성을 가져야 하는 부분이 무엇인지 파악하자.
디자인 패턴 사용 방법
- 전체를 훑는 기분으로 한번 읽는다.
- 다시 처음으로 가서 구조, 참여자, 협력 방법을 다시 공부한다.
- 확실한 이해를 위해 예제 코드를 본다.
- 실제 프로젝트에서 사용할 참여자 이름을 정한다.
- 클래스를 정의한다.
- 패턴에 정의한 연산에 대해 이름을 정의한다.
- 연산을 구현한다.
Reference