DesignPatternSmalltalkCompanion:AbstractFactory: Difference between revisions
Onionmixer (talk | contribs) (메소드 > 메서드 수정) |
Onionmixer (talk | contribs) (검수 20180727) |
||
(One intermediate revision by the same user not shown) | |||
Line 5: | Line 5: | ||
===의도=== | ===의도=== | ||
관련 객체 또는 종속 객체군을 작성하기위한 인터페이스를 제공한다. 클라이언트는 구체적인 클래스를 지정하지 않고도 추상적인 방식으로 모든 제품군의 제품을 만들 수 있다. | |||
===구조=== | ===구조=== | ||
Line 13: | Line 13: | ||
===논의=== | ===논의=== | ||
역설적으로 느껴지겠지만, 때때로 패턴이 적용되는 상황이 패턴 해결책만큼이나 복잡한 경우가 발생되는데, 추상 팩토리(abstract factory)가 이런 경우에 해당된다. 패턴 자체는 사실 꽤 단순한데, 문제 컨텍스트<sup>problem context</sup>에 많은 부분이 포함되어 있다. 일단 어떤 상황에 추상 팩토리 패턴을 적용할 수 있는지 부터 살펴보도록 하자.<ref name="주석1">추상 팩토리 패턴 이후에 나오는 Builder (47) 패턴을 읽어보길 권한다; 두 패턴은 밀접한 관계가 있으며 여러 문제를 공유한다.</ref> | |||
첫째, 여기에 컴포넌트 하위부품(subpart)부터 하나씩 시작해서 제품을 구축하는 목적을 가진 애플리케이션이 하나 있다. 이 애플리케이션을 이용해 차체, 엔진, 변속기, 차 내부로 나누어지는 자동차를 구현할 수 있다. 둘째, 이 애플리케이션에서는 단일 제품의 컴포넌트가 동일한 부품군, 즉 세트로 된 부품이어야 하며, 예를들어 Ford(Ford) 자동차의 경우는 Ford사에서 만든 엔진과 변속기를 필요로 한다. 이러한 부품은 Ford 군(계열)에 속한다고 할 수 있다. 셋째, 우리는 Ford 부품, Toyota(Toyota) 부품, 포르쉐(Porsche) 부품 등 여러 종류의 부품군을 가지고 있다. 해당 클래스는 클래스 계층구조(class hierarchy)를 통해 적절한 하위계층구조(subhierarchy)로 뻗어 나간다: 엔진은 CarEngine 하위계층구조에, 차체는 CarBody 계층구조에 속하게 된다. 따라서 우리는 이 애플리케이션이, (1) 하나의 부품군으로부터 자동차 컴포넌트를 쉽게 검색하고 부품군들 간의 오류를 허용하지 않으며(Toyota 엔진이 Ford 자동차에 사용되는 경우가 없도록), (2) 모든 부품군에 있어 통일된 부품 검색 코드를 사용할 수 있도록 하는 방법이 필요하다. 이런 경우에 추상 팩토리 패턴을 사용하면 두 가지 조건을 모두 충족시킬 수 있다. | |||
아래와 같은 자동차 클래스와 자동차 부품 클래스가 있다: | 아래와 같은 자동차 클래스와 자동차 부품 클래스가 있다: | ||
Line 22: | Line 23: | ||
[[image:dpsc_chapter03_AbstractFactory_03.png]] | [[image:dpsc_chapter03_AbstractFactory_03.png]] | ||
Vehicle 과 CarPart 는 Object 의 하위클래스이다. 물론 이러한 클래스 구조는 여러모로 지나치게 단순화시킨 구조이다. Ford와 같은 자동차 회사에서는 자동차, 차체, 엔진, 심지어 엔진 유형까지 (예: 카솔린 구동식 또는 디젤 엔진) 여러 유형의 모델이 있다. 따라서 실 세계의 자동차 모델에는 여기서 나타낸 것보다 추상화 단계가 더 많다. 하지만 이 책의 패턴 설명은 최대한 단순하고 쉽게 관리하는 것을 목적으로 한다. | |||
먼저 | 먼저 패턴 구현을 CarPartFactory 라는 추상 팩토리 클래스를 정의하는것부터 시작하자. 클래스는 "구체적 클래스를 지정하지 않고 관련 객체군 또는 종속 객체군을 생성할 수 있는 인터페이스를 제공한다" ('의도' 단락 참조). 또한 클래스는 makeCar, makeEngine, makeBody와 같은 추상적 '''Product'''-creation 메서드를 정의한다. 메서드까지 정의하고 나면 사용자는 제품군마다 하나씩의 팩토리의 구체적 하위클래스를 정의한다. 각 하위클래스는 적절한 부품을 생성하고 반환(return)하기 위해 제품 생성 메서드를 재정의한다. 따라서 사용자는 Object 아래에 다음과 같이 새로운 하위계층구조를 추가한다. | ||
[[image:dpsc_chapter03_AbstractFactory_04.png]] | [[image:dpsc_chapter03_AbstractFactory_04.png]] | ||
부품 생성 메서드를 구현하기 위해 추상 팩토리 클래스부터 | 부품 생성 메서드를 구현하기 위해 추상 팩토리 클래스부터 시작해야 하며, | ||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 39: | Line 40: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
이후 이러한 메서드를 오버라이드(override)하는 구체적 | 이후 이러한 메서드를 오버라이드(override)하는 구체적 하위클래스를 추가한다: | ||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 61: | Line 62: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
전체적으로 | 만들어질 팩토리는 전체적으로 다음과 같은 모습이 된다. | ||
[[image:dpsc_chapter03_AbstractFactory_05.png]] | [[image:dpsc_chapter03_AbstractFactory_05.png]] | ||
추상 팩토리 패턴을 | 추상 팩토리 패턴을 이용할때, 이 조각들을 조립하는 것은 팩토리 클라이언트에 달려 있다. 팩토리는 부품이 한 부품군에서 나오도록 보장하지만 부품을 반환하는 일만 하며, 최종 제품으로 조립하는 작업은 하지 않는다. 조립하는 작업은 클라이언트의 일이다.<ref name="주석2">이 점은 팩토리의 부품 자체가 복잡하게 구성된 부품일 경우에도 마찬가지다. 예를 들어, 팩토리는 여러 개의 하위컴포넌트 위젯들이 통합된 복합 판유리와 같이 미리 조립된 부품을 반환할 수도 있는 것이다. 그럼에도 불구하고, 팩토리 클라이언트에게는 이것이 창문처럼 좀 더 복잡한 제품으로 통합될 수 있는 하나의 부품으로 간주된다.</ref>(이것이 추상 팩토리와 Builder 패턴 (47) 사이의 주요 차이점이라는 것은 다음에 살펴보자.) | ||
CarAssembler 객체가 팩토리 클라이언트이고, 여기에 CarPartFactory 객체를 참조하는 factory 라는 이름의 인스턴스 변수가 하나 있다고 가정하자<ref name="주석3">언뜻 보면 조립 작업 과정의 최종 제품인 새 Car 를 생성하는 factory makeCar 를 말함으로써 자동차 조립 공정을 시작하기가 혼란스러워 보인다. 사실 이 방법은 복합 객체를 구축하는 전형적인 방법이다. 예를 들어 Visual Smalltalk 애서는 Menu 의 구성을 시작할 때 Menu new 라고 말한다. 이 시점에서 사용자가 가진 것은 메뉴의 껍데기(shell)에 불과하며, MenuItems 라는 하위컴포넌트를 추가하기 전까지는 제대로된 메뉴의 기능을 하지 못한다. 이와 비슷하게, factory makeCar 는 엔진이나 차체처럼 사용자가 추가해야 하는 컴포넌트로소 Car 의 빈 껍데기를 반환한다.</ref>. | |||
CarAssembler 객체가 팩토리 클라이언트이고, 여기에 CarPartFactory 객체를 참조하는 factory 라는 이름의 인스턴스 변수가 하나 있다고 가정하자. | |||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 76: | Line 76: | ||
starts out having no subcomponents, and add and | starts out having no subcomponents, and add and | ||
engine, body, etc." | engine, body, etc." | ||
car := factory makeCar. | |||
car := factory makeCar. | |||
car | car | ||
addEngine: factory makeEngine; | addEngine: factory makeEngine; | ||
Line 84: | Line 85: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
여전히 풀리지 않은 궁금증이 하나 있다. CarAssembler(팩토리 클라이언트)는 어떻게 | 만약 factory 가 FordFactory 의 인스턴스라면 자동차에 추가되는 엔진은 FordEngine 이 되며, 만약 factory 가 ToyotaFactory 의 인스턴스였다면 factory makeEngine 에 의해 ToyotaEngine 이 생성되어 진행 중인 자동차에 추가된다. | ||
여전히 풀리지 않은 궁금증이 하나 있다. CarAssembler(팩토리 클라이언트) 는 어떻게 해서 CarPartFactory 의 특정 하위클래스의 인스턴스를 얻게될까? 소비자의 선택을 바탕으로 해서 스스로 특정 하위클래스를 인스턴스화하기도 하며, 외부 객체에 의해 factory 인스턴스를 전달받기도 한다. 그러나 두 경우 모두 자동차 및 구성요소의 하위부품을 생성하는 코드는 동일하게 존재한다. 즉, 모든 CarPartFactory 클래스는 다형적으로 동일한 메시징 프로토콜을 실행하기 때문에, factory 의 클라이언트는 어떤 유형의 factory 와 대화 중인지에 대해 신경 쓰지 않아도 된다. 그저 factory 의 프로토콜이 제공하는 일반 메시지를 전송할 뿐이다. | |||
다형성 덕분에 클라이언트는 다수의 조건문(conditional)<ref name="역자주1">타 언어의 if 등의 비교 조건문</ref>보다는 한 가지 버전의 코드만 구현해서, 여전히 어떤 종류의 자동차든 생산할 수 있다. 비교해보자면, 추상 팩토리 패턴이 없는경우 자동차 생성 코드는 다음과 같은 모습이 된다: | |||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 109: | Line 111: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
추상 팩토리 | 지금의 경우에서 CarAssembler 객체 내에서는 어떠한 종류의 자동차를 구축하고, 그 하위부품은 무엇이 될 것인지를 스스로 결정하며, 실제 부품의 인스턴스화를 실행한다. 하지만 추상 팩토리 해법은 CarAssembler 객체로부터 생기는 모든 행위를 factory 라는 하나의 구분된 행위로 추상화시킨다. CarAssembler 내부에서 사용할 객체를 특정 자동차에 대한 factory 객체로 구성한 뒤에, 자동차와 하위부품을 생산하려 할때 CarAssembler 객체는 취습하는 factory 객체로 원하는 부품의 종류와 상관없이 항상 동일한 메시지로 부품을 요청하게 된다. | ||
추상 팩토리 접근법은 좀 더 모듈식이며 쉽게 확장이 가능한 설계로 만든다. 시스템안에 두 가지 유형의 자동차를 추가하려고 할때, 복잡한 조건문 집합 내에서 코드상의 여러 위치에 새 부품을 추가하기 위해 CarAssembler>>buildCar 를 다시 찾는것 보다는, CarPartFactory의 새 하위클래스와 이를 인스턴스화시킬 코드만 존재하면 된다. | |||
여기서는 사실상 두 가지의 추상화가 이루어지고 있다. 첫째, 모든 CarPartFactory 의 객체들은 동일한 메시지 인터페이스를 구현한다. 이런 구현은 factory 클라이언트가 보내는 CarPartFactory 타입이 정확히 무엇인지 신경쓰지 않고 동일한 부품 생성 메시지를 보낼 수 있게 해준다. 둘째, ConcreteProducts(자동차 부품 클래스) 는 각 부품 하위계층구조의 추상 상위클래스(superclass)에서 정의된 것과 동일한 인터페이스를 구현하고 있다. 예를 들어, 모든 Car 들은 addEngine: 과 addBody: 메시지에 대해 어떻게 응답해야 하는지를 알고 있다. 또한 CarBody 객체들은 모두 color: 과 color 메시지들을 구현한다. 모든 CarEngine 들 또한 이와 마찬가지로 공통된 메시징 인터페이스를 제공한다. 다른 부품의 경우도 마찬가지다. | |||
이 상황을 보다 상세히 설명하기 위해서, [디자인 패턴]에서와 마찬가지로 factory 클라이언트가 외부<sup>outsider</sup>로부터 factory 객체를 전달받았다고 가정하자. 이는 CarAssembler 가 어떤 종류의 CarPartFactory 를 사용하는지 스스로 정확히 알지 못하고 있다는 점을 암시한다. 이를 위해 (1) 자신의 factory 객체를 참조하는 인스턴스 변수를 갖기 위해 factory 클라이언트인 CarAssembler 를 정의하던가, 또는 (2) factory 객체를 argument 로서 클라이언트의 자동차 생성 메서드로 전달함으로써 추상화를 구현해 낸다. 첫 번째 방법을 사용한다면, 지금의 경우에서는 CarAssembler 클래스 및, 클래스의 관련 메서드를 다음과 같이 정의해야할 것이다: | |||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 132: | Line 135: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
외부 객체, 즉 CarAssembler의 클라이언트는 적절한 | 외부 객체, 즉 CarAssembler의 클라이언트는 적절한 factory 인스턴스를 이용해 CarAssembler 를 생성하여 초기화(initialize)할 것이다. CarAssembler 의 클라이언트가 대화형 3D 자동차 시각화 애플리케이션이라고 가정해 보자. 사용자는 유저 인터페이스에서 자동차를 선택할 수 있다; 애플리케이션은 이에 대한 응답으로 화면에 3차원 그래픽의 이미지를 만든다. 사용자는 자동차가 어떻게 생겼는지 살펴보고 인테리어, 엔진 또는 관심 부분을 "살펴보기"(가까이 가기, 내부 모습)위해 3D 공간을 탐색한다. 자동차는 화면 위의 사용자 버튼의 선택을 기반으로 구성된다고 가정하며, 프로그램의 사용자는 ''Ford, Toyota, Porsche'' 버튼 중 하나를 클릭해서 살펴보기 원하는 차를 선택할 수 있다. 이에 대한 응답으로 유저 인터페이스 코드는 다음과 같은 작업을 수행할 것이다<ref name="역자주2">다음의 코드에서 FordFactory 는 CarPartFactory 의 하위 클래스이다.</ref>. | ||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 144: | Line 147: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
앞에서 논의했던 두번째 해법의 경우, CarAssembler 는 자신의 factory 객체를 참조하는 인스턴스 변수를 가지지 않는다. 그 대신에 클라이언트가 차를 조립하고 싶을때 factory 는 간단히 넘겨버려도 된다. 이런 과정 덕분에 assembleCar 메서드는 하나의 인수만 가지도록 변경된다. | |||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 156: | Line 160: | ||
^car | ^car | ||
</syntaxhighlight> | </syntaxhighlight> | ||
===구현과 예제 코드=== | ===구현과 예제 코드=== | ||
이제 추상 팩토리 패턴의 | 이제 추상 팩토리 패턴의 주요 요소를 본인의 애플리케이션에 사용하기에 충분할 만큼 살펴보았다. 이번 단락에서는 추상 팩토리 주제에서 변형의 경우들을 이야기하려 하며, 특히 팩토리 객체를 구현하는 여러가지 방법을 논의하고자 한다. 이 변형 중 일부는 [디자인 패턴]에서 다루고 있고, 저자들은 Smalltalk 특정적인 다양성도 몇 가지 추가하였다. 여기서는 각 구현의 장단점을 지적하겠지만, 특별한 가지 해법을 옹호하려는 것은 아니다; 오히려 다른 모든 패턴들과 마찬가지로 다양한 방법으로 구현될 수 있음을 보이는 것이 목표다. 선택권은 특정 애플리케이션과 다른 객체들과의 상호작용으로 인해 부과되는 추가 제약이나 개인의 선호도 및 미학에 따라 좌우된다. 크리스토퍼 알렉산더(Christopher Alexander)와 그 동료들은, 패턴이 특정 문제에 대한 해법에 관련된 기본 견해와 개념을 제공하기는 하지만 "본인의 선호도와 지역적 상태에 대해 조정함으로써 자신만의 방식으로" 실현시킬 수 있다고 주장했다 (Alexander et al., 1977, p. xiii). | ||
====자동차 팩토리에 대한 바닐라 구현==== | ====자동차 팩토리에 대한 바닐라 구현==== | ||
예제로 보여 준 애플리케이션에서 | 예제로 보여 준 애플리케이션에서 CarPartfactory 는 모든 자동차 factory 에 대한 인터페이스를 정의한다. 대안 팩토리는 CarPartFactory 의 하위클래스로 정의되며, 각 대안 팩토리는 적절한 부품 생성 메서드를 오버라이드한다. 가장 간단히 말해 바닐라 구현에서 이러한 메서드 각각은 다음과 같이 인스턴스화하고 반환하기 위해서 클래스를 하드코딩(hard-code)한다. | ||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 173: | Line 181: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
이 접근법은 각 부품에 대한 코드를 지역화(localize)한다. 물론 메서드를 지역화하는 것은 나중에 변경된 내용을 지역화 하는 것을 의미한다. | |||
Toyota 가 Ford 로부터 엔진을 구매하기 시작한다고 가정하자(자동차 산업에서는 실제로 이렇게 희한한 일이 발생하고 있기도 한다!). 새로운 유형의 엔진은 FordEngine 인스턴스를 반환하는 ToyotaFactory>>makeEngine 메서드만 변경하는것을 의미하며, 클라이언트 코드는 변경되지 않은 채로 유지된다. | |||
====Constant Method 해법==== | ====Constant Method 해법==== | ||
앞에서 살펴본 구현은 가장 간단한 형태기는 하지만, 그 안에서 모든 factory 클래스의 각 부품에 맞는 각 메서드를 인스턴스화하기 위해 클래스를 하드코딩했다. 이 작업은 CarPartFactory 와 CarPartFactory 의 구체 하위클래스에 makeEngine, makeBody 등의 메서드를 필요로 한다. 이 방법 대신 Factory Method (63) 패턴의 Constant Method 변형 중 하나를 적용시킬 수도 있다. 여기에서는 각각의 부품생성 메서드를 한번만 정의해서 필요한 클래스 객체를 쉽게 인스턴스화 할 수 있도록 Constant Method 를 이용해서 각 factory 클래스가 부품 클래스의 "이름을 만들게" 할 것이다(Beck, 1997). | |||
추상적 상위클래스에서 부품 생성 메서드를 | 추상적 상위클래스에서 부품 생성 메서드를 factory 메서드로 재정의함으로써 시작한다: | ||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 194: | Line 205: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
그 뒤에 각 factory 클래스에 Constant Method 를 정의한다. | |||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 213: | Line 224: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
이제 코드를 모듈화시켰으므로 새로운 자동차 팩토리 클래스가 정의되면 그에 상응하는 부품 클래스만 이름을 정하면 된다. 그러나 자동차 부품 | 이제 코드를 모듈화시켰으므로 새로운 자동차 팩토리 클래스가 정의되면 그에 상응하는 부품 클래스만 이름을 정하면 된다. 그러나 자동차 부품 예제의 경우 이것과 바닐라 구현 간의 차이는 그다지 크지 않다. | ||
====부품 카탈로그 해법==== | ====부품 카탈로그 해법==== | ||
앞에서 설명된 구현들의 경우, 자동차의 각 부품은 factory 클래스 내의 고유 메서드를 필요로 한다 (예: makeBody, makeEngine). 이는 fatory 내에 메서드의 수를 넘치게 생성하도록 유도하기도 한다. 자동차에 새로운 유형의 부품, 즉 새로운 유형의 고급 오디오 시스템을 추가하려면 CarPartFactory 와 그에 해당하는 모든 하위클래스에 새로운 makeDeluxeCDAudioSystem 메서드를 추가해야 할 것이며, 이런 작업을 통해 factory 클라이언트가 알아야 하는 인터페이스를 확장할 수 있을 것이다. | |||
[디자인 패턴] | |||
[디자인 패턴] 에서는 Smalltalk 에서 클래스는 일급 객체<sup>first-class</sup><ref name="일급 객체">https://ko.wikipedia.org/wiki/일급_객체</ref>라는 사실을 이용해서 이 문제를 (DP90과 그 다음 문제) 해결하는 방법을 설명하고 있다. 자동차 부품의 클래스를 "부품 카탈로그<sup>part catalog</sup>"로 저장하고 부품마다 하나씩 메서드를 만드는 것이 아니라 파라미터화된 유일한 부품 생성 메서드를 가진 CarPartFactory 를 구현한다. | |||
이러한 접근법은 CarPartFactory 클래스에 partCatalog 인스턴스 변수를 추가하고 이것을 Dictionaryㅡ여기서 키<sup>key</sup>는 부품 유형이 (Symbol과 같은) 되며 해당 값<sup>value</sup>은 적절한 클래스가 된다ㅡ로서 초기화하는 과정을 동반한다. FordFactory 클래스의 partCatalog 는 다음과 같다: | |||
{| style="border: 1px solid black;" | {| style="border: 1px solid black;" | ||
Line 235: | Line 249: | ||
|} | |} | ||
이 접근법을 가능하게 하는 새로운 CarPartFactory 클래스 정의와 메서드를 다음 | |||
이 방식의 접근법을 가능하게 하는 새로운 CarPartFactory 클래스 정의와 메서드를 다음 내용에서 소개한다: | |||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 250: | Line 265: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
하위클래스는 고유의 부품 카탈로그 버전을 구축하기 위해 추상적 상위클래스에서 선언된 initialize 메서드를 다음과 같이 오버라이드하게될 것이다: | |||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 273: | Line 289: | ||
이제 추상 상위클래스에 부품 생성을 위한 단일 메서드(single method)를 정의한다. 이는 | 이제 추상 상위클래스에 부품 생성을 위해 값(객체)를 반환하기 위한 단일 메서드(single method)를 정의한다. 이는 부품의 유형<sup>part type</sup>을 argument 로 받는다. | ||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 283: | Line 299: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
이제 | |||
이제 자동차의 factory 클라이언트는 (예: CarAssembler) 이렇게 만들어진 단일 메시지를 이용해 모든 부품을 생성해 낼수 있다. 부품 생성 코드는 다음과 같은 모습이 아니라, | |||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 297: | Line 314: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
부품 카탈로그 접근법을 | 부품 카탈로그 접근법을 사용하면, CarPartFactory 계층구조의 구체적 클래스는 고유한 부품 카탈로그를 초기화시키는 단일 메서드만 정의하면 된다. 그 외의 부품 생성 동작은 CarPartFactory 의 추상 클래스에 정의된다. | ||
====다른 부품 카탈로그를 위한 또 다른 구현==== | ====다른 부품 카탈로그를 위한 또 다른 구현==== | ||
"부품 카탈로그" 접근법을 고려하는 경우, 다른형태의 factory 를 구현하는 또 다른 방법이 있다. 이 경우에도 대체 factory 제공을 위해 서브클래싱(subclassing)을 사용하기 때문에 FordFactory, ToyotaFactory, PorscheFactory 중 하나를 클라이언트에서 선택할 필요가 있다. 하지만 부품 카탈로그는 각 factory 클래스의 모든 인스턴스에서 동일해야 하기 때문에 partCatalog 를 상위클래스내에서 인스턴스 변수로 정의하고, 클래스에서 선언된 변수를 적절히 초기화하기 위해, 각각의 factory 하위클래스에서 사용될 클래스 메서드를 코딩하며, 각 하위클래스마다 초기화 메서드를 한번씩 호출하도록 할 수 있다. | |||
여기서 추상클래스는 다음과 같이 재정의한다: | |||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 320: | Line 340: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
이제 CarPartFactory의 구체적 | |||
이제 CarPartFactory의 구체적 하위클래스마다 하나씩의 클래스 인스턴스 변수가 있다. 모든 하위클래스가 내용을 공유하는 클래스 변수와는 달리, 각 하위클래스는 해당 클래스 인스턴스 변수에 대해 고유의 private 버전을 가지게 된다. 따라서 FordFactory 의 partCatalog 를 초기화한다고 해도 ToyotaFactory 의 partCatalog 에는 영향을 미치지 않는다. 각 클래스의 카탈로그는 다음과 같이 초기화할 수 있다: | |||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 341: | Line 362: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
이제 남은 일은 인스턴스 변수 | |||
이제 남은 일은 인스턴스 변수 대신에 각 하위클래스 고유의 클래스 인스턴스 변수를 사용하기 위해 부품 생성 인스턴스의 단일 메서드를 재정의하는 것이다. 이 메서드가 하는 일은 동일한 서명(signature)으로 클래스 메서드를 호출하는것 뿐이다: | |||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 349: | Line 371: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
====단일 | 그럼에도 불구하고 DP 90 페이지에서 지적한 바와 같이 이 접근법은 제품군마다 새로운 구체 factory 하위클래스를 필요로 한다 (Fords 에 하나, Toyotas 에 하나). 메시징 인터페이스를 단일 메서드로 줄이긴 했지만 팩토리 클라이언트들은 여전히 여러 개의 팩토리 클래스들 중 인스턴스화해야 할 클래스를 선택해야 한다. 다만 유연성은 약간 감소될 수 있다; 새로운 제품군이 생길 때 새로운 하위클래스를 추가하는 것은 확실히 간단한 작업이지만, 그럼에도 좀 더 작은 단일-클래스 구현을 만들 수도 있다. | ||
====단일 factory 클래스==== | |||
단일-클래스 해법은 하나의 | 단일-클래스 해법은 하나의 factory 클래스만 구현해서, 인스턴스화하기 적절한 부품 클래스를 이용해 새로 생성되는 각 인스턴스의 부품 카탈로그를 초기화하는 것이다(partCatalog 를 인스턴스 변수로 사용하는 작업으로 돌아왔다). 다시 말해, CarPartFactory 는 어떠한 하위클래스도 가지지 않으며, 클라이언트들은 항상 CarPartFactory 자체를 인스턴스화한다. | ||
이 접근법에서는 | |||
이 접근법에서는 프로그래머가 CarPartFactory 안에 단순한 클래스 메서드를 구현해야 하며, 각 메서드마다 의미를 지니는 factory 생성 이름을 가진다. 단일 factory 의 각 메서드는 팩토리의 유형마다 필요한 초기화를 수행한다. 이는 다음과 같은 형태를 가진다: | |||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 378: | Line 404: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
CarPartFactory 클라이언트가 | CarPartFactory 클래스를 사용할 클라이언트가 Ford factory 를 생성하고자 할 경우 다음과 같은 메시지 전송이 이루어진다: | ||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
carFactory := CarPartFactory fordFactory. | |||
</syntaxhighlight> | </syntaxhighlight> | ||
부품은 정확히 이전과 동일하게 생성된다: | 부품은 정확히 이전과 동일하게 생성된다: | ||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
carFactory make: #engine. | |||
</syntaxhighlight> | </syntaxhighlight> | ||
====교묘한 단일-클래스 구현==== | ====교묘한 단일-클래스 구현==== | ||
사용자는 (1) 일관된 명명규칙에 따라 우리의 모든 클래스를 정의하였고, (2) Smalltalk 는 반영적 특성<sup>reflective</sup>을 가진다, 라는 두 가지 사실을 이용하는 접근법을 통해 또 다른 단일-클래스 팩토리를 구현할 수 있다. 모든 부품 클래스 이름은 자동차 제작회사의 이름을 접두사로 하고 부품 이름을 접미사로 한다: FordEngine, ToyotaEngine, PorscheEngine; FordCar, ToyotaCar, PorscheCar. 이 규칙을 다음과 같이 이용할 수 있다: | |||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 412: | Line 441: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
이 접근법을 이용해 자동차 | 어떻게 이런 방식이 가능하나면, 모든 전역변수는 Smalltalk Dictionary 의 항목으로 참조되며, 클래스는 클래스와 동일한 이름의 global 로 참조되기 때문이다. 따라서 Smalltalk at: #FordEngine이라는 표현식(expression)은 FordEngine 클래스 객체를 가져온다(retrieves). | ||
이 접근법을 이용해 자동차 factory 는 항상 CarPartFactory 의 인스턴스가 된다. 자동차 부품 생성은 다음 형식을 취한다 (여기서 carCompany 는 사용자로부터 얻은 Symbol이다): | |||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 424: | Line 454: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
===알려진 | 그러나 방법에는 단점이 있다. 정적 검사(static inspection)또는 동적 런타임 추적(dynamic runtime trace)으로부터 코드를 이해야기가 힘든데, 코드는 참조로 만들어지며 해당 identity 는 즉석에서 생성된 클래스로 메시지를 보내기 때문이다. 예를 들어, FordCar 클래스를 참조하는 메서드를 모두 찾기 위해 Smalltalk 개발환경의 도구를 사용한다면, makeCar: 메서드는 검색 결과로 나타나는 메서드 목록에 포함되지 않을 것이다. makeCar: 는 모든 자동차 클래스를 암묵적으로 참조하지만 코드에 명시적으로 표현되어 있지는 않기 때문이디. | ||
===알려진 Smalltalk 사용예=== | |||
====UILookPolicy==== | ====UILookPolicy==== | ||
VisualWorks 에서 UIBuilder 는 위젯의 특징들을 바탕으로 윈도우를 구성하는 책임을 가진다 (다른 기능들도 있지만). 각 위젯마다 UIBuilder 는 실제 위젯 인스턴스 생성을 수행하기 위해 그와 관련된 UILookPolicy 를 요청한다. UILookPolicy 의 서로 다른 하위클래스들이 최근 선택된 룩앤필(look and feel)에 따라 (윈도우, OS/2, Motif, Macintosh 또는 기본 Smalltalk 모양) 서로 다른 위젯을 인스턴스화시키기 위해 이러한 생성 메시지를 다형적으로 구현한다. 하지만 UIBuilder 는 그것이 다루는 모양 정책 객체(look policy object)가 무엇인지에 대한 지식이 없다. 다시 말해 UIBuilder 는 Win3LookPolicy, CUALookPolicy 또는 다른 정책 객체(policy object)와 연결되어 있는지 알지 못하거나 신경쓰지 않는다는 의미이며, 단지 추상적 UILookPolicy 프로토콜에 정의된 일반적 위젯 생성 메시지를 전송할 뿐이다. 따라서 모양 정책 객체는 위젯에 대한 추상 팩토리 역할을 한다. | |||
====Constant Method 이용하기==== | ====Constant Method 이용하기==== | ||
Constant 메서드를 | Constant 메서드를 사용해서 추상 팩토리를 구현하는 방법은 이미 소개한 바 있다. UILookPolicy 에 의한 사용 예제도 제시되었다 (팩토리 메서드에서도 이 예제를 취급한다). 예를 들어, UILookPolicy 에 슬라이더 위젯을 생성하는 메서드가 있다고 가정하자. | ||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 443: | Line 478: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
model: 메시지는 sliderClass가 반환시킨 클래스를 인스턴스화한다. | |||
model: 메시지는 sliderClass가 반환시킨 클래스를 인스턴스화한다. UILookPolicy 는 sliderClass 의 기본(default) 버전을 정의하며, 그의 구체 하위클래스 일부는 슬라이드를 각각 다른 모양으로 생성하기 위해서 그것을 오버라이드한다: | |||
<syntaxhighlight lang="smalltalk"> | <syntaxhighlight lang="smalltalk"> | ||
Line 455: | Line 491: | ||
^Win3SliderView | ^Win3SliderView | ||
</syntaxhighlight> | </syntaxhighlight> | ||
===관련 패턴=== | ===관련 패턴=== | ||
Line 460: | Line 498: | ||
====Builder==== | ====Builder==== | ||
추상 팩토리는 Builder(47) 패턴과 밀접하게 관련되어 있다. 주요 차이점은 전체적인 제품 조립을 누가 | 추상 팩토리는 Builder(47) 패턴과 밀접하게 관련되어 있다. 주요 차이점은 전체적인 제품 조립을 누가 책임지는가에서 온다. 추상 팩토리에서는 함께 작업하거나 함께 작동하도록 보장된 부품을 팩토리가 반환한다. 하지만 하나의 최종 제품으로 조립하는 일은 factory 클라이언트가 한다. factory 로 전송되는 모든 메시지는 전체적인 최종 제품의 하위컴포넌트 또는 새로운 부품등의 결과를 가져오며ㅡ즉 factory 안의 모든 부품 생성 메서드는 컴포넌트 부품을 반환한다는 의미ㅡfactory 의 클라이언트는 이 부품을 각각 생성되는 제품에 추가한다. 반면, Builder 는 제품이 조립되는 동안 제품을 유지할 수 있는 고유의 내부 상태를 가진다. Builder 가 "컴포넌트 A를 추가한다," "컴포넌트 B를 추가한다,"라는 요청을 듣게 되면 이 하위컴포넌트를 캡슐화된 Product 로 추가하는 것은 Builder 의 일이 된다. "컴포넌트 X를 추가하라" 라는 메시지가 전송될때에도 Builder 는 아무 것도 반환하지 않는다. Builder 의 클라이언트가 하위부품의 추가작업을 완료하면 클라이언트가 Builder에게 "최종 제품을 보여달라,"고 말한다. 그때서야 Builder는 최종 제품을 반환한다. | ||
====Factory Method==== | ====Factory Method==== | ||
factory 객체는 어떤 클래스를 인스턴스화할지 결정하기 위해 팩토리 메서드(63) 패턴의 Constant 메서드 변수를 호출할 수 있음을 앞에서 알아봤다. 그리고 팩토리 메서드는 실질적으로 추상 팩토리의 경쟁자라고 볼 수 있지만, 두 패턴은 어떻게 보면 서로 구조적으로 정반대에 해당한다. 추상 팩토리의 바닐라 구현을 살펴본 본문에서 다수의 부품군ㅡ제품군마다 하나의 클래스ㅡ를 고려하기 위해 다수의 구체 factory 클래스를 정의하고 있다. 그 뒤에 단일 애플리케이션 객체가 동일한 코드를 이용해 어떤 제품군으로부터든 부품을 인스턴스화시킬 수 있도록 하였다. 애플리케이션은 바람직한 부품군과 관련된 factory 클래스를 인스턴스화시킬 뿐이다 (Ford 부품을 원하면 FordFactory 를 인스턴스화하는 것이다). 구조 다이어그램에서 단일 클라이언트는 factory 클래스의 하위계층구조로부터 하나의 factory 객체를 가리킨다는 점을 명심한다. | |||
팩토리 메서드 접근법을 이용할 경우 factory 클래스의 전체 계층구조를 정의하는 작업을 피할 수 있다. 하지만 그 대신 애플리케이션 클래스의 계층구조가 필요할 것이다. 모든 구체적 하위클래스에 대한 엄청난 양의 동작을 정의하면서도 factory 메서드의 정의는 하위클래스로 미루는 추상 애플리케이션 클래스가 필요할 것이다. 각 하위클래스는 그에 따라 factory 메서드를 오버라이드할 것이다: Ford 애플리케이션 클래스는 해당되는 factory 메서드가 Ford 부품을 반환하도록 시킬 것이며, Toyota 와 관련된 애플리케이션 클래스는 Toyota 부품 객체를 생성 및 반환하는 factory 메서드가 있을 것이다. | |||
==Notes== | ==Notes== |
Latest revision as of 06:44, 27 July 2018
ABSTRACT FACTORY (DP 87)
객체생성
의도
관련 객체 또는 종속 객체군을 작성하기위한 인터페이스를 제공한다. 클라이언트는 구체적인 클래스를 지정하지 않고도 추상적인 방식으로 모든 제품군의 제품을 만들 수 있다.
구조
논의
역설적으로 느껴지겠지만, 때때로 패턴이 적용되는 상황이 패턴 해결책만큼이나 복잡한 경우가 발생되는데, 추상 팩토리(abstract factory)가 이런 경우에 해당된다. 패턴 자체는 사실 꽤 단순한데, 문제 컨텍스트problem context에 많은 부분이 포함되어 있다. 일단 어떤 상황에 추상 팩토리 패턴을 적용할 수 있는지 부터 살펴보도록 하자.[1]
첫째, 여기에 컴포넌트 하위부품(subpart)부터 하나씩 시작해서 제품을 구축하는 목적을 가진 애플리케이션이 하나 있다. 이 애플리케이션을 이용해 차체, 엔진, 변속기, 차 내부로 나누어지는 자동차를 구현할 수 있다. 둘째, 이 애플리케이션에서는 단일 제품의 컴포넌트가 동일한 부품군, 즉 세트로 된 부품이어야 하며, 예를들어 Ford(Ford) 자동차의 경우는 Ford사에서 만든 엔진과 변속기를 필요로 한다. 이러한 부품은 Ford 군(계열)에 속한다고 할 수 있다. 셋째, 우리는 Ford 부품, Toyota(Toyota) 부품, 포르쉐(Porsche) 부품 등 여러 종류의 부품군을 가지고 있다. 해당 클래스는 클래스 계층구조(class hierarchy)를 통해 적절한 하위계층구조(subhierarchy)로 뻗어 나간다: 엔진은 CarEngine 하위계층구조에, 차체는 CarBody 계층구조에 속하게 된다. 따라서 우리는 이 애플리케이션이, (1) 하나의 부품군으로부터 자동차 컴포넌트를 쉽게 검색하고 부품군들 간의 오류를 허용하지 않으며(Toyota 엔진이 Ford 자동차에 사용되는 경우가 없도록), (2) 모든 부품군에 있어 통일된 부품 검색 코드를 사용할 수 있도록 하는 방법이 필요하다. 이런 경우에 추상 팩토리 패턴을 사용하면 두 가지 조건을 모두 충족시킬 수 있다.
아래와 같은 자동차 클래스와 자동차 부품 클래스가 있다:
Vehicle 과 CarPart 는 Object 의 하위클래스이다. 물론 이러한 클래스 구조는 여러모로 지나치게 단순화시킨 구조이다. Ford와 같은 자동차 회사에서는 자동차, 차체, 엔진, 심지어 엔진 유형까지 (예: 카솔린 구동식 또는 디젤 엔진) 여러 유형의 모델이 있다. 따라서 실 세계의 자동차 모델에는 여기서 나타낸 것보다 추상화 단계가 더 많다. 하지만 이 책의 패턴 설명은 최대한 단순하고 쉽게 관리하는 것을 목적으로 한다.
먼저 패턴 구현을 CarPartFactory 라는 추상 팩토리 클래스를 정의하는것부터 시작하자. 클래스는 "구체적 클래스를 지정하지 않고 관련 객체군 또는 종속 객체군을 생성할 수 있는 인터페이스를 제공한다" ('의도' 단락 참조). 또한 클래스는 makeCar, makeEngine, makeBody와 같은 추상적 Product-creation 메서드를 정의한다. 메서드까지 정의하고 나면 사용자는 제품군마다 하나씩의 팩토리의 구체적 하위클래스를 정의한다. 각 하위클래스는 적절한 부품을 생성하고 반환(return)하기 위해 제품 생성 메서드를 재정의한다. 따라서 사용자는 Object 아래에 다음과 같이 새로운 하위계층구조를 추가한다.
부품 생성 메서드를 구현하기 위해 추상 팩토리 클래스부터 시작해야 하며,
CarPartFactory>>makeCar
self subclassResponsibi1ity
CarPartFactory>>makeEngine
self subclassResponsibility
CarPartFactory>>makeBody
self subclassResponsibility
이후 이러한 메서드를 오버라이드(override)하는 구체적 하위클래스를 추가한다:
FordFactory>>makeCar
^FordCar new
FordFactory>>makeEngine
^FordEngine new
FordFactory>>makeBody
^FordBody new
ToyotaFactory>>makeCar
^ToyotaCar new
ToyotaFactory>>makeEngine
^ToyotaEngine new
ToyotaFactory>>makeBody
^ToyotaBody new
만들어질 팩토리는 전체적으로 다음과 같은 모습이 된다.
추상 팩토리 패턴을 이용할때, 이 조각들을 조립하는 것은 팩토리 클라이언트에 달려 있다. 팩토리는 부품이 한 부품군에서 나오도록 보장하지만 부품을 반환하는 일만 하며, 최종 제품으로 조립하는 작업은 하지 않는다. 조립하는 작업은 클라이언트의 일이다.[2](이것이 추상 팩토리와 Builder 패턴 (47) 사이의 주요 차이점이라는 것은 다음에 살펴보자.)
CarAssembler 객체가 팩토리 클라이언트이고, 여기에 CarPartFactory 객체를 참조하는 factory 라는 이름의 인스턴스 변수가 하나 있다고 가정하자[3].
CarAssembler>>assembler
| car |
"Create the top-level part, the car object which
starts out having no subcomponents, and add and
engine, body, etc."
car := factory makeCar.
car
addEngine: factory makeEngine;
addBody: factory makeBody;
...
^car
만약 factory 가 FordFactory 의 인스턴스라면 자동차에 추가되는 엔진은 FordEngine 이 되며, 만약 factory 가 ToyotaFactory 의 인스턴스였다면 factory makeEngine 에 의해 ToyotaEngine 이 생성되어 진행 중인 자동차에 추가된다.
여전히 풀리지 않은 궁금증이 하나 있다. CarAssembler(팩토리 클라이언트) 는 어떻게 해서 CarPartFactory 의 특정 하위클래스의 인스턴스를 얻게될까? 소비자의 선택을 바탕으로 해서 스스로 특정 하위클래스를 인스턴스화하기도 하며, 외부 객체에 의해 factory 인스턴스를 전달받기도 한다. 그러나 두 경우 모두 자동차 및 구성요소의 하위부품을 생성하는 코드는 동일하게 존재한다. 즉, 모든 CarPartFactory 클래스는 다형적으로 동일한 메시징 프로토콜을 실행하기 때문에, factory 의 클라이언트는 어떤 유형의 factory 와 대화 중인지에 대해 신경 쓰지 않아도 된다. 그저 factory 의 프로토콜이 제공하는 일반 메시지를 전송할 뿐이다.
다형성 덕분에 클라이언트는 다수의 조건문(conditional)[4]보다는 한 가지 버전의 코드만 구현해서, 여전히 어떤 종류의 자동차든 생산할 수 있다. 비교해보자면, 추상 팩토리 패턴이 없는경우 자동차 생성 코드는 다음과 같은 모습이 된다:
CarAssembler>>assembler
"Without Abstract Factory."
| car |
car := (consumerChoice == #Ford
ifTrue: [FordCar new]
ifFalse: [consumerChoice ==#Toyota
ifTrue: [ToyotaCar new]
ifFalse: [consumerChoice == #Porsche
ifTrue: [PorscheCar new]
ifFalse: [...]).
car addEngine:
(consumerChoice == #Ford
ifTrue: [FordEngine new]
ifFalse: [...]).
...
^car
지금의 경우에서 CarAssembler 객체 내에서는 어떠한 종류의 자동차를 구축하고, 그 하위부품은 무엇이 될 것인지를 스스로 결정하며, 실제 부품의 인스턴스화를 실행한다. 하지만 추상 팩토리 해법은 CarAssembler 객체로부터 생기는 모든 행위를 factory 라는 하나의 구분된 행위로 추상화시킨다. CarAssembler 내부에서 사용할 객체를 특정 자동차에 대한 factory 객체로 구성한 뒤에, 자동차와 하위부품을 생산하려 할때 CarAssembler 객체는 취습하는 factory 객체로 원하는 부품의 종류와 상관없이 항상 동일한 메시지로 부품을 요청하게 된다.
추상 팩토리 접근법은 좀 더 모듈식이며 쉽게 확장이 가능한 설계로 만든다. 시스템안에 두 가지 유형의 자동차를 추가하려고 할때, 복잡한 조건문 집합 내에서 코드상의 여러 위치에 새 부품을 추가하기 위해 CarAssembler>>buildCar 를 다시 찾는것 보다는, CarPartFactory의 새 하위클래스와 이를 인스턴스화시킬 코드만 존재하면 된다.
여기서는 사실상 두 가지의 추상화가 이루어지고 있다. 첫째, 모든 CarPartFactory 의 객체들은 동일한 메시지 인터페이스를 구현한다. 이런 구현은 factory 클라이언트가 보내는 CarPartFactory 타입이 정확히 무엇인지 신경쓰지 않고 동일한 부품 생성 메시지를 보낼 수 있게 해준다. 둘째, ConcreteProducts(자동차 부품 클래스) 는 각 부품 하위계층구조의 추상 상위클래스(superclass)에서 정의된 것과 동일한 인터페이스를 구현하고 있다. 예를 들어, 모든 Car 들은 addEngine: 과 addBody: 메시지에 대해 어떻게 응답해야 하는지를 알고 있다. 또한 CarBody 객체들은 모두 color: 과 color 메시지들을 구현한다. 모든 CarEngine 들 또한 이와 마찬가지로 공통된 메시징 인터페이스를 제공한다. 다른 부품의 경우도 마찬가지다.
이 상황을 보다 상세히 설명하기 위해서, [디자인 패턴]에서와 마찬가지로 factory 클라이언트가 외부outsider로부터 factory 객체를 전달받았다고 가정하자. 이는 CarAssembler 가 어떤 종류의 CarPartFactory 를 사용하는지 스스로 정확히 알지 못하고 있다는 점을 암시한다. 이를 위해 (1) 자신의 factory 객체를 참조하는 인스턴스 변수를 갖기 위해 factory 클라이언트인 CarAssembler 를 정의하던가, 또는 (2) factory 객체를 argument 로서 클라이언트의 자동차 생성 메서드로 전달함으로써 추상화를 구현해 낸다. 첫 번째 방법을 사용한다면, 지금의 경우에서는 CarAssembler 클래스 및, 클래스의 관련 메서드를 다음과 같이 정의해야할 것이다:
Object subclass: #CarAssembler
instanseVariableNames: 'factory'
classVariablesNames: ''
poolDictionaries: ''
CarAssembler>>factory: aCarPartFactory
"setter method"
factory := aCarPartFactory
CarAssembler class>>using: aCarPartFactory
"Instance creation method"
^self new factory: aCarPartFactory
외부 객체, 즉 CarAssembler의 클라이언트는 적절한 factory 인스턴스를 이용해 CarAssembler 를 생성하여 초기화(initialize)할 것이다. CarAssembler 의 클라이언트가 대화형 3D 자동차 시각화 애플리케이션이라고 가정해 보자. 사용자는 유저 인터페이스에서 자동차를 선택할 수 있다; 애플리케이션은 이에 대한 응답으로 화면에 3차원 그래픽의 이미지를 만든다. 사용자는 자동차가 어떻게 생겼는지 살펴보고 인테리어, 엔진 또는 관심 부분을 "살펴보기"(가까이 가기, 내부 모습)위해 3D 공간을 탐색한다. 자동차는 화면 위의 사용자 버튼의 선택을 기반으로 구성된다고 가정하며, 프로그램의 사용자는 Ford, Toyota, Porsche 버튼 중 하나를 클릭해서 살펴보기 원하는 차를 선택할 수 있다. 이에 대한 응답으로 유저 인터페이스 코드는 다음과 같은 작업을 수행할 것이다[5].
CarAssemblerUI>>fordButtonClicked
"The user clicked the 'Ford' button."
| assembler car |
assembler := CarAssembler using: FordFactory new.
car := assembler assembleCar.
"Now, draw the assembled car on the screen:"
...
앞에서 논의했던 두번째 해법의 경우, CarAssembler 는 자신의 factory 객체를 참조하는 인스턴스 변수를 가지지 않는다. 그 대신에 클라이언트가 차를 조립하고 싶을때 factory 는 간단히 넘겨버려도 된다. 이런 과정 덕분에 assembleCar 메서드는 하나의 인수만 가지도록 변경된다.
CarAssembler>>assemblerCarUsing: aCarPartFactory
| car |
car = aCarPartFactory makeCar.
car
addEngine: aCarPartFactory makeEngine;
addBody: aCarPartFactory makeBody;
...
^car
구현과 예제 코드
이제 추상 팩토리 패턴의 주요 요소를 본인의 애플리케이션에 사용하기에 충분할 만큼 살펴보았다. 이번 단락에서는 추상 팩토리 주제에서 변형의 경우들을 이야기하려 하며, 특히 팩토리 객체를 구현하는 여러가지 방법을 논의하고자 한다. 이 변형 중 일부는 [디자인 패턴]에서 다루고 있고, 저자들은 Smalltalk 특정적인 다양성도 몇 가지 추가하였다. 여기서는 각 구현의 장단점을 지적하겠지만, 특별한 가지 해법을 옹호하려는 것은 아니다; 오히려 다른 모든 패턴들과 마찬가지로 다양한 방법으로 구현될 수 있음을 보이는 것이 목표다. 선택권은 특정 애플리케이션과 다른 객체들과의 상호작용으로 인해 부과되는 추가 제약이나 개인의 선호도 및 미학에 따라 좌우된다. 크리스토퍼 알렉산더(Christopher Alexander)와 그 동료들은, 패턴이 특정 문제에 대한 해법에 관련된 기본 견해와 개념을 제공하기는 하지만 "본인의 선호도와 지역적 상태에 대해 조정함으로써 자신만의 방식으로" 실현시킬 수 있다고 주장했다 (Alexander et al., 1977, p. xiii).
자동차 팩토리에 대한 바닐라 구현
예제로 보여 준 애플리케이션에서 CarPartfactory 는 모든 자동차 factory 에 대한 인터페이스를 정의한다. 대안 팩토리는 CarPartFactory 의 하위클래스로 정의되며, 각 대안 팩토리는 적절한 부품 생성 메서드를 오버라이드한다. 가장 간단히 말해 바닐라 구현에서 이러한 메서드 각각은 다음과 같이 인스턴스화하고 반환하기 위해서 클래스를 하드코딩(hard-code)한다.
FordFactory>>makeEngine
^FordEngine new
PorscheFactory>>makeEngine
^PorscheEngine new
이 접근법은 각 부품에 대한 코드를 지역화(localize)한다. 물론 메서드를 지역화하는 것은 나중에 변경된 내용을 지역화 하는 것을 의미한다.
Toyota 가 Ford 로부터 엔진을 구매하기 시작한다고 가정하자(자동차 산업에서는 실제로 이렇게 희한한 일이 발생하고 있기도 한다!). 새로운 유형의 엔진은 FordEngine 인스턴스를 반환하는 ToyotaFactory>>makeEngine 메서드만 변경하는것을 의미하며, 클라이언트 코드는 변경되지 않은 채로 유지된다.
Constant Method 해법
앞에서 살펴본 구현은 가장 간단한 형태기는 하지만, 그 안에서 모든 factory 클래스의 각 부품에 맞는 각 메서드를 인스턴스화하기 위해 클래스를 하드코딩했다. 이 작업은 CarPartFactory 와 CarPartFactory 의 구체 하위클래스에 makeEngine, makeBody 등의 메서드를 필요로 한다. 이 방법 대신 Factory Method (63) 패턴의 Constant Method 변형 중 하나를 적용시킬 수도 있다. 여기에서는 각각의 부품생성 메서드를 한번만 정의해서 필요한 클래스 객체를 쉽게 인스턴스화 할 수 있도록 Constant Method 를 이용해서 각 factory 클래스가 부품 클래스의 "이름을 만들게" 할 것이다(Beck, 1997).
추상적 상위클래스에서 부품 생성 메서드를 factory 메서드로 재정의함으로써 시작한다:
CarPartFactory>>makeCar
^self carClass new
CarPartFactory>>makeEngine
^self engineClass new
CarPartFactory>>makeBody
^self bodyClass new
그 뒤에 각 factory 클래스에 Constant Method 를 정의한다.
FordFactory>>carClass
^FordCar
FordFactory>>engineClass
^FordEngine
FordFactory>>bodyCalss
^FordBody
PorscheFactory>>engineClass
^PorscheEngine
PorscheFactory>>bodyClass
^PorscheBody
이제 코드를 모듈화시켰으므로 새로운 자동차 팩토리 클래스가 정의되면 그에 상응하는 부품 클래스만 이름을 정하면 된다. 그러나 자동차 부품 예제의 경우 이것과 바닐라 구현 간의 차이는 그다지 크지 않다.
부품 카탈로그 해법
앞에서 설명된 구현들의 경우, 자동차의 각 부품은 factory 클래스 내의 고유 메서드를 필요로 한다 (예: makeBody, makeEngine). 이는 fatory 내에 메서드의 수를 넘치게 생성하도록 유도하기도 한다. 자동차에 새로운 유형의 부품, 즉 새로운 유형의 고급 오디오 시스템을 추가하려면 CarPartFactory 와 그에 해당하는 모든 하위클래스에 새로운 makeDeluxeCDAudioSystem 메서드를 추가해야 할 것이며, 이런 작업을 통해 factory 클라이언트가 알아야 하는 인터페이스를 확장할 수 있을 것이다.
[디자인 패턴] 에서는 Smalltalk 에서 클래스는 일급 객체first-class[6]라는 사실을 이용해서 이 문제를 (DP90과 그 다음 문제) 해결하는 방법을 설명하고 있다. 자동차 부품의 클래스를 "부품 카탈로그part catalog"로 저장하고 부품마다 하나씩 메서드를 만드는 것이 아니라 파라미터화된 유일한 부품 생성 메서드를 가진 CarPartFactory 를 구현한다.
이러한 접근법은 CarPartFactory 클래스에 partCatalog 인스턴스 변수를 추가하고 이것을 Dictionaryㅡ여기서 키key는 부품 유형이 (Symbol과 같은) 되며 해당 값value은 적절한 클래스가 된다ㅡ로서 초기화하는 과정을 동반한다. FordFactory 클래스의 partCatalog 는 다음과 같다:
Key | Value |
#Car | <the FordCar class object> |
#Engine | <the FornEngine class Object> |
#body | <the FordBody class object> |
... | ... |
이 방식의 접근법을 가능하게 하는 새로운 CarPartFactory 클래스 정의와 메서드를 다음 내용에서 소개한다:
Object subclass: #CarPartFactory
instanceVariableNames: 'PartCatalog'
classVariablesNames: ''
poolDictionaries: ''
CarPartFactory class>>new
^self basicNew initialize
CarPartFactory>>initialize
partCatalog := Dictionary new
하위클래스는 고유의 부품 카탈로그 버전을 구축하기 위해 추상적 상위클래스에서 선언된 initialize 메서드를 다음과 같이 오버라이드하게될 것이다:
FordFactory>>initialize
super initialize.
partCatalog
at: #car put: Fordcar;
at: #body put: FordBody;
at: #engine put: FordEngine;
...
^self
PorscheFactory>>initialize
super initialize.
partCatalog
at: #car put: PorscheCar;
at: #body put: PorscheBody;
at: #engine put: PorscheEngine;
...
^self
이제 추상 상위클래스에 부품 생성을 위해 값(객체)를 반환하기 위한 단일 메서드(single method)를 정의한다. 이는 부품의 유형part type을 argument 로 받는다.
CarPartFactory>>make: partType
"Create a new part based on partType."
| partClass |
partClass := partCatalog at: partType ifAbsent: [^nil].
^partClass new
이제 자동차의 factory 클라이언트는 (예: CarAssembler) 이렇게 만들어진 단일 메시지를 이용해 모든 부품을 생성해 낼수 있다. 부품 생성 코드는 다음과 같은 모습이 아니라,
anAutoFactory make: #engine.
anAutoFactory make: #body.
다음과 같을 것이다.
anAutoFactory makeEngine.
anAutoFactory makeBody.
부품 카탈로그 접근법을 사용하면, CarPartFactory 계층구조의 구체적 클래스는 고유한 부품 카탈로그를 초기화시키는 단일 메서드만 정의하면 된다. 그 외의 부품 생성 동작은 CarPartFactory 의 추상 클래스에 정의된다.
다른 부품 카탈로그를 위한 또 다른 구현
"부품 카탈로그" 접근법을 고려하는 경우, 다른형태의 factory 를 구현하는 또 다른 방법이 있다. 이 경우에도 대체 factory 제공을 위해 서브클래싱(subclassing)을 사용하기 때문에 FordFactory, ToyotaFactory, PorscheFactory 중 하나를 클라이언트에서 선택할 필요가 있다. 하지만 부품 카탈로그는 각 factory 클래스의 모든 인스턴스에서 동일해야 하기 때문에 partCatalog 를 상위클래스내에서 인스턴스 변수로 정의하고, 클래스에서 선언된 변수를 적절히 초기화하기 위해, 각각의 factory 하위클래스에서 사용될 클래스 메서드를 코딩하며, 각 하위클래스마다 초기화 메서드를 한번씩 호출하도록 할 수 있다.
여기서 추상클래스는 다음과 같이 재정의한다:
object subclass: #carPartFactory
instanceValiableNames: ''
classVariablesNames: ''
poolDictionaries: ''
CarpartFactory class
instanceVariableNames: 'partCatalog'
CarPartFactory class>>make: partType
"We moved this method; it's a class method now."
| partClass |
partClass := partCatalog at: partType ifAbsent: [^nil].
^partClass new
이제 CarPartFactory의 구체적 하위클래스마다 하나씩의 클래스 인스턴스 변수가 있다. 모든 하위클래스가 내용을 공유하는 클래스 변수와는 달리, 각 하위클래스는 해당 클래스 인스턴스 변수에 대해 고유의 private 버전을 가지게 된다. 따라서 FordFactory 의 partCatalog 를 초기화한다고 해도 ToyotaFactory 의 partCatalog 에는 영향을 미치지 않는다. 각 클래스의 카탈로그는 다음과 같이 초기화할 수 있다:
CarPartFactory class>>new
partCatalog isNil ifTrue: [self initialize].
^self basicNew
CarPartFactory class>>initialize
"Initialize the part catalog. This is now a class method"
partCatalog := Dictionary new
FordFactory class>>initialize
"Initialize the *local* part catalog"
super initialize
partCatalog
at: #car put: FordCar;
at: #bodyt put: FordBody;
at: #engine put: FordEngine;
...
이제 남은 일은 인스턴스 변수 대신에 각 하위클래스 고유의 클래스 인스턴스 변수를 사용하기 위해 부품 생성 인스턴스의 단일 메서드를 재정의하는 것이다. 이 메서드가 하는 일은 동일한 서명(signature)으로 클래스 메서드를 호출하는것 뿐이다:
CarPartFactory>>make: partType
"Create a new part based on partTyp"
^self class make: partType
그럼에도 불구하고 DP 90 페이지에서 지적한 바와 같이 이 접근법은 제품군마다 새로운 구체 factory 하위클래스를 필요로 한다 (Fords 에 하나, Toyotas 에 하나). 메시징 인터페이스를 단일 메서드로 줄이긴 했지만 팩토리 클라이언트들은 여전히 여러 개의 팩토리 클래스들 중 인스턴스화해야 할 클래스를 선택해야 한다. 다만 유연성은 약간 감소될 수 있다; 새로운 제품군이 생길 때 새로운 하위클래스를 추가하는 것은 확실히 간단한 작업이지만, 그럼에도 좀 더 작은 단일-클래스 구현을 만들 수도 있다.
단일 factory 클래스
단일-클래스 해법은 하나의 factory 클래스만 구현해서, 인스턴스화하기 적절한 부품 클래스를 이용해 새로 생성되는 각 인스턴스의 부품 카탈로그를 초기화하는 것이다(partCatalog 를 인스턴스 변수로 사용하는 작업으로 돌아왔다). 다시 말해, CarPartFactory 는 어떠한 하위클래스도 가지지 않으며, 클라이언트들은 항상 CarPartFactory 자체를 인스턴스화한다.
이 접근법에서는 프로그래머가 CarPartFactory 안에 단순한 클래스 메서드를 구현해야 하며, 각 메서드마다 의미를 지니는 factory 생성 이름을 가진다. 단일 factory 의 각 메서드는 팩토리의 유형마다 필요한 초기화를 수행한다. 이는 다음과 같은 형태를 가진다:
CarPartFactory class>>fordFactory
"Create and return a new Ford factory"
| catalog |
catalog := Dictionary new.
catalog
at: #car put: FordCar;
at: #engine put: FordEngine;
...
^self new partCatalog: catalog
CarPartFactory class>>porscheFactory
"Create and return a new Porsche factory"
| catalog |
catalog := Dictionary new.
catalog
at: #car put: PorscheCar;
at: #engine put: Porscheengine;
...
^self new partCatalog: catalog
CarPartFactory 클래스를 사용할 클라이언트가 Ford factory 를 생성하고자 할 경우 다음과 같은 메시지 전송이 이루어진다:
carFactory := CarPartFactory fordFactory.
부품은 정확히 이전과 동일하게 생성된다:
carFactory make: #engine.
교묘한 단일-클래스 구현
사용자는 (1) 일관된 명명규칙에 따라 우리의 모든 클래스를 정의하였고, (2) Smalltalk 는 반영적 특성reflective을 가진다, 라는 두 가지 사실을 이용하는 접근법을 통해 또 다른 단일-클래스 팩토리를 구현할 수 있다. 모든 부품 클래스 이름은 자동차 제작회사의 이름을 접두사로 하고 부품 이름을 접미사로 한다: FordEngine, ToyotaEngine, PorscheEngine; FordCar, ToyotaCar, PorscheCar. 이 규칙을 다음과 같이 이용할 수 있다:
CarPartFactory>>makeCar: manufacturersName
"manufacturesName is a Symbol, such as
#Ford, #Toyota, or #Porsche."
| carClass |
carClass := Smalltalk
at: (manufacturersName, #car) asSymbol
ifAbsent: [^nil].
^carClass new
carPartFactory>>makeEngine: manufacturersName
| engineClass |
engineClass := Smalltalk
at: (manufacturersName, #Engine) asSymbol
ifAbsent: [^nil].
^engineClass new
어떻게 이런 방식이 가능하나면, 모든 전역변수는 Smalltalk Dictionary 의 항목으로 참조되며, 클래스는 클래스와 동일한 이름의 global 로 참조되기 때문이다. 따라서 Smalltalk at: #FordEngine이라는 표현식(expression)은 FordEngine 클래스 객체를 가져온다(retrieves).
이 접근법을 이용해 자동차 factory 는 항상 CarPartFactory 의 인스턴스가 된다. 자동차 부품 생성은 다음 형식을 취한다 (여기서 carCompany 는 사용자로부터 얻은 Symbol이다):
carFactory := CarPartFactory new.
car := carFactory makeCar: carCompany.
car
addEngine: (carFactory makeEngine: carCompany);
...
그러나 방법에는 단점이 있다. 정적 검사(static inspection)또는 동적 런타임 추적(dynamic runtime trace)으로부터 코드를 이해야기가 힘든데, 코드는 참조로 만들어지며 해당 identity 는 즉석에서 생성된 클래스로 메시지를 보내기 때문이다. 예를 들어, FordCar 클래스를 참조하는 메서드를 모두 찾기 위해 Smalltalk 개발환경의 도구를 사용한다면, makeCar: 메서드는 검색 결과로 나타나는 메서드 목록에 포함되지 않을 것이다. makeCar: 는 모든 자동차 클래스를 암묵적으로 참조하지만 코드에 명시적으로 표현되어 있지는 않기 때문이디.
알려진 Smalltalk 사용예
UILookPolicy
VisualWorks 에서 UIBuilder 는 위젯의 특징들을 바탕으로 윈도우를 구성하는 책임을 가진다 (다른 기능들도 있지만). 각 위젯마다 UIBuilder 는 실제 위젯 인스턴스 생성을 수행하기 위해 그와 관련된 UILookPolicy 를 요청한다. UILookPolicy 의 서로 다른 하위클래스들이 최근 선택된 룩앤필(look and feel)에 따라 (윈도우, OS/2, Motif, Macintosh 또는 기본 Smalltalk 모양) 서로 다른 위젯을 인스턴스화시키기 위해 이러한 생성 메시지를 다형적으로 구현한다. 하지만 UIBuilder 는 그것이 다루는 모양 정책 객체(look policy object)가 무엇인지에 대한 지식이 없다. 다시 말해 UIBuilder 는 Win3LookPolicy, CUALookPolicy 또는 다른 정책 객체(policy object)와 연결되어 있는지 알지 못하거나 신경쓰지 않는다는 의미이며, 단지 추상적 UILookPolicy 프로토콜에 정의된 일반적 위젯 생성 메시지를 전송할 뿐이다. 따라서 모양 정책 객체는 위젯에 대한 추상 팩토리 역할을 한다.
Constant Method 이용하기
Constant 메서드를 사용해서 추상 팩토리를 구현하는 방법은 이미 소개한 바 있다. UILookPolicy 에 의한 사용 예제도 제시되었다 (팩토리 메서드에서도 이 예제를 취급한다). 예를 들어, UILookPolicy 에 슬라이더 위젯을 생성하는 메서드가 있다고 가정하자.
slider: spec into: builder
...
component := self sliderClass model: model.
...
model: 메시지는 sliderClass가 반환시킨 클래스를 인스턴스화한다. UILookPolicy 는 sliderClass 의 기본(default) 버전을 정의하며, 그의 구체 하위클래스 일부는 슬라이드를 각각 다른 모양으로 생성하기 위해서 그것을 오버라이드한다:
UILookPolicy>>sliderClass
^SliderView
MacLookPolicy>>sliderClass
^MacSliderView
Win3LookPolicy>>sliderClass
^Win3SliderView
관련 패턴
Builder
추상 팩토리는 Builder(47) 패턴과 밀접하게 관련되어 있다. 주요 차이점은 전체적인 제품 조립을 누가 책임지는가에서 온다. 추상 팩토리에서는 함께 작업하거나 함께 작동하도록 보장된 부품을 팩토리가 반환한다. 하지만 하나의 최종 제품으로 조립하는 일은 factory 클라이언트가 한다. factory 로 전송되는 모든 메시지는 전체적인 최종 제품의 하위컴포넌트 또는 새로운 부품등의 결과를 가져오며ㅡ즉 factory 안의 모든 부품 생성 메서드는 컴포넌트 부품을 반환한다는 의미ㅡfactory 의 클라이언트는 이 부품을 각각 생성되는 제품에 추가한다. 반면, Builder 는 제품이 조립되는 동안 제품을 유지할 수 있는 고유의 내부 상태를 가진다. Builder 가 "컴포넌트 A를 추가한다," "컴포넌트 B를 추가한다,"라는 요청을 듣게 되면 이 하위컴포넌트를 캡슐화된 Product 로 추가하는 것은 Builder 의 일이 된다. "컴포넌트 X를 추가하라" 라는 메시지가 전송될때에도 Builder 는 아무 것도 반환하지 않는다. Builder 의 클라이언트가 하위부품의 추가작업을 완료하면 클라이언트가 Builder에게 "최종 제품을 보여달라,"고 말한다. 그때서야 Builder는 최종 제품을 반환한다.
Factory Method
factory 객체는 어떤 클래스를 인스턴스화할지 결정하기 위해 팩토리 메서드(63) 패턴의 Constant 메서드 변수를 호출할 수 있음을 앞에서 알아봤다. 그리고 팩토리 메서드는 실질적으로 추상 팩토리의 경쟁자라고 볼 수 있지만, 두 패턴은 어떻게 보면 서로 구조적으로 정반대에 해당한다. 추상 팩토리의 바닐라 구현을 살펴본 본문에서 다수의 부품군ㅡ제품군마다 하나의 클래스ㅡ를 고려하기 위해 다수의 구체 factory 클래스를 정의하고 있다. 그 뒤에 단일 애플리케이션 객체가 동일한 코드를 이용해 어떤 제품군으로부터든 부품을 인스턴스화시킬 수 있도록 하였다. 애플리케이션은 바람직한 부품군과 관련된 factory 클래스를 인스턴스화시킬 뿐이다 (Ford 부품을 원하면 FordFactory 를 인스턴스화하는 것이다). 구조 다이어그램에서 단일 클라이언트는 factory 클래스의 하위계층구조로부터 하나의 factory 객체를 가리킨다는 점을 명심한다.
팩토리 메서드 접근법을 이용할 경우 factory 클래스의 전체 계층구조를 정의하는 작업을 피할 수 있다. 하지만 그 대신 애플리케이션 클래스의 계층구조가 필요할 것이다. 모든 구체적 하위클래스에 대한 엄청난 양의 동작을 정의하면서도 factory 메서드의 정의는 하위클래스로 미루는 추상 애플리케이션 클래스가 필요할 것이다. 각 하위클래스는 그에 따라 factory 메서드를 오버라이드할 것이다: Ford 애플리케이션 클래스는 해당되는 factory 메서드가 Ford 부품을 반환하도록 시킬 것이며, Toyota 와 관련된 애플리케이션 클래스는 Toyota 부품 객체를 생성 및 반환하는 factory 메서드가 있을 것이다.
Notes
- ↑ 추상 팩토리 패턴 이후에 나오는 Builder (47) 패턴을 읽어보길 권한다; 두 패턴은 밀접한 관계가 있으며 여러 문제를 공유한다.
- ↑ 이 점은 팩토리의 부품 자체가 복잡하게 구성된 부품일 경우에도 마찬가지다. 예를 들어, 팩토리는 여러 개의 하위컴포넌트 위젯들이 통합된 복합 판유리와 같이 미리 조립된 부품을 반환할 수도 있는 것이다. 그럼에도 불구하고, 팩토리 클라이언트에게는 이것이 창문처럼 좀 더 복잡한 제품으로 통합될 수 있는 하나의 부품으로 간주된다.
- ↑ 언뜻 보면 조립 작업 과정의 최종 제품인 새 Car 를 생성하는 factory makeCar 를 말함으로써 자동차 조립 공정을 시작하기가 혼란스러워 보인다. 사실 이 방법은 복합 객체를 구축하는 전형적인 방법이다. 예를 들어 Visual Smalltalk 애서는 Menu 의 구성을 시작할 때 Menu new 라고 말한다. 이 시점에서 사용자가 가진 것은 메뉴의 껍데기(shell)에 불과하며, MenuItems 라는 하위컴포넌트를 추가하기 전까지는 제대로된 메뉴의 기능을 하지 못한다. 이와 비슷하게, factory makeCar 는 엔진이나 차체처럼 사용자가 추가해야 하는 컴포넌트로소 Car 의 빈 껍데기를 반환한다.
- ↑ 타 언어의 if 등의 비교 조건문
- ↑ 다음의 코드에서 FordFactory 는 CarPartFactory 의 하위 클래스이다.
- ↑ https://ko.wikipedia.org/wiki/일급_객체