DesignPatternSmalltalkCompanion:Facade
FACADE (DP 185)
의도
하위시스템에 있는 인터페이스 집합에 대해서 하나의 통합된 인터페이스를 제공한다. Facade 패턴은 하위시스템을 더 쉽게 사용할 수 있도록 높은 수준의 인터페이스를 정의한다.
구조
논의
객체지향 기법들의 가장 큰 장점들 중 하나는 거대한 체제의 복잡성을 감소시킨다는 점이다. [디자인 패턴]편에 소개된 패턴의 다수는 특히 복잡성을 관리하는 법을 강조하고 있으며, Facade도 그에 속한다. 유연성과 확장성을 위해 고안된 하위시스템은 "틀에서 벗어난 상황" 에서는 사용하기가 힘든데, 사용하기 전에 프로그래머들이 하위시스템 내에 모든 클래스를 이해해야 한다고 생각하기 때문이다. Facade 패턴은 하위시스템의 전체적인 범용성과 관련된 복잡성을 걱정하지 않고 공통된 사용법 사례를 처리하는 간소화된 인터페이스를 제공함으로써 문제를 해결한다.
이 패턴에서 핵심은 하위시스템에 있는 객체로의 통일된 접촉지점을 제시하는 Facade 객체를 들 수 있다. Facade는 기반이 되는 클래스를 클라이언트에게 숨기면서 하위시스템으로 인터페이스를 제공한다.
파일 시스템 Facade
패턴의 관점을 고려하기 전에 먼저 실생활의 사용예를 살펴보자. 언젠가 작업을 했던 한 애플리케이션에서 다른 애플리케이션에게 파일 시스템의 구현을 숨기기 위해 Facade 패턴을 사용하였다. 애플리케이션은 Objectworks (비주얼웍스 이전에 ParcPlace의 모델)에 구현하였고 벤더의 파일 처리 프레임워크를 광범위하게 사용하였다. 문제는 벤더가 행위를 더 잘 분배하고 인터페이스를 사용하기 쉽도록 만들기 위해 이러한 프레임워크를 계속해서 리팩토링했다는 점이다. 이러한 개선이 많은 경우에서는 유용했지만 파일 시스템을 직접적으로 평가하는 코드가 많았기 때문에 우리 애플리케이션에 큰 혼란을 초래했다. 벤더가 인터페이스를 변경할 때마다 코드는 새로운 인터페이스로 이동해야 했다ㅡ이는 시간 소모가 많고 오류 발생이 쉽다. 더 나은 방법이 있음이 확실했다.
이 문제를 해결하기 위해 벤더의 파일 처리 프레임워크에 Facade 역할을 하는 FileSystem 클래스를 개발했다. 단순화된 인터페이스를 제공하여 공통된 여러 단계의 연산을 단일 메시지로 통합하였다 (절차성 추상화의 예제ㅡ비객체지향 언어에서도 가능하나 객체지향 프로그래머의 툴킷에 또 다른 중요한 도구임):
벤더의 프레임워크로 적절하게 작업을 위임하기 위해 Facade 패턴을 구현하였다. 마지막으로 파일 서비스에서 Facade를 사용해야 하는 애플리케이션 일부를 변환하여, 파일 시스템이 하나의 객체에 구현된 것처럼 보이도록 만들었다. 그리고 난 후 벤더가 파일 처리 프레임워크를 수정할 때마다 우리가 새 인터페이스에 FileSystem만 이동시키면 나머지 애플리케이션은 문제없이 작동했다.
FileSystem은 하나의 클래스가 다른 여러 객체들을 통합시켜 마치 하나의 객체처럼 작동하는 가장 단순한 형태의 Facade를 보여준다. Facade는 많은 행위를 제공하며 많은 작업을 하는 듯 보이지만 정작 하는 일은 대부분 작업을 다른 객체들에게 맡기는 업무이다.
Facade는 하위시스템 객체를 클라이언트로 리턴하는 것을 피한다는 점을 주목한다. 그 결과, 클라이언트는 하위시스템과 그 복잡한 내용을 알지 못하며 그 지식은 Facade에게 맡긴다. Facade는 훌륭한 사무 보조원과 같다: 업무를 해결하도록 요청하면 당신은 내용을 상세히 알 필요 없이 그가 처리하는 것이다. 하지만 대부분 Facade는 그 하위시스템을 완전히 숨기지 못한다. 예를 들어, FileSystem에서 readStreamOnFilename: 은 읽기 쉬운 스트림을 반환하고, asValidFilename:은 Filename을 반환할 수 있다. 두 개의 반환 값은 하위시스템에 의해 정의된다.
클라이언트는 그들의 메시지의 최종 수신자를 알지 못하므로 메시지가 Facade 인터페이스에서 종종 이름이 바뀌는 경우가 있다. “실제” 수신자는 주로 메시지의 첫 argument가 된다. 예를 들어, String>>asValidFilename이 클래스와 프로토콜에게 파일명의 타당성을 검사하도록 요구한다. FileStream Facade는 이러한 상세 정보를 숨길 수는 있지만 파라미터와 같이 문자열이 있는 이름 asValidFilename: 으로 메시지 이름을 변경한다. Facade는 하위시스템을 이해하는 복잡성을 줄이지만 Facade가 수반하는 추가적인 간접 지정(indirection) 수준은 감당해야 할 문제이다.
분산 객체 시스템에서의 Facade
복잡성의 감소는 Facade 패턴을 사용하는 주요 동기이다. GemStone이나 ParcPlace Distributed Smalltalk와 같은 분산 객체 시스템에서도 또 다른 동기가 발생한다. 이러한 제품들은 본질상 논리적 이미지를 두 개의 (또는 그 이상의) 구분된 부분으로 나눈다: 한 이미지는 사용자에게 정보를 표시하는 "클라이언트"를 나타내고, 다른 하나는 클라이언트가 요청한 정보를 처리하는 "서버" 이미지이다. 개발자들은 때때로 분산을 고려하지 않고 이와 같은 시스템을 설계하는데, 특히 초기에는 더 그러하다. 이후에 그들은 로컬 공간에 있는 객체에 적절한 설계가 분산 객체 공간에는 적절하지 않다는 사실을 깨닫는다.
예를 들어, 로컬 스몰토크 시스템에 사용되는 ApplicationModel은 다른 많은 도메인 객체와 협력할 수 있다. 분산 시스템에서는 원격 메시지의 네트워크 오버헤드가 그러한 설계를 바람직하지 못하게 만들 수도 있다. ApplicationModel이 Employee의 집합체에 정보를 문의한다고 가정하자. Employee들이 한 기계에 있고 ApplicationModel이 다른 기계에 있을 경우 결론적으로 네트워크 트래픽이 틀림없이 금지될 것이다. 공통된 해법에서는 Employee의 그룹과 협력을 처리하는 Facade를 도입하는 것이다. 이는 클라이언트와 메모리 공간에 있는 Proxy와 원격 시스템에 남아 있는 Facade와 함께 원격 Facade를 위해 Proxy를 포함하기도 한다. 이러한 방식을 통해 총 요구되는 Proxy 수가 감소되며 각 상호작용에 필요한 네트워크 전송 수도 줄어든다:
Gemstone에서는 데이터베이스로부터 클라이언트 이미지로 로드되거나 "faulted" 된 총 객체 수를 감소시키는데 위와 같은 동기를 적용할 수 있다. 클라이언트 측에서 서버 측으로 실행장소를 옮기는 행동에는 많은 이점이 있다. 네트워크 트래픽 수를 감소시킬 뿐 아니라 클라이언트 측에서 애플리케이션이 필요로 하는 공간의 양 또한 감소시킬 수 있다.
다수의 Facade
[디자인 패턴] (DP 193) 편에서는 "주로 하나의 Facade 객체만 필요로 한다. 따라서 Facade 객체들은 Singleton인 경우가 보통이다." 이를 읽으면 시스템은 하나의 Facade만 가질 수 있다고 추론할 수 있지만 사실은 하위시스템 당 대개 하나의 Facade가 있음을 의미한다. 시스템에 다수의 하위시스템이 있는 경우 시스템은 다수의 Facade를 한꺼번에 쉽게 사용할 수 있다.
예를 들어 Smalltalk Communications/Transactions 특징에 대한 VisualAge는 함께 작용하는 다수의 Facade의 예를 포함한다. 이 특징은 다른 커뮤니케이션 프로토콜에 대해 인터페이스를 구현하기 위해 VisualAge 부분들과 클래스 집합을 제공한다. VisualAge는 당신이 FCP/IP, NetBIOS, APPC, MQSeries, 그 외 여러 프로토콜 중 선택할 수 있게 해준다. 이들 각각은 구분되고 호환성이 없는 C API를 가진다. 하지만 VisualAge는 이들 각각의 복잡성과 차이점을 은닉하는 Facade를 통해 각각에 공통된 인터페이스를 제공한다.
예를 들어, AbtSocket은 TCP/IP Socket을 의미하고, AbtAPPCConversation은 APPC Conversation을, 그리고 AbtMQQueue는 MQSeries queue를 나타낸다. 이러한 클래스들은 단순화된 facade 뒤에 이러한 특정 프로토콜을 구현하는 클래스의 수를 숨긴다. VisualAge에서 이러한 클래스들은 추상적 통신 대화를 위한 public 프로토콜을 공유한다. 각 클래스는 다음 메서드에 응답한다:
- connect-이 부분을 원격 서버와 연결한다.
- disconnect-대화창을 닫고 서버와 연결을 끊는다.
- sendBuffer-정보를 포함한 AbtBuffer를 서버로 전송한다.
- receiveBuffer-서버로부터 수신한 정보를 포함하는 AbtBuffer를 반환한다.
이는 부분적 예제일 뿐 전형적인 사레는 되지 못한다. 이러한 메시지들이 특정 프로토콜에 의해 어떻게 처리되는지에 관한 자세한 내용은 Facade 뒤에 완전히 숨겨져 있다. 흥미로운 점은 Facade 모두 공통된 인터페이스를 공유하므로 서로 대체가 가능하다는 것이다. (디자인 패턴 (DP 88)은 첫 구현 지점에서 다수의 Facade 서브클래스를 논한다.)
이는 특정 통신 프로토콜의 특성에 의존하지 않는 클라이언트 소프트웨어를 쓸 수 있게 해준다. 예를 들어, APPC를 위한 클라이언트를 쓴 후에 최소의 레코딩을 가진 TCP/IP를 이용하도록 전환할 수 있다. 또한 다수의 통신 프로토콜을 사용하는 클라이언트를 쓰는 것도 가능하다; 당신은 APPC와 TCP/IP에 의한 통신을 동시에 하는 시스템을 쓸 수 있게 된다.
Facade 의 흔한 실수
초보 설계자들이 Facade 패턴을 적용할 때 하는 실수가 몇 가지 있다. 첫 번째는 하위시스템에 새 행위를 추가할 때 Facade 클래스의 서브클래싱과 관련된 문제이다. 프로그래머들의 이러한 실수는 Facade 패턴의 의도를 이해하지 못할 때 발생한다. Facade는 하위시스템에 새로운 행위를 추가해선 안된다; 하위시스템에 집중화되고 단순화된 접근만 제공할 뿐이다. Facade가 충분한 행위를 제공하지 않을 경우 확장되거나 서브클래스화될 수 있으나, 단 하위시스템에 바람직한 행위가 이미 있을 경우에만 가능하다. 그렇지 않을 경우 Facade가 아닌 하위시스템이 고정(fixing)을 필요로 한다. 새로운 행위가 하위시스템 클래스에 먼저 추가된 후 Facade에 추가되어야 한다.
두 번째로 흔히 범하는 실수는 (첫 번째 실수와 밀접한 관련이 있는) Facade 클래스를 Manager 클래스로 변하는 것이다 (Mediator (287)의 Manager 클래스에 관한 단락을 참조). Facade는 스스로 더 많은 기능을 구현시킴으로써 하위시스템의 일을 맡기 시작한다. 그 결과 크고 복잡한 클래스가 되며, 이해와 유지가 힘들어지는 경향이 있다.
Facade의 메서드는 수가 많지 않아야 하며 간결해야 한다. Facade가 하위시스템에 아날로그가 없는 메서드를 나타내기 시작하면 그러한 책임을 만족시키도록 하위시스템을 변경하라.
ENVY/Developer 하위애플리케이션
[디자인 패턴] (DP 188) 편에서는 FAcADE 패턴이 클래스 간 컴파일 의존성을 감소시킴으로써 시간을 절약한다고 설명한다. 스몰토크는 C++와 같은 파일기반의 컴파일 언어가 아니기 때문에 이는 스몰토크 프로그래머에게는 문제가 되지 않는다. 하지만 스몰토크에서 이와 관련된 문제 중 Facade 패턴을 사용 시 해결할 수 있는 경우가 있다.
많은 스몰토크 개발 프로젝트들은 ENVY/Developer를 사용하는데, 이는 아마도 스몰토크에 사용되는 가낭 유명한 코드 관리 시스템일 것이다. ENVY에 소수 코드 통제의 주요 유닛은 application 이라 알려진 클래스의 논리적 그루핑(logical grouping)이다. 다음 다이어그램이 나타내듯 애플리케이션은 애플리케이션 내의 클래스의 논리적 그루핑이기도 한 subapplication와 클래스를 모두 포함한다. 하위애플리케이션은 결국 더 많은 하위애플리케이션과 클래스를 얻을 수 있다:
Loading이라는 프로세스는 애플리케이션들을 ENVY 리포지토리로부터 ENVY 이미지로 가져온다. 로드는 최소단위의 실행이므로 애플리케이션의 클래스와 메서드 모두 완전히 로드되거나 아예 로드되지 않음을 의미한다. 오류가 발생하여 로드가 실패할 경우 클래스는 부분적으로 로드되지 않거나 자신이 의존하는 클래스 없이는 로드되지 않을 것이다. ENVY는 실패한 클래스 로드로 인해 절대 비일관적 상태가 되지 않도록 보장한다.
애플리케이션이 로드되면 그것의 하위애플리케이션을 각각 로드하고자 시도한다. 각 하위애플리케이션에는 구성 연산식(configuration expression)이라 불리는 부울 연산식(Boolean expression)이 있는데, 이는 하위애플리케이션이 언제 로드되어야 하는지를 결정한다. ENVY는 구성 연산식이 true로 평가되는 서브클래스를 모두 로드시킨다.
개발자들은 ENVY 애플리케이션과 Facade 패턴을 결합하여 플랫폼 의존적 또는 방언 의존적인 스몰토크 시스템을 구현할 수 있다. 이를 위해선 플랫폼 특유의 코드와 방언 특유의 코드를 분리시켜 Facade 뒤에 위치시켜 하위애플리케이션에 저장해야 한다. 그리고 나면 코드는 각 대안적 플랫폼 또는 방언마다 구현되어야 하고, 고유의 Facade 내부에 감싸지며 고유의 하위애플리케이션에 저장되어야 한다. Facade는 모두 동일한 인터페이스를 가지므로 서로 교체가 가능하며, 하위애플리케이션도 마찬가지이다. 나머지 시스템은 분리된 코드로 Facade를 통해 상호작용하여 그것이 어떤 구현을 사용하고 있는지 알지 못한다. 이런 방식으로 ENVY는 구성 연산식을 이용해 적절한 구현을 위한 하위애플리케이션을 선택하고 로드한다. 그 동안 나머지 시스템은 플랫폼 또는 방언 의존적으로 남아 있다.
여기서 한 가지 예를 들어보겠다. 여러분이 여러 이미지 형식으로 변환해주는 그래픽 변환 프로그램을 구축하고 있다고 가정해보자. 이 프로그램은 스몰토크의 방언들 간 동일한 메서드를 사용하는 다수의 Stream 조작으로 구성되어 있을 것이다. 하지만 프로그램을 세 가지 방언으로 구현하고자 할 때 문제가 될 수 있는 몇 가지 방언 특정적 차이가 있다.
파일 열기, Stream으로 파일 첨부하기, 파일 닫기에 관한 세부 내용은 VisualAge, VisualWorks, Visual Smalltalk에서 모두 다르다. 하지만 Stream에서 바이트를 조작하는 메서드는 (next:, upTo:, nextPut:) 기본적으로 방언들 간 동일하다. 한 방언에서 다른 방언으로 쉽게 프로그램을 복사하고자 할 경우 방언 특유의 부분을 FileHandlingFacade라는 이름의 클래스 뒤로 분리시킬 수 있다. 이 클래스는 openFileNamed:, closeFile, fileStream과 같은 메서드를 이해할 수 있다. 당신은 방언마다 클래스의 버전을 하나씩, 총 3개를 구축한다. 각 클래스 버전에 대한 코드는 상당히 다르다; Facade 클래스의 public 프로토콜만이 그대로 유지된다.
그리고 나서 여러분은 3가지 클래스 버전에 대해 각 방언마다 ENVY에 3개의 하위애플리케이션을 구성할 것이다. 각 방언마다 다른 구성 연산식이 있을 것이다ㅡ각 연산식은 VM 타입을 확인하고 적절한 하위애플리케이션을 선택한다. 일치하는 타입만이 true로 평가될 것이고, 이에 해당하는 하위애플리케이션에 로드될 것이다.
구현
[디자인 패턴]에 언급된 구현 문제에 더해 고려해야 할 문제가 두 가지 더 있다:
- 메시지가 복잡한 실행을 수행한다. Facade의 인터페이스를 설계할 때 그것은 스스로를 팩토리로 사용하도록 부추겨 클라이언트에게 올바른 객체를 반환하고자 한다. 그렇다면 클라이언트는 어떤 타입의 객체가 반환되는지와 원하는 행위를 얻기 위해 어떻게 이 객체와 협력해야 하는지를 알아야 한다. 이번 사례에서는 Facade가 하위시스템의 복잡성을 그다지 잘 숨기지 못하고 있다. 클라이언트가 원하는 것을 Facade에게 전해주고 Facade가 일을 수행하도록 둔다면 클라이언트는 훨씬 단순해질 것이다.
- 수신자가 첫 파라미터가 된다. 메시지의 수신자인 Facade는 더 이상 그렇게 중요하지 않기 때문에 메시지는 Facade로 이동하면서 변형된다. 따라서 본래 수신자는 메시지의 첫 argument가 된다. 예를 들어, 앞서 설명한 바와 같이 String>>asValidFilename은 FileSystem>>asValidFilename: aString으로 변한다.
예제 코드
많은 관계형 데이터베이스 프레임워크에서는 데이터베이스 브로커라는 개념을 사용하는데, 이는 관계형 데이터베이스로부터 객체의 회수와 보관을 처리한다. 많은 프레임워크에서는 지원 데이터베이스 하위시스템에 있는 Facade가 브로커이다.
저장할 객체를 관계형 데이터베이스의 단일 행(single row)에 모두 쓸 수 있는 단순한 사례를 고려해보자. 단순화를 위해 객체를 회수하는 것은 고려하지 말고 객체를 저장하는 것만 생각해보자. 우리 프레임워크는 다음과 같은 클래스를 포함할 것이다:
- SQLStatement는 도메인 객체와 ColumnMap가 주어졌을 때 유효한 SQL 문자열을 생성하는 모든 클래스의 추상적 슈퍼클래스이다. 이것은 다음 3개의 서브클래스를 가진다: SQLInsert, SQLUpdate, SQLSelect.
- DatabaseConnection은 관계형 데이터베이스로의 연결을 나타낸다. 데이터베이스로 연결할 수 있고, 연결을 해제할 수 있으며, 유효한 SQL 문자열을 실행할 수 있다.
- ColumnMap은 도메인 객체의 특정 클래스에 대해 인스턴스 변수 이름으로부터 테이블 열 이름까지 매핑을 제공한다.
어떻게 작용하는지 알아보기 위해 DatabaseBroker 안의 save: 메서드를 살펴보자. 이것은 Facade 내의 메서드로서 객체들을 하위시스템 내부로 모은다. Facade의 클라이언트는 이 메서드가 객체들을 데이터베이스에 저장한다는 사실만 알면 된다; 하위시스템에 관한 자세한 내용은 무관하다.
DatabaseBroker>>save: anObject
"Save this object into the database."
|columnMap statement|
columnMap := anObject class columnMap.
statement := (anObject isPersistent)
ifTrue: [SQLUpdate new
fromObject: anObject
columnMap: columnMap]
ifFalse: [SQLInsert new
fromObject: anObject
columnMap: columnMap].
self databaseConnection execute: statement
하위시스템 내의 객체들 간 상호작용의 일부를 관찰하기 위해 SQLInsert>>fromObject:columnMap:의 구현을 살펴보겠다.
SQLInsert>>fromObject: object columnMap: columnMap
"Create an insert statement from this object
and its column map."
| stream |
stream := WriteStream on: String new.
stream
nextPutAll: "INSERT INTO ';
nextPutAll: columnMap tableName;
nextPut: $(.
columnMap columnNames do:
[:name |
stream
nextPutAll: name;
nextPut: $,].
"Eliminate the last comma:"
stream position: stream position - 1.
stream nextPutAll: ') VALUES ('.
(columnMap valuesFrom: anObject) do:
[:value |
stream
nextPutAll: value;
nextPut: $,].
stream position: stream position - 1.
stream nextPut: $).
^stream contents
예제를 끝마치기 전에 ColumnMap의 메서드 중 일부를 살펴보자. ColulmnMap은 데이터베이스 테이블 내 열 이름을 객체 내 해당 값을 가지는 인스턴스 변수의 이름으로 매핑하는 Dictionary를 포함한다:
ColumnMap>>columnNames
"Return the column names for my mapping."
^columnMappings keys
ColumnMap>>valuesFrom: anObject
"Return a collection of the values of the values of the
instance variables that correspond to my columns."
^self columnNames collect:
[:key | anObject perform: (columnMappings at: key)]
위의 단순한 예제를[1] 통해 항목이 어떻게 저장되는지 알 수 있다; 회수는 이보다 좀 더 복잡한 과정이나 동일한 프레임워크 객체의 사용을 수반한다. 하지만 Facade에 사용되는 코드의 복잡성은 저장과 회수에 있어 별반 다르지 않다.
흥미로운 점 한 가지는 DatabaseConnection이 또 다른 Facade가 될 수도 있다는 점이다. VisualWorks와 VisualAge 모두 클래스에 DatabaseConnection의 것과 같은 메서드를 제공한다. 이 둘은 하위시스템 상에 위치한 Facade로서, Oracle이나 OBDC와 같은 좀 더 복잡한 API를 처리한다.
알려진 스몰토크 사용예
스몰토크 벤더들이 수 많은 프레임워크를 제공하는데도 불구하고 Facade는 거의 나타나지 않는다는 사실이 놀랍다. [디자인 패턴] 편에서는 비주얼웍스의 클래스 Compiler를 설명하고 있다. 이것은 파싱과 compilation 클래스에 대해 facade처럼 행동한다. IBM 스몰토크 클래스 EsCompiler와 비주얼 스몰토크 클래스 Compiler도 각각 방언에서 비슷한 역할을 수행한다.
비주얼에 사용되는 또 다른 소규모의 facade로 클래스 TableInterface를 들 수 있다. TableView 의 인스턴스 3개가 테이블을 시각적으로 표현한다: 하나는 행 라벨, 하나는 열 라벨, 나머지는 행과 열을 나타낸다. TableInterface는 TableDecorator와 SelectionInTable에 대해 Facade의 역할을 한다. TableInterface는 테이블을 사용하고자 하는 설계자에게 이러한 클래스에 대한 세부 내용, 특히 상호작용에 대한 내용을 숨긴다.
SelectionInTable은 TableDecorator에 붙어 있는 TableView에 대한 모델이다. TableInterface는 사용자가 이러한 연결을 구성할 필요가 없도록 하고, 둘을 동기화된 채로 유지하는 Observer 알림을 유지하지 못하도록 한다. 또한 TableView들 사이의 Observer 연결을 숨긴다. TableView는 라벨의 텍스트를 제공하는 문자열 집합체와 테이블 내의 행 라벨과 열 라벨을 나타낸다. 이러한 클래스의 인스턴스들 간 관계를 아래 다이어그램에서 나타내고자 한다:
관련 패턴
Facade 패턴 대 Adapter 패턴
Facade는 Adapter(105) 패턴과 비슷하다. Adapter 패턴은 단일 객체의 인스턴스를 전환하는 반면 Facade는 다수의 객체에 대한 인터페이스를 전환한다. Adapter는 전부는 아니더라도 대부분 Adaptee의 행위를 제공한다. Facade는 그 객체의 행위에 대한 단순화된 하위집합을 제공한다.
Notes
- ↑ 이는 객체관계형 통합의 Crossing Chasms 패턴 언어에서 도출한 예제이다. 패턴 언어의 상세 내용은 Brown과 Whitenack (1996)의 연구를 참조한다.