DesignPatternSmalltalkCompanion:Flyweight

From 흡혈양파의 번역工房
Revision as of 11:02, 8 January 2013 by Onionmixer (talk | contribs) (메소드 > 메서드 수정)
Jump to navigation Jump to search

FLYWEIGHT (DP 195)

의도

작은 크기의 객체들이 많은 경우, 객체를 효과적으로 사용하는 방법으로 객체를 공유하게 한다.

구조

Dpsc chapter04 Flyweight 01.png

다음 인스턴스 다이어그램은 flyweight가 어떻게 공유되는지를 나타낸다:

Dpsc chapter04 Flyweight 02.png

논의

클래스가 Flyweight으로 구현될 때는 클래스의 어떠한 두 인스턴스도 동일한 엔티티(entity)를 나타내지 않는다. 두 클라이언트가 Flyweight 클래스의 인스턴스들을 독립적으로 도출하고 두 인스턴스가 동일한 값을 가지는 경우라면 두 클라이언트는 사실상 동일한 Flyweight 인스턴스를 공유한다. 클라이언트는 클래스가 Flyweight 임을 인식하지 못할 수도 있기 때문에 각자 고유의 독립된 인스턴스를 가졌다고 생각할 수도 있으나 Flyweight 클래스의 구현은 클라이언트가 동일한 인스턴스를 사용하도록 보장한다.

Flyweight들은 그들의 내부 상태와 외부 상태를 구분하기 때문에 서로 공유할 수 있다. 내부 상태는 Flyweight의 내부 상태로서 공유가 가능하다. 외부 상태는 Flyweight에 주로 보관되지만 서로 다른 클라이언트가 동일한 Flyweight에 대해 이 상태의 서로 다른 값을 기대하기 때문에 대신 클라이언트에 보관되므로 공유가 불가능하다. 일부 Flyweight 오퍼레이션은 외부 상태로의 접근을 필요로 하여 그 상태를 파라미터로 수용하기도 한다.

일반 클래스는 클라이언트가 클래스 new를 전송할 때마다 언제든 인스턴스를 제공하지만, Flyweight 클래스의 인스턴스 생성은 Flyweightfactory를 통해 통제된다. 클라이언트는 그가 원하는 Flyweight를 FlyweightFactory에 명시하면 팩토리가 그를 반환한다. 단 팩토리는 꼭 새로운 인스턴스를 생성할 필요가 없다. 이것은 기존 인스턴스의 flyweight pool을 포함한다; 명시된 인스턴스가 풀 안에 있을 경우 팩토리는 기존 인스턴스를 반환한다. 어떤 쪽이든 클라이언트는 그것이 새 인스턴스를 받는지, 기존에 있던 인스턴스를 받는지 알지 못한다.

Flyweight 패턴은, 본래 인스턴스가 공유되도록 고안되지 않은 클래스를 취한 후 재설계하여 그 인스턴스를 공유할 수 있도록 만드는 과정에 적용된다. 여기에는 클래스를 Flyweight로 향하는 공유 상태와 클라이언트로 향하는 비공유 상태로 구분하는 과정이 수반된다. 비공유 상태를 필요로 하는 Flyweight의 오퍼레이션 각각은 비공유 상태에 대한 파라미터를 추가하는 새로운 메시지 서명을 받는다. 마지막으로, 클라이언트는 더 이상 필요 시 Flyweight를 생성할 수 없게 된다; 대신 FlyweightFactory를 거쳐야 한다.

기호

Symbols

스몰토크에서 Flyweight 패턴의 또 다른 사용, Symbol 클래스를 예로 들어보자. Symbol은 변경할 수 없는 String으로, 그 문자를 교체하거나 변경할 수 없음을 뜻한다. 예를 들어, 함수 문자열을 취해 그 첫 번째 글자를 대문자화하는 것은 쉬운 작업이다:

| string |
string := 'string'.
string := string, ''.	"Only necessary in IBM Smalltalk."
string at: 1 put: (string at: 1) asUppercase.
^string

하지만 Symbol로는 이와 같은 작업을 할 수 없다.

| symbol |
symbol := #symbol.
symbol at: 1 put: (symbol at: 1) asUppercase.
^symbol

구체적으로 Symbol>>at:put: 과 같은 코드는 기호를 변경할 수 없다고 적힌 오류를 생성할 것이다. 따라서 기호는 불변의 특성을 가지지만 문자열은 변경이 가능하다.

불변의 문자열의 장점은 무엇일까? 이것은 다수의 객체가 문자열을 공유하면서 어떠한 객체도 문자열을 변경하지 않도록 보장해준다. 공통된 값의 공유는 어느 정도의 신뢰를 수반한다. 다른 객체들 중 값을 변경하였는데 당신의 마음에 들지 않을 수도 있다. 예를 들어 몇 가지 다른 객체와 문자열을 공유하고 있고 그 중 하나가 대문자화시키기로 결정했다고 치자. 당신은 대문자화를 원하지 않았지만 아쉽게도 문자열의 상태는 이미 변경되었고 그러한 사실을 알지도 못한다. 따라서 값을 공유하는 것은 위험 부담이 있다. 값을 공유하는 모든 이가 그 상태 변경에 동의해야만 한다. 가장 쉬운 동의 방법은 아무도 값의 상태를 변경하지 않는 것이다. 동의를 보장하기 위해선 값이 상태 변경을 허용하지 않는 것이 도움이 된다. 이렇게 하여 기호의 변경을 (Symbol>>at:put:) 시도할 경우 오류가 발생한다. 따라서 문자열을 공유하고 싶지만 누구도 변경하지 못하도록 하기 위해선 기호를 대신 공유하면 되겠다.

기호는 절대로 상태를 변경하지 않으므로 동일한 일련의 문자를 포함한 하나 이상의 인스턴스를 가질 필요가 없다. 문자열을 이용 시 당신은 고유의 ‘characters’ 복사본을 가져 여러분이 원하는 대로 변경하고, 또 다른 객체는 ‘characters’의 독립된 복사를 갖고 객체가 원하는 대로 문자열을 변경할 수 있다. 하지만 두 객체 모두 #characters 기호는 변경할 수 없기 때문에 각자 고유의 복사를 필요로 할 이유가 없고, 두 개의 복사보다는 두 객체가 공유하는 하나의 복사된 내용만 보관하는 것이 시스템 측면에서도 효율적이다.

두 객체는 서로에 대해 알지 못하며 그들이 동일한 기호를 사용한다거나 공유하고 있을 가능성이 있다는 사실도 알지 못한다. 서로가 아는 것은 특수 기호, 즉 특별한 연속 문자를 포함한 기호를 필요로 한다는 사실이다. 두 객체 모두 서로에 대해 아는 바가 없으므로 서로를 아는 어떤 것이 있다면 이것은 둘 사이를 중재하고 서로 동일한 기호를 사용하고자 한다는 것을 인식해야 하며, 둘은 서로 공유하게 될 동일한 기호를 제공해야겠지만 결국 그러한 사실을 인지하지는 못할 것이다. 기호의 경우 이러한 중재 객체는 스몰토크 시스템 자체, 그 중에서도 Symbol 클래스가 되겠다.

기호 생성

Symbol Creation

스몰토크에서 기호를 생성하는 방법에는 두 가지가 있다: 소스코드는 기호문자(symbol literal)를 포함할 수 있고, 문자열은 기호로 변환시킬 수 있다:

| symbol |
symbol := #characters.

| symbol |
symbol := 'characters' asSymbol.

둘 중 어느 방식을 사용하든 기호는 문자열로 시작하는데, 이는 소스 코드가 파서의 일부가 되는 문자열에 불과하기 때문이다. 파서와 String>>asSymbol은 새 인스턴스를 생성하기 위해 Symbol 클래스를 사용한다. 각 방언은 문자열을 기호로 변환하는 고유의 구현을 가지며, 각 구현은 상당히 복잡하다. 하지만 아래 코드는 모든 방언에서 사용하는 일반적 알고리즘을 보여준다:

String>>asSymbol
	^Symbol intern: self

Symbol class>>intern: aString
	"If the symbol already exists, it will be in the
	symbol table, so find it and return it. Otherwise,
	create the symbol, add it to the table, and return it."
	| symbol |
	symbol := self findInTable: aString.
	symbol notNil ifTrue: [^symbol].
	symbol := Symbol privateFromString: aString.
	self addToTable: symbol.
	^symbol

코드는 기호 테이블에 따라, 즉 클래스가 기존 인스턴스를 모두 저장하기 위해 사용하는 집합체에 따라 달라진다. 문자열이 나타내는 기호가 테이블에 있는 경우 클래스는 그 기존 인스턴스를 반환하고, 클래스가 새 인스턴스를 생성하는 경우는 이를 테이블에 추가한 후 반환한다. IBM 스몰토크에 사용되는 기호 테이블은 Symbol 내의 SymbolTable 클래스 변수이다. 비주얼웍스의 Symbol은 두 개의 클래스 변수인 USTable과 SingleCharsymbol들을 사용한다.

기호 테이블은 하나 또는 이상의 약한 집합체 클래스로 구현되는, 근본적으로 거대한 집합체이다. Weak pointer는 쓰레기 수집기(garbage collector)가 무시하는 것 중 하나이다. 객체가 그것을 가리키는 단일 변수 참조를 갖고 있는 한 사용 중이므로 쓰레기 수집기는 객체를 쓰레기로 수집하지 않을 것이다. 하지만 객체가 그를 가리키는 약한 참조의 수가 많은 경우도 있는데, 이 때 만일 실행 가능한 일반 참조가 없을 시 쓰레기 수집기는 사용하지 않는 객체로 간주하여 그 메모리를 회수할 것이다. 기호 테이블은 약한 집합체를 사용하여 사용하지 않는 기호들을 쓰레기로 수집할 수 있다. 또 기호가 일단 기호 테이블에 추가되었다면 절대 떠나지 않을 것이다.

Symbol은 Flyweight 패턴의 예제가 된다. Symbol에서는 두 개의 구분된 클래스 인스턴스가 동일한 값을 나타낼 수 없으므로 (예: 동일한 문자열을 포함하는 값) Flyweight 클래스가 된다. 각 인스턴스의 고유성은 FlyweightFactory의 역할을 하는 Symbol의 클래스 측에서 보장한다. 대부분 클래스들은 필요 시 new를 통해 새 인스턴스를 생성하지만, 기호 테이블에 그 값을 가진 인스턴스가 없는 경우에 한해 Symbol의 클래스 측이 새 인스턴스를 생성한다. 따라서 기호 테이블, SymbolTable 또는 USTable과 같은 클래스 변수는 flyweight pool, 즉 기존에 있는 모든 인스턴스의 집합체이다.

Symbol의 클래스 측은 어떻게 기호 테이블에서 찾아야 하는 값을 알까? 인스턴스 생성 메서드는 한결 같이 요청 시마다 다른 인스턴스를 인스턴스화시키는 new와 같은 단순 메서드와는 다르다. 오히려 조건부로 새 인스턴스를 생성하여 자신이 할 일을 결정하는데 도와주는 파라미터를 수용하는 intern: 와 같은 메서드처럼 복잡한 편에 속한다. 이번 경우, 파라미터는 String이 되는데, 이것은 자신이 반환해야 할 Symbol을 설명한다. String은 키 역할을 하여 어떤 인스턴스를 반환하는지 명시한다. FlyweightFactory는 flyweight pool에서 그 키를 가진 인스턴스를 찾거나 키에 해당하는 새 인스턴스를 만들어 그 키 아래의 풀에 추가한 후 새 인스턴스를 반환한다. 어떤 방법을 택하든 클라이언트는 요청한 키에 상응하는 인스턴스를 되돌려 받는다.

Flyweight 설계하기

FlyweightFactory는 서로를 알지조차 못하는 다수의 클라이언트들이 서로 공유 여부를 알 필요 없이 flyweight들을 공유할 수 있게 해준다. Flyweight를 사용하는 클라이어언트는 다른 클라이언트와 그것을 공유할 수도 있고 아닐 수도 있지만 그러한 사실을 알지 못하며, 그 객체가 flyweight인지, 그 객체가 공유되는지도 아마 모를 것이다.

Flyweight는 공유가 가능한 상태만 포함하기 때문에 공유될 수 있다. Flyweight 클래스는 주로 중복 인스턴스를 가진 일반 클래스로 시작하는데 그 이유는 이러한 클래스를 설계하기가 훨씬 수월하기 때문이다. 일반 클래스는 자신의 일을 수행하는데 필요한 상태가 무엇이든 포함한다. 하지만 시스템에서 이 클래스의 너무 많은 인스턴스 수를 요구하는데 반해 고유의 인스턴스가 상대적으로 적은 경우, 시스템은 많은 수의 역할을 하는 소수의 인스턴스를 공유하는 편이 나을 것이다.

여기서 문제는 클래스와 클라이언트 코드를 어떻게 재설계하여 Flyweight 클래스로 넣는지가 된다. 이에 2가지 중요한 단계가 필요하다: 공유 가능한 상태를 공유 불가한 상태로부터 분리하는 단계와 클래스가 고유의 인스턴스 생성을 통제하도록 허용하는 단계이다.

많은 클라이언트가 일반 클래스에 대한 중복 인스턴스를 사용하는 것으로 보이지만 가까이서 살펴보면 이러한 인스턴스들이 정확하게 같은 상태를 가지는 것은 아니다. 중복 인스턴스들은 두 범주의 인스턴스 변수를 가진다: 항상 값은 값을 가지는 범주와 (값이 같지 않을 경우 인스턴스가 중복되지 않음)항상 동일한 값을 가지지 않아도 되는 범주이다. 동일한 값을 가지도록 보장된 인스턴스 변수들은 공유가 가능하다; 단 클라이언트에 따라 달라지는 것은 공유가 불가하다. 공유가 불가한 변수들은 일반 클래스에서 클라이언트 클래스로 이동시켜 일반 클래스를 Flyweight로 만드는 일부분이 된다. 이는 [디자인 패턴] 편에서 Flyweight 내부의 응시된(stared) 상태를 내부 상태라고 부르고, 클라이언트 내의 공유되지 않은 상태를 외부 상태라고 부르는 이유이다 (DP 196).

Flyweight 밖으로 상태를 이동시킬 때 발생하는 문제는, 그 행위가 여전히 그 상태를 필요로 한다는 데에 있다. 대부분 클래스는 절반 이상의 인스턴스 변수를 제거 시 잘 작동하지 않으며 Flyweight도 마찬가지이다. 이 문제를 해결할 두 가지 방법이 있다: 행위를 클라이언트 안으로 이동하거나 클라이언트가 Flyweight의 행위에게 외부 상태를 제공하도록 시키는 방법이다. 클라이언트 내부로 행위를 옮길 경우, 당신이 Flyweight의 행위를 반캡슐화(uncapsulating)하는데 동일한 Flyweight 클래스를 사용하는 다수의 클라이언트 클래스에 그 행위가 중복될 수도 있다는 사실에 문제가 발생한다. 따라서 Flyweight의 캡슐화를 유지하여 외부 행위를 필요로 하는 Flyweight 행위에게 외부 행위를 제공하는 편이 낫겠다. 이는 필요한 외부 상태에 대한 파라미터를 추가하기 위한 행위의 오퍼레이션 중 메서드 서명을 변경한 후 이러한 새 메서드를 사용하기 위해 클라이언트가 전송한 메시지를 변경함으로써 달성할 수 있다. 이것이 바로 구조 클래스 다이어그램에 있는 Flyweight 오퍼레이션이 외부 상태를 파라미터로서 필요로 하는 이유이다.

클래스를 재설계하는데 발생하는 또 다른 문제는, 클래스가 고유의 인스턴스 생성을 통제하도록 허용하는 데에서 발생한다. 일반 클래스들은 최종적으로 new라고 불리는, 무조건적으로 새 인스턴스를 인스턴스화시켜 반환하는 메시지를 통해 인스턴스 생성을 허용한다. 일반 클래스를 Flyweight로 만들기 위해선 클래스에 프로세스를 좀 더 주의 깊게 통제하는, 좀 더 구체적인 인스턴스 생성 메시지가 필요할 것이다. 이것이 바로 FlyweightFactory의 getFlyweight: 메시지의 목적이다: 이 메시지 또한 new와 마찬가지로 새로운 인스턴스를 생성하나, 단 공유할 수 있는 적절한 인스턴스가 없는 경우로 제한한다.

클래스 측 Flyweigh 팩토리

[디자인 패턴]에서는 FlyweightFactory를 구분된 클래스로 나타내지만 스몰토크에서 FlyweightFactory은 주로 Flyweight 클래스의 클래스 측(class side)이다. 이는 Flyweight와 FlyweightFactory를 하나의 클래스로 결합하여 FlyweightFactory 룩업 행위가 클래스 측에 있게 된다. 어떠한 클래스든 그 클래스 측은 인스턴스 생성의 책임이 있으므로 (new와 같이) 인스턴스 관리의 책임을 지게 만드는 것 또한 쉽다.

FlyweightFactorysms 기존 인스턴스를 flyweight pool에 보관한다. 이 pool은 flyweights 집합체 변수, FlyweightFactory 내부의 인스턴스 변수로서, Flyweight 인스턴스를 참조로 사용한다. FlyweightFactory를 Flyweight의 클래스 측에 구현할 때 인스턴스 변수는 클래스 측 변수가 된다.

클래스 측 flyweight pool이 클래스 변수가 될지 클래스 인스턴스 변수가 될지는 시스템이 얼마나 많은 풀을 필요로 하는지와 그 풀에 어떤 Flyweight를 포함하길 원하는지에 따라 좌우된다. 한 계층구조의 모든 클래스는 클래스 변수의 동일한 인스턴스를 공유하지만 계층의 각 클래스는 클래스 인스턴스 변수에 대해 고유의 복사를 가진다. 따라서 다음과 같은 질문을 해야 한다: 한 계층의 모든 Flyweight 인스턴스를 하나의 풀에 유지해야 할까, 아니면 각 서브클래스에 대한 모든 인스턴스를 고유의 풀에 유지해야 할까? 이 질문은 Flyweight가 계층구조가 아니라 단일 클래스일 경우 실제적 차이가 없기 때문에 더 결정하기가 힘들다. 불확실할 경우에는 클래스 변수를 사용하여 모든 Flyweight가 단일 풀에 위치하도록 한다.

"flyweight pool"이란 이름으로 미루어보면 팩토리가 풀 변수에 flyweight들을 유지해야 하는 것처럼 보이지만 "풀(pool; 작은 공간)"이란 용어를 사용하는 둘은 서로 아무런 관계가 없다. 팩토리는 풀 변수가 아니라 클래스 변수 또는 클래스 인스턴스 변수 중 하나를 사용해야 한다.[1]

FlyweightFactory는 Flyweight 인스턴스로의 접근을 통제하는 싱글톤(91)이다. 패턴의 이름에서 알 수 있듯이 클래스의 클래스 측에 싱글톤을 구현하는 것은 당신이 후에 하나 이상의 인스턴스를 필요로 할 가능성이 있으므로 근시안적이라고 볼 수도 있겠다. 이번 사례에서는 하나의 Flyweight 클래스에 다수의 flyweight pool을 필요로 하는지의 여부가 문제가 된다. 그럴 경우 그러한 풀들을 관리하기 위해 FlyweightFactory에 대한 다수의 인스턴스를 필요로 할 것이며, 그에 따라 FlyweightFactory는 Flyweight의 클래스 측에 구현되는 대신 구분된 클래스로 구현되어야 한다.

효율적인 비교

Flyweight 클래스의 한 가지 좋은 부작용은 이퀄을 (=) 효율적으로 구현한다는 점이다. 두 문자열을 비교하기 위해선 그들이 확실히 동일하도록 각 문자쌍을 비교해야만 한다. 문자열이 길수록 비교시간이 길어진다. 두 기호를 비교하는 작업은 기호가 얼마나 길든 상관없이 둘이 동일한 객체냐 다른 객체냐의 문제이므로 비교가 빠르다. 둘이 다른 객체라면 동일한 문자열을 포함하지 않으므로 동일할 수가 없다. 따라서 비주얼웍스에서는 Symbol이 더블 이퀄(==)로 이퀄을 구현한다 (비주얼 스몰토크에서는 ==와 같은 역할을 하는 Symbol>>=가 우선적으로 구현된다):

Symbol>>= aSymbol
	^self == aSymbol

더블 이퀄은 두 객체가 같은 객체인지, 메모리에 같은 위치를 차지하는지를 검사한다. 일반 클래스의 두 인스턴스는 구분된 객체이나 다른 상태를 가지므로 서로 일치할 필요 없이 같아질 수 있다. 하지만 두 개의 flyweight가 같은 상태를 가질 수 있는 유일한 방법은 같은 객체일 경우에만 가능하므로, 두 flyweight의 동등과 동일성이 같아야 한다.

아래 코드는 String은 Flyweight에 속하지만 Symbol은 아니라는 사실이 String과 Symbol에서 더블 이퀄이 어떻게 다르게 작용하는지를 보여준다:

"VisualWorks and Visual Smalltalk"
"(String literals work differently in IBM Smalltalk)"
"line 1"	'String' = 'String'		"Returns true."
"line 2"	'String' == 'String'		"Returns false."

	"VisualWorks, Visual Smalltalk, and IBM Smalltalk"

"line 3"	#Symbol = #Symbol		"Returns true."
"line 4"	#Symbol == #Symbol		"Returns true."
"line 5"	'String' = ('Str','ing')	"Returns true."
"line 6"	'String' == ('Str','ing')	"Returns false."
"line 7"	#Symbol = 'Symbol' asSymbol	"Returns true."
"line 8"	#Symbol == 'Symbol' asSymbol	"Returns true."

같은 문자열에 대한 두 개의 String 리터럴은 동등하지만 (라인 1 참조) 동일하진 않다 (라인 2 참조). 동일한 문자열을 포함하는 두 Symbol 리터럴은 동등하면서도 (라인 3) 동일하다 (라인 4). String과 Symbol 리터럴의 경우뿐만 아니라 프로그램에 따라서 생성된 리터럴의 경우도 마찬가지다 (라인 5~8 참조). (동등(=)과 동일성 (==) 간 차이점에 대한 정보는 Woolf, 1996를 참조한다.)

Integer, Float, 그리고 Character

Integer는 구체적 클래스가 아니다; 다수의 구체적 서브클래스를 가진 추상 클래스이다. 그 중 하나는 SmallInteger로서, 대략 -108부터 108까지 범위의 "일반" 정수를 위한 클래스이다. SmallInteger 중 같은 값을 나타내는 두 인스턴스는 구분할 수가 없다; 이들은 더블 이퀄이며 같은 hash 값이 같다 (스스로). 정말로 Flyweight로서 구현될까? 장담할 수 없다; 구현은 가상머신과 primitives에 묻혀 있다. SmallInteger의 flyweight pool을 포함하는 FlyweightFactory가 있다면 스몰토크 이미지로부터 알아볼 수 없다. 하지만 동일한 값을 가진 두 인스턴스를 구분할 수 없다면, 스몰토크 코드는 이들을 Flyweight로 취급한다.

모든 Number 클래스가 Flyweight처럼 행동하는 것은 아니며, SmallInteger만 해당한다. Integer 서브클래스는 이런 방식으로 작용하지 않는다. SmallInteger가 정수 데이터 타입을 감싸 CPU가 정수 수학을 실행할 수 있기 때문에 SmallInteger가 Flyweight라고 생각할 수도 있다. 하지만 Float 또한 효율성을 위한 실제 데이터 타입을 감싸지만 Flyweight는 아니다. 작업 공간에 이를 시도해보자:

"line 1"	4 == 4				"Returns true."
"line 2"	5 == (2 + 3)			"Returns true."
"line 3"	8589934592 == (2**33)		"Returns false."
"line 4"	4.0 == 4.0			"Returns false."

마찬가지로 스몰토크의 Character는 [디자인 패턴]의 동기 단락에서와 같이 Flyweight 클래스에 해당한다. 두 개의 Character는 같은 ASCII 값이 실제로 같은 인스턴스임을 나타낸다.

Symbol, SmallInteger, Character의 한 가지 결점은, Flyweight 패턴의 예제에서 나타내듯 내부 상태와 외부 상태를 구분하지 않는다는 사실에 있다. 클라이언트 코드에 의해 수동적으로 사용되며, 클라이언트에 의해 파라미터에 들어가는 외부 상태를 이용해 실행할 것을 듣지 못한다.

Flyweight Pool

[디자인 패턴]편에서 flyweight의 보관된 내용을 회수하는 것 또한 문제가 된다고 언급하고 있다 (DP 200). FlyweightFactory의 구현이 까다로울 수 있는 것은 우선 flyweight를 flyweight pool에 추가하고 나면 객체가 절대 떠나지 않기 때문이다. 클라이언트가 더 이상 특정 flyweight를 사용하지 않을 경우, 그것은 아무런 문제 없이 쓰레기로 수집된다. 하지만 이것이 과연 옳을까? Flyweight가 곧 다시 사용된다면 풀은 그때까지 갖고 있어야 하지 않을까? 반면 누구도 flyweight를 사용하지 않고 가까운 미래에 사용할 계획이 없다면 flyweight pool도 공간을 절약하기 위해 flyweight를 폐기해야 할 것이다. Flyweight가 나중에 다시 필요하면 그 때 다시 재생성할 수도 있다.

그렇다면 FlyweightFactory는 flywweight를 더 이상 사용하지 않는 시점을 어떻게 알 수 있을까? 그것은 flyweight pool을 통해 flyweight를 가리키고 있지만, 또 누가 가리키고 있을까? 다수의 클라이언트가 서로에 대해 모르듯이 FlyweightFactory 팩토리 또한 다른 팩토리에 대해서는 알지 못한다; flyweight에 관해서만 알 뿐이다. Flyweight들은 그들을 사용하는 클라이언트가 있더라도 그 수가 얼마나 되는지 알지 못한다.

한 가지 접근법으로 주로 C++에 사용되는 참조 카운팅을 들 수 있다. 클라이언트가 flyweight를 요청할 때마다 FlyweightFactory는 이 클라이언트가 flyweight를 사용하고 있다는 사실을 기억한다. 클라이언트가 더 이상 flyweight를 필요로 하지 않을 경우 이는 FlyweightFactory에게 알려야 한다. 클라이언트가 더 이상 flyweight를 사용하지 않을 경우 flyweight pool로부터 제거될 수 있다. 이러한 접근법은 좀 더 복잡하며 FlyweightFactory와 그 클라이언트들 간 더 많은 협동을 요한다.

스몰토크에서 사용할 수 있는 좀 더 간단한 접근법으로, 하나 또는 그 이상의 약한 집합체를 이용해 flyweight pool를 구현하는 방법이 있다. 쓰레기 수집기는 약한 집합체의 참조를 무시한다. 클라이언트가 더 이상 flyweight를 사용하고 있지 않다면 그에 대한 유일한 참조는 flyweight pool로부터 나온 것으로 약한 참조가 된다. 쓰레기 수집기가 실행되면 약한 참조는 무시하고 flyweight에 다른 참조가 없음을 발견하고는 flyweight를 할당 해제한다. Symbol은 그 기호 테이블을 구현하는 데에 있어 약한 집합체를 사용한다.

매우 드문 사용

스몰토크에서 Flyweight 패턴의 사용예는 매우 드물다. Symbol, SmallInteger, Character는 흔히 사용되는 클래스이지만 그들의 구현조차 패턴을 완벽하게 설명하진 못한다. SmallInteger과 Character 클래스는 가상 머신에서 주로 구현되며, 세 가지 클래스의 오퍼레이션 중 어떤 것도 외부 상태를 파라미터로 할 것을 요구하지 않는다.

Flyweight들은 근본적으로는 단순한 타입이다ㅡ잘게 나누어진 객체들로서 수백만 번 사용되지만 고유의 값은 거의 없다. 스몰토크 프로그래머들은 그들 고유의 단순 타입 클래스를 구현할 필요가 없다; 벤더들이 이미 구현해놨기 때문이다. 새로운 Number 클래스와 같이 새로운 간단한 타입을 구현한다 하더라도 이를 Flyweight로 굳이 구현할 필요가 없을 것이다.

Sharable

아무리 예제를 찾기가 힘들다 할지라도 Flyweight 패턴을 배워두면 유용한 이유는 Flyweight는 우리가 Sharable이라 부르게 될, 광범위한 패턴의 전문화 때문이다. 두 패턴의 구현과 원칙은 서로 매우 흡사하므로, 하나를 이해할 경우 나머지 하나를 이해하기가 훨씬 수월해진다. 게다가 Flyweight의 사용예는 드물지만 Sharable의 예는 좀 더 흔히 발견된다.

두 패턴들 간 차이는 그 의도라고 볼 수 있다. Flyweight는 잘게 나누어진 객체들을 위한 것이다; Sharable은 최소의 객체들로부터 최대 객체까지 무엇이든 처리가 가능하다. Flyweight 패턴은 많은 수의 중복 객체를 필요로 한다; Sharable 패턴은 효율성이 떨어지기 시작할 만큼 충분한 중첩, 즉 두 개 정도의 객체만 필요로 한다. Flyweight 패턴은 효율성에 중점을 두지만 Sharable은 일관성과 정확성에 중점을 둔다.

Flyweight과 Sharable 패턴은 비슷하게 구현된다. 두 패턴 모두 기존 인스턴스의 객체 풀로부터 도출하는 SharableFactory를 수반한다. 그리고 객체를 공유할 수 있고 공유할 수 없는 상태로 구분하여 클라이언트로 들어가게 한다. 뿐만 아니라 두 패턴 모두 외부 상태를 필요로 하는 오퍼레이션은 파라미터를 통해 얻어야 한다.

Sharable 패턴의 예제

스몰토크에 사용되는 Sharable 패턴의 예로는 매우 큰 객체의 중복을 피하기 위해 객체들을 공유하는 것을 들 수 있다. 객체가 수 십, 수 백 개의 클라이언트에 의해 사용될 수 있다는 점에 문제가 있다; 또한 소수의 클라이언트만 사용하고 있을 수도 있다. 그럼에도 불구하고 몇 번의 횟수라 하더라도 낭비가 크기 때문에 복제하는 것은 규모가 크다. 따라서 이렇게 작은 규모에서도 공유는 유용하다ㅡSharable이 잘게 나누어진 객체라서가 아니라 너무 큰 패턴이기 때문이다.

Sharable 패턴의 또 다른 사용예는 데이터 무결성과 일관성을 확보하도록 돕는데 사용된다. [디자인 패턴]에서는 Flyweight (DP 137)의 중점이 아니라고 명시적으로 나타내지만 두 가지 목적은 유사하다. 다수의 객체가 같은 개념적 엔티티를 나타내선 안 된다. 중복 객체가 같은 엔티티를 나타내지만 상태는 서로 다를 경우 무엇이 엔티티의 진짜 상태가 되겠는가? 따라서 이러한 중복된 객체들은 상태를 똑같이 유지하기 위해 동기화되어야 한다. 즉시 재동기화시키기보다는 하나의 객체만 유지하고 클라이언트가 그것을 공유하도록 두는 것이 낫다. 이렇게 하면 하나의 클라이언트가 객체의 상태를 변경할 때 동기화를 실행할 필요가 없다. (Sharable의 변경에 따라 다양한 클라이언트를 동시에 움직이는 방법은 Observer (305)를 참조한다.)

세 가지 경우 모두ㅡ작게 나누어진 수많은 객체들, 중복된 큰 객체들, 그리고 일관적으로 동일한 상태를 보관해야 하는 객체들ㅡ클라이언트는 인스턴스를 중복시키기보다는 공유할 수 있어야 한다. 이러한 공유는 투명해야 한다. 즉, 각 클라이언트는 새로운 Sharable를 얻기 위해 표준 프로토콜과 중앙 위치를 사용하며, 다른 클라이언트가 있다 하더라도 그것이 객체를 공유하는지 알지 못한다. 이러한 투명한 공유가 Sharable 패턴의 핵심이다.

USState

한 가지 예로 미국 내 한 주(state)를 나타내는 객체, USState를 구현하는 방법을 가정해보자. 50개 주밖에 없기 때문에 시스템은 무제한의 USStage 인스턴스 수가 필요 없으며, 하나의 주당 하나만 필요하다. 각 USState는 각 도시의 인구, 전화지역코드와 지역번호의 목록, 각 주가 어떻게 생겼는지 보여주는 이미지를 포함해 그 주에 관한 통계와 인구조사 정보로 가득찬 하나의 거대한 객체가 될 것이다. 따라서 시스템은 특정 주에 대해 굳이 하나 이상의 인스턴스를 가질 필요가 없을 뿐만 아니라 다수의 인스턴스는 메모리 낭비가 되고 새로운 통계를 이용할 수 있을 때 업데이트 문제를 발생시킨다.

USState가 Sharable로서 구현되는 것은 필수가 아니라 유용할 뿐이다. 이는 조합 내에 각 주에 하나의 인스턴스만 있도록 보장한다. 다음 다이어그램은 어떠한 USState가 Sharable로 구현되는지를 보여준다:

Dpsc chapter04 Flyweight 03.png

각 주는 고유의 판매세율을 설정하므로 50개 주마다 다르겠다. 통신 판매 제도 회사가 고객에게 주문서를 보내면 세금은 고객이 거주하는 주에 따라 달라진다. 또한 일부 항목은 세금이 부과되지만 일부는 그렇지 않으므로, 이 또한 주마다 다르다. 따라서 간단한 주문에 대한 세금을 계산하는 것도 복잡해질 수 있다. 세금을 계산하기 위한 모든 차이가 주문서의 책임은 아니며, 명세가 주별로 다르므로 이러한 행위를 위치시키기에 USState가 논리적 공간으로 보인다.

세금을 계산하기 위해 USState는 Order를 그 argument로 받아들이는 computeSalesTaxOn:와 같은 메시지를 구현하는데 그 다이어그램은 다음 페이지에 소개하고자 한다. 각 주에서 모든 타입의 항목에 대해 동일한 세금을 부과하였다면 단순히 주문서의 부분합을 바탕으로 세금을 계산할 수 있다. 일부 항목 타입은 세금이 부과되고 일부는 부과되지 않을 경우, 주 객체는 각 항목마다 세금을 계산하여 이를 합할 것이다. 어떤 방법이든 주는 주문서에 세금을 말해주어야 주문서가 총액을 계산할 수 있을 것이다.

Dpsc chapter04 Flyweight 04.png

이 예제에서 USState는 Sharable 클래스이고, Order는 클라이언트이다. 주는 세율표와 같은 내부 상태를 포함한다. 주문서는 세금을 계산하기 위한 항목과 같은 내부 상태를 포함한다. 세금계산 메서드인 USState>>computeSalesTaxOn: anOrder는 세금이 부과되는 주와 세금을 부과하는 항목의 주문서를 모두 알고 있다. 이것은 세금을 계산하고 그 결과를 주문서로 반환한다.

영속 객체

영속 객체들은ㅡ스몰토크 이미지 외부에서 데이터베이스 또는 파일에 저장되는 객체ㅡ무결성과 효율성을 고려해 중복되어선 안 된다. 영속적 보관에 대한 시스템의 인터페이스는 SharableFactory로서 Sharable 패턴을 이용해 각 개념적 엔티티마다 하나의 인스턴스만 있도록 할 수 있다.

데이터베이스, 그 중에서도 관계형 데이터베이스를 사용하는 시스템의 아키텍쳐는 데이터베이스로 접근을 가능하게 하고 나머지 시스템에 대해 세부 내용을 숨기는 데이터베이스층을 포함해야 한다 (Brown, 1995a). 이 데이터베이스층은 도메인 객체를 로딩하고 그 변경을 실행하는 책임이 있다. 시스템이 도메인 객체로 접근해야 하는 경우 시스템은 데이터베이스층에게 그 키를 명시함으로써 객체를 요청한다. 성능을 향상시키기 위해 데이터베이스층은 종종 데이터베이스로부터 이미 읽은 객체를 캐시에 저장하여 다시 로드시킬 필요가 없도록 한다. 따라서 한 클라이언트가 다른 클라이언트가 앞서 요청한 도메인 객체를 요청할 경우, 데이터베이스층이 캐시에 저장된 객체를 반환하고, 두 클라이언트는 서로 모르는 채 동일한 객체를 공유한다.

이것은 Sharable 패턴이다. 데이터베이스층은 거대한 SharableFactory로서 도메인 객체의 생성과 접근을 통제한다. 이 층의 캐시는 도메인 객체를 포함하는 객체 풀이다. 이것은 도메인 객체 자체를 Sharable로 만든다. 도메인 (영속) 행위를 애플리케이션 (일시) 행위로부터 구분하는 과정은 내부 상태를 외부 상태로부터 구분하는 것과 비슷하다. 그 결과 일시적 상태를 보관하지 않고 영속 상태만 보관할 수 있다. 이는 [디자인 패턴] 편에 소개된 Flyweight의 의미는 아니지만 그 결과적 구조와 행위를 비롯해 개발 과정만큼은 매우 유사하다.

Distributed Flyweight

분산 스몰토크 또는 어떤 유형의 분산 객체 설계든 Flyweight를 포함해 Sharable 패턴의 변형체에 특히 도전과제가 된다. 분산 스몰토크 이미지는 몇 개의 물리적 스몰토크 이미지로 구성되어 있고, 각각의 이미지는 구분된 기계에 실행되며 단일 객체 공간을 가진 하나의 논리 이미지로서 작용한다. 객체들은 결과를 예상하는 객체 위치와 상관없이 대부분 이용 가능한 CPU 주기를 가진 기계면 어디든 계산을 수행할 수 있으므로 로드 균형을 허용한다. 이는 지리적으로 분산된 사용자들이 동기화해야 하는 중복 객체들을 사용하는 대신 동일한 객체들을 공유하도록 해준다 (Proxy (213)).

Sharable 패턴에서는, 이미지가 특정 값을 가진 Sharable 클래스의 인스턴스를 하나만 포함하도록 되어 있다는 것이 문제이다. 예를 들자면, 시스템 내에 대문자 A는 하나만 존재하며 정수 5도 하나만 존재한다. 이미지가 다수의 기계에 걸쳐 (논리적으로) 분산되어 있다면 어떤 기계가 Sharable를 포함해야 할까? 정수 5가 한 기계에 보관되어 있고 다른 기계에는 7이 보관되어 있다면 네트워크 통신으로 인해 5에 7을 합하는 계산이 갑자기 훨씬 더 복잡해진다. 네트워크가 일시적으로 꺼져있는 경우 계산을 아예 실행할 수 없게 된다.

물리적 이미지들간 통신을 최소화시키기 위해선 심하게 사용되는 Sharable들이 (Character들, SmallInteger들, Symbol들과 같은 Flyweight가 대부분이고, true, false, nil과 같은 싱글톤도 (91) 포함) 물리적 이미지들에 걸쳐 중복된다. 따라서 동일한 값을 가진 두 개의 Flyweight는 분리된 물리적 이미지에 (기계 당 하나씩) 있는 한 동일한 논리적 이미지에 (다수의 기계에 걸쳐 분산된) 존재할 수 있다. 여전히 단일 값을 나타내는 분리된 Flyweight 객체들은 논리적으로 하나의 객체이다. 이러한 Flyweight들은 변경되지 않는 내부 상태를 가지기 때문에 관리가 가능하다. 하지만 더블 이퀄과 같은 정체성 메시지로 혼란을 일으킨다. 두 개의 메모리 주소가 동일한지 확인하기 위해 비교하는 과정은 두 개의 주소가 같은 물리적 메모리 공간에 있을 때, 즉 같은 기계에 있을 때에만 가능하다. 따라서 두 개의 argument가 구분된 메모리 공간에 있을 때 더블 이퀄은 다르게 작용해야만 한다. 사실상 분산 스몰토크 벤더들은 동일한 메모리 공간에 있지 않은 객체들을 비교할 때 더블 이퀄의 사용을 권하지 않는다.

Applicability

Sharable 패턴을 적용하기로 결정했다면 시스템이 이 패턴을 사용하는 방법에는 3가지가 있다. 시스템이 Sharable를 사용하는 방법은 당신이 이를 얼마나 조심스럽게 구현해야 하는지를 결정한다:

  1. 정체성 고유성. 시스템은 같은 값을 가진 두 개의 Sharable 인스턴스가 동일한 객체라고 가정하며 이 가정에 의존한다. 객체들을 가능한 한 효율적으로 관리하기 위해 더블 이퀄을 (==) 이용할 수 있다. 이러한 시스템은 같은 값을 가진 두 개의 구분된 Sharable을 마치 구분된 값을 가진 것처럼 취급하며 중복을 제거하는데 실패한다. 예를 들어, 중복된 Sharable을 제거하기 위해 IdentitySet를 사용할 순 있지만 단 중복된 Sharable이 identical하다고 보장된 경우에만 가능하다.
  2. 데이터 무결성. 시스템은 각 개념적 엔티티가 하나의 인스턴스에 의해서만 표시되도록 보장하기 위해 Sharable을 사용한다. 동기화되지 않은 인스턴스는 데이터 무결성의 손실을 야기하기 때문에 다수의 인스턴스는 자신들의 상태를 동기화된 채로 유지하기 위해 오버헤드를 필요로 한다. 중복들은 코드의 작동을 중지시키지는 않지만 시스템의 데이터에 대한 신뢰성을 해칠 가능성이 있다.
  3. 공간 절약. 시스템은 Sharable을 이용해 동등한 인스턴스들을 저장하곤 하는데, 이는 중복 인스턴스들을 인스턴스화시키기란 시간 소모와 메모리 소모가 크기 때문이다. 그러한 시스템은 인스턴스가 Sharable이라고 가정하지 않는다. 오히려 반대다: 각 클라이언트는 자신에게 고유의 독립된 복사가 있으며 두 인스턴스가 동일한 인스턴스일 가능성은 적다고 가정한다. 시스템 내 Sharable이 중복된다 해도 괜찮다; 단 시스템은 정확하게 작동하겠지만 효율성은 약간 떨어질 것이다.

구현

Sharable 클래스와 SharableFactory 클래스를 구현하는 방법에는 두 가지가 있다. 두 방법의 차이는 새로운 Sharable 인스턴스가 어떻게 생성되는지와, 접근 방식에 어떻게 영향을 미치는지와 관련이 있다:

  1. 코드를 통한 생성. Sharable을 생성하는 한 가지 방법으로는 그러한 작업을 수행하는 메서드를 실행하는 것이다. 따라서 SharableFactory는 각 Sharable을 생성하는 구분된 메서드를 포함한다. 이렇게 작용하는 SharableFactory는 [디자인 패턴] 편에서 Flyweight에 대해 제시하는 것처럼 하나의 룩업(look-up) 메서드를 (예: SharableFactory>>getSharable:key) 필요로 하지는 않는다. 이러한 룩업 메서드는 불가피하게 Sharable 키를 그 생성 메서드와 일치시키는 case문을 포함해야 한다. 대신 팩토리는 각 Sharable마다 (getSharableA, getSharableB 등) 구분된 접근자 메서드를 사용해야 한다. 이는 이용 가능한 다양한 Sharable 수를 컴파일 시간에 하드코딩하지만, 컴파일 시간에 구현 메서드가 구현되지 않았을 경우 새로운 Sharable을 런타임 시 생성할 수 없기 때문에 이미 하드코딩되어있을 것이다.
  2. 메타설명(metadescription)에 의한 생성. 새로운 Sharable을 생성하는 또 다른 방법으로, 사용자가 새로운 Sharable을 런타임 시간에 명시하도록 허용하는 방법이 있다. 이에 따라 SharableFactory는 새로운 Sharable을 생성하기 위한 코드는 포함하지 않는 대신 새로운 Sharable의 내부 상태를 명시하고 그에 따라 새로운 Sharable 인스턴스를 생성하는 인스턴스는 포함한다. 이러한 새로운 인스턴스들은 사용자에 의해 생성되기도 하고 파일이나 데이터베이스와 같은 영속적 저장공간으로부터 읽어오기도 한다.

명시해야 할 속성들 중 하나는 Sharable로 접근 시 키로 사용될 새로운 Sharable에 대한 고유의 이름이다. 시스템의 다른 부분이 특정 이름을 가진 Sharable를 이용하고 있다면 시스템은 SharableFactory의 Sharable의 이름을 키로 가진 룩업 메서드를 이용해 (예: SharableFactory>>getSharable : key) Sharable로 접근한다. Sharable의 총 다양성은 컴파일 시간에 알려져있지 않기 때문에 이러한 SharableFactory는 구분된 접근자 메서드를 (getSharableA, getSharableB 등) 제공하지 못한다.

새로운 Sharable 인스턴스가 어떻게 생성되는지와 상관없이 Sharable을 구현 시 고려해야 할 문제가 몇 가지 있다:

  1. 이퀄은 더블 이퀄이다. 스몰토크의 모든 객체들은 = (이퀄)과 == (더블 이퀄)을 이해한다. Sharable 클래스는 동등(equality)과 동일성(identity)를 동일하게 구현한다. 이는 Sharable에게는 객체의 동일성과 동등이 같기 때문이다. 하지만 이 특성을 이용하는 클라이언트라면 객체들이 Sharable이며 이 객체들은 서로 공유되고 있음을 깨달아야 한다.
  2. Copy가 original을 반환한다. 스몰토크의 모든 객체들은 copy를 이해한다. Sharable 클래스는 복제된 객체보다는 단순히 self (수신자)를 반환하기 위해 copy를 구현한다. 이는 Sharable 클래스가 같은 값을 가진 두 개의 인스턴스를 가질 수 없기 때문이다 (예: 복제). (copy에 관한 자세한 정보는 Prototype (77)을 참조한다.)
  3. 클래스 측 SharableFactory. 스몰토크에서 SharableFactory는 주로 Sharable 클래스의 클래스 측에서 구현된다. 클래스 측은 항시 인스턴스 생성의 책임이 있는데, Sharable 클래스의 경우 인스턴스 접근에 대한 책임도 있다.
    SharableFactory를 클래스 측에 구현하는 것이 항상 적절한 방법은 아니다. SharableFactory는 본질상 싱글톤(91)이어야 하지만 시스템은 다수의 인스턴스를 가질 수 있는 구분된 SharableFactory클래스를 요구할 수도 있다. 예를 들어, 시스템은 단일 이미지 내에 구분된 세션을 구현할지도 모른다. 그 Sharable들은 세션 내에서 유일할 것으로 기대하지만 세션들 간에는 중복될 수도 있다. 따라서 다른 세션들에 대한 각 세션의 의존성을 위해 Sharable을 생성하고 관리하려면 고유의 SharableFactory를 필요로 할 것이다.
  4. 영속적 Sharable. Sharable은 독특한 영속성 문제를 제기한다. 본질상 객체 데이터베이스는 객체의 이진 이미지를 취해 저장하는 방식으로 작동한다. 후에 그것은 스몰토크 이미지에 바이트를 다시 로딩시킴으로써 객체를 로드한다. 관계형 데이터베이스 저장공간도 비슷하지만 객체와 관계형 양식을 서로 변환하는 과정을 필요로 한다.
    이러한 표준 기법은 Sharable에선 작용하지 않을지도 모른다. 일부 객체 데이터베이스는 객체 정체성을 보존하기에 주의를 기울여 하나의 유일한 객체가 이미지에 한 번에 로드될 것이다. 이를 이용해 필요 시 Sharable을 보관하는 경우 Sharable의 copy를 다수로 생성시켜 이미지 공간에서와 같이 데이터베이스 공간도 소모한다. 이보다 더 나쁜 소식은, 필요 시 Sharable을 로딩할 때 다수의 Sharable copy가 로드될 수 있다는 점이다.
    위를 대신해 Sharable을 두 가지 단계를 통해 영속적으로 저장할 수도 있다. 첫 번째 단계에서는 Sharable 클래스가 늘 그렇듯이 그의 각 인스턴스를 영속적으로 저장한다. 두 번째로, Sharable이 속한 객체와 그 부분들을 보관할 때 전체 Sharable을 저장하지 않도록 한다; 그 키만 저장하도록 하라. 그리고 다시 Sharable을 로드하기 위해선 프로세스를 거꾸로 실행한다. 먼저, 영속적 Sharable을 로드하고, 이미 로드된 것을 복제하는 Sharable을 로드하지 않도록 확실히 한다. 그리고 난 후 Sharable 부분을 포함하는 객체를 로딩 시 키를 이용해 Sharable로 접근하여 이를 클라이언트 객체로 연결시킨다. 이 프로세스는 Sharable을 영속적으로 저장하도록 보장할 뿐더러 Sharable을 다시 로드할 때도 Sharable일 것을 보장한다.

마지막으로 SharableFactory는 약한 집합체를 이용해 그 공유 가능한 풀을 구현하길 원할지도 모른다. 그럴 경우, 클라이언트가 더 이상 sharable을 이용하고 있지 않을 때 쓰레기 수집기는 공용 가능한 sharable pool의 참조를 무시하고 sharable의 메모리를 회수할 것이다.

예제 코드

Sharable 패턴의 한 가지 흔한 사용은 그래픽 아이콘을 관리할 때이다. 그래픽 사용자 인터페이스(GUI) 윈도우는 버튼 라벨, 메뉴 선택, 목록 항목 등과 같은 그래픽 아이콘을 나타내곤 한다. 종종 동일한 아이콘이 여러 장소에 사용된다. Help 아이콘은 “Help” 버튼과 Help 메뉴 선택에 모두 표시된다. 아이콘이 목록의 각 항목 상태를 나타내고 선택 가능한 상태가 몇 가지에 그친다면 목록에서 동일한 몇 개의 아이콘을 반복해 사용할 것이다.

비주얼웍스에서 아이콘은 PixelArray/Image 계층구조의 인스턴스로 표시되고, 비주얼 스몰토크에서는 Icon 으로, IBM 스몰토크에선 CgIcon으로서 나타난다. 이번 예제에서는 비주얼웍스의 Image 클래스를 이용하고자 한다. Image는 자원 집중적으로 볼 수 있다; 엄청난 메모리를 소모한다ㅡ스몰토크 메모리, 윈도우 시스템 메모리 중 하나 또는 둘 다 소모한다. 따라서 동일한 아이콘을 여러 장소에 사용하고 각 장소에서 정확히 똑같은 모습으로 보일 예정이라면 그 모든 장소에 Image의 동일한 인스턴스를 사용하는 것이 효율적이다. Image는 한 번 생성되고 나면 그 내부 상태가 변하지 않기 때문에 이러한 상황에 효과가 있을 것이다. Image를 사용하는 위젯은 그 상태를 바꿔선 안 된다.

Image 계층구조는 이미 존재하기 때문에 Sharable 클래스는 이미 구현되어 있을 것이다. 하지만 Image는 Sharable 클래스로서 구현되지 않는다; 단지 같은 값의 인스턴스들을 둔 일반/클래스 계층구조이다. 따라서 Image를 Sharable로 사용하기 위해선 SharableFactory를 구현할 필요가 있다.

SharableFactory를 구현하는 데에는 두 가지 접근법이 있다: Image의 (또는 그 슈퍼클래스 PixelArray) 클래스 측에 구현하거나 구분된 클래스로서 구현하는 방법이다. Image는 벤더 이미지이므로 클래스 측에 팩토리를 구현한다는 것은 벤더 코드를 수정함을 의미한다. ENVY/Developer로 클래스 확장을 이용하여 벤더 애플리케이션에 발생하는 변경을 피할 수는 있지만 클래스 변수를 추가하기 위해선 당신이 클래스 정의를 변경해야 한다.

클래스 측 팩토리

Sharable 클래스에 해당하는 Image의 클래스 측에 SharableFactory를 구현하는 것을 살펴보자. 먼저 구조의 인스턴스 다이어그램에 나타난 객체 풀을 구현하기 위한 변수가 필요할 것이다. 이 변수는 계층구조에 Sharable 인스턴스를 모두 캐시 저장하는 클래스 변수이다. 각 서브클래스가 다른 클래스와 독립된 인스턴스들을 캐시에 저장할 수 있도록 하려면 클래스 인스턴스 변수를 사용할 것이다. 우리는 전체 계층구조에 대한 클래스 변수를 사용하고 있는데 이를 ImagePool이라 부르겠다.

새로운 클래스 변수를 추가하는 것은 클래스 정의의 수정을 필요로 한다. 하지만 벤더 코드의 변경은 피해야 하는데 이는 버그를 불러올 수 있고 새로운 벤더 릴리즈로의 포팅 변경을 더 까다롭게 만들기 때문이다. 클래스 변수를 추가하는 것은 클래스의 변경을 요하지만 Image는 벤더 클래스이므로 클래스를 변경할 필요가 없다면 하지 않을 것이다. 따라서 클래스 변수를 사용하는 대신 전역 변수 ImagePool을 사용할 것인데, 이 변수는 클래스에 의해 마치 클래스 변수인 것처럼 private로 사용된다.

이제 Image는 전역 변수를 필요로 하므로 변수를 생성하고 그 초기 값을 설정하기 위해선 클래스 측에 initialize를 구현할 필요가 있겠다. 이미지 이름을 명시하고 각 값은 해당 Image인 Symbol을 각 키로 가지는 IdentityDictionary를 이용해 캐시를 구현하고자 한다. 키는 Symbol이고 Symbol은 Sharable이므로 (Flyweight) 일반적 Dictionary보다 좀 더 효율적인 IdentityDictionary를 사용할 수 있다.

왜 IdentityDictionary가 일반 Dictionary보다 더 효율적일까? Dictionary는 키를 찾기 위해 findIndexOrNil: 메서드를 사용한다. 이 메서드는 그것이 찾던 키와 발견한 키가 맞는지 결정하기 위해 이퀄을 사용한다. IdentityDictionary이 이퀄 대신 더블 이퀄을 사용하기 위해 findIndexOrNil:을 재구현한다는 점을 제외하면 Dictionary와 동일하다. 더블 이퀄이 좀 더 효율적이기 때문에 IdentityDictionary 또한 더 효율적이겠다. Sharable 클래스는 이퀄과 더블 이퀄을 같게 구현하므로 그 키가 Sharable이면 IdentityDictionary는 Dictionary와 마찬가지로 작용한다.

우리는 클래스 측에 initialize를 구현하고 있으므로 initialize가 행한 작업을 원상태로 돌리기 위해선 release를 구현해야 할 것이다:

Image class>>initialize
	"Set the class' initial state."
	"[Image initialize]"
	Smalltalk
		at: #ImagePool
		put: IdentityDictionary new.
	^self

Image class>>release
	"Prepare the class to be deleted."
	"[Image release]"
	Smalltalk at: #ImagePool put: nil.
	Smalltalk removeKey: #ImagePool.
	^self

그리고 난 후 전역 변수를 생성하고 초기화하기 위해 initialize 메서드를 실행해야 할 것이다. 이제 전역 변수로 접근하기 위해 메서드를 구현해야 한다:

Image class>>imageCache
	"Return the Image caching dictionary."
	^ImagePool

다음으로 Sharable 생성 메서드를 구현한다. 각 메서드는 Image를 생성하여 반환할 것이다. “create<icon name>Icon”과 같은 명명 규칙을 따르는 것이 도움이 된다. 예를 들어, Help 기능과 Save 기능에 대한 아이콘이 필요하다면 createHelpIcon과 createSaveIcon의 아이콘 생성 메서드의 이름을 지정해야 한다.

어떠한 특정 클래스도 Help 아이콘의 책임을 갖지 없다고 치자. 그러면 Image에 기본으로 구현할 것이다. Image 인스턴스의 생성은 꽤 세부적이며 Sharable 패턴을 이해하는데 있어 그다지 중요하지 않다. 이미지에 대한 비트맵과 그것을 어떻게 만들지를 명시하는 수많은 파라미터를 가진 CachedImage class>>on:extent:depth:bitsPerPixel:palette:usingBits: 메서드의 실행을 수반한다고 말하면 충분하겠다. 예를 들어, 비주얼웍스 런처에 표시되는 도움말 책 이미지를 생성하고 반환하는 VisualLauncher class>>CGHelp32 메서드를 참조한다.

createHelpIcon의 구현부는 다음과 같은 모습일 것이다:

Image class>>createHelpIcon
	"Create and return the Image for the Help icon."
	^CachedImage
		on: ...
		extent: ...
		depta: ...
		bitsPerPixel: ...
		palette: ...
		usingBits: ...

하지만 시스템의 모든 아이콘을 Image의 클래스 측에 구현한다면 클래스 측은 고아(orphan) 아이콘 정의로 찬 쓰레기 하치장이 될 것이다. 아이콘 정의를 사용하는 코드로 아이콘 정의를 캡슐화하는 편이 낫다. Save 아이콘이 주로 메인 메뉴 창에서 사용된다고 치자. 그렇게 되면 그 클래스는 (예: MainMenuUI) createSaveIcon 메서드를 정의해야 한다. 이는 위에서 표시된 Image class>>createHelpIcon 메서드와 같은 모양을 할 것이다. Image는 여전히 createSaveIcon의 구현부를 필요로 하지만 이 메서드는 적절한 클래스, MainMenuUI로 요청을 전달할 것이다. 따라서 구현은 다음과 같을 것이다:

Image class>>createSaveIcon
	"Create and return the Image for the Save icon."
	^MainMenUI createSaveIcon

이제 Image의 클래스 측에 SharableFactory가 있고 이는 Image Sharable을 생성하는 메서드를 가진다. 이제 필요한 것은 Sharable에 접근하기 위한 메서드 또는 메서드들이다. [디자인 패턴] 편에서는 getFlyweight : key와 같은 메서드의 사용을 제안한다. 하지만 Sharable의 수가 고정되어 있다면 각 Sharable마다 구분된 접근자 메서드를 구현하는 편이 쉽다. 생성 메서드를 위한 명명 규칙이 "create<icon name>Icon"이었던 것과 마찬가지로 접근 명명 규칙은 “<icon name>Icon”이 되어야 한다.

따라서 helpIcon과 saveIcon은 다음과 같이 구현할 필요가 있겠다:

Image class>>helpIcon
	"Return the Image for the Help icon."
	| cacheDictionary |
	cacheDictionary := self imageCache.
	^cacheDictionary
		at: #help
		ifAbsent:
			[cacheDictionary
				at: #help
				put: self createHelpIcon]

Image class>>saveIcon
	"Return the Image for the Save icon."
	| cacheDictionary |
	cacheDictionary := self imageCache.
	^cacheDictionary
		at: #save
		ifAbsent:
			[cacheDictionary
				at: #save
				put: self createSaveIcon]

at:ifAbsent:는 어떻게 작용할까? 사전에 명시된 키를 이미 포함하고 있다면 메서드는 그 값을 반환할 것이다. 그렇지 않을 경우, 메서드는 빈 블록을 실행한다. 블록에는 생성 메서드를 실행하여 새로운 값을 얻고, 이를 사전의 명시된 키에 추가한 후 새 값을 반환한다. 이는 at:ifAbsent:로 하여금 줄곧 사전에 있었던 것처럼 새로운 값을 반환하도록 만든다. 이렇게 helpIcon과 saveIcon은 캐시에 이미 있든 없든 상관없이 항상 명시된 이미지를 반환한다.

스몰토크 방언이 Dictionary>>at:ifAbsentPut:를 구현한다면 이것을 at:ifAbsent: 대신 사용하여 helpIcon과 saveIcon의 구현을 단순화시킬 수 있다. 비주얼웍스는 이를 구현하지 않지만 ENVY를 구비한 비주얼웍스는 구현한다.

helpIcon과 saveIcon 메서드의 구현은 매우 비슷하다. 공통된 코드를 둘이 모두 사용할 법한 공통된, 파라미터화된 메서드에 추출할 수 있다. 하지만 이를 위한 코드는 복잡할 뿐더러 여기서 설명하기가 힘들다.

다음 코드는 Help 아이콘 Sharable을 검색한다: Image helpIcon.

구분된 팩토리 클래스

구분된 팩토리 클래스는 싱글톤을 (91) 이용해 구현된다는 점을 제외하면 클래스 측 팩토리와 매우 비슷하게 작용한다. 싱글톤 패턴은 다음과 같이 기입하고 있다: 싱글톤 객체는 클래스 측에 구현하지 말고 인스턴스 측에 구현한다 (DP 128). 이에 따라 가장 먼저 새로운 추상 클래스를 생성할 필요가 있다. 이것은 이미지의 캐시 저장을 위한 인스턴스 변수와 싱글톤 인스턴스의 캐시 저장을 위한 클래스 변수를 정의한다.

Object subclass: #ImageFactory
	instanceVariableNames: 'imagePool '
	classVariableNames: 'Singleton '
	poolDictionaries: ''

싱글톤을 생성하기 위해 initialize의 클래스 측 구현부와, 그를 파괴하기 위한 release, 그를 회수하기 위한 getter 메서드를 정의한다:

ImageFactory class>>initialize
	"Set the class' initial state."
	"[ImageFactory initialize]"
	Singleton := self new.
	^self

ImageFactory class>>release
	"Prepare the class to be deleted."
	"[ImageFactory release]"
	Singleton := nil.
	^self

ImageFactory class>>default
	"Return the class' primary instance."
	^Singleton

인스턴스 측은 초기 상태를 설정하기 위해 initialize를 구현하고, 그것을 호출하기 위해 (클래스 측에) 인스턴스 생성 메서드를 구현한다:

ImageFactory>>initialize
	"Set the instance's initial state."
	imagePool := IdentityDictionary new.
	^self

ImageFactory class>>new
	"Create and return an instance of the class."
	^self basicNew initialize

이제 Image의 클래스 측에 팩토리가 구현된 방식과 같이 나머지 ImageFactory를 인스턴스 측에 구현할 차례이다. 따라서 imagePool, createHelpIcon, helpIcon이 ImageFactory에 인스턴스 메서드로 정의된다.

다음 코드는 Help 아이콘 Sharable을 검색할 것이다: ImageFactory default helpIcon.

알려진 스몰토크 사용예

Character, Symbol, 그리고 SmallInteger

이 시스템 클래스들은 Flyweight로 구성된다. 모두들 어느 코드에서든 꽤 많이 사용된다.

Cursor와 ColorValue

함할 수 있다. 하지만 시스템의 다른 부분이 모두 동일한 스타일의 커서를 사용하고 있다면 (예: 일반, 실행, 쓰레기 수집) 사실상 동일한 Cursor 인스턴스를 표시하고 있는 것인데, 이는 각 커서 스타일이 클래스 내의 캐시에 저장되기 때문이다. 클래스에 특정 스타일의 커서를 (실행 커서와 같은) 요청할 때마다 당신은 캐시에 저장된 동일한 인스턴스를 수신한다. 이로 인해 동일한 커서를 반복하여 생석할 필요성을 제거함으로써ㅡ그리고 후에 쓰레기 수집의 필요성도 제거ㅡ시간을 절약해주고, 다수의 위치에 동일한 커서를 저장함으로써 메모리를 절약한다.

불행히도 Cursor는 그 인스턴스를 캐시에 저장하는 데에 Dictionary를 사용하지 않는다. 대신 각 커서 스타일이 그를 캐시에 저장할 수 있는 해당 클래스 변수를 가진다. 변수를 찾는 것이 사전 변수를 찾아 그 사전에서 값을 검색하는 것보다 빠르기 때문에 조금 더 효율적인 방법이라고 할 수 있다. 하지만 커서 스타일마다 변수를 사용하는 경우 쉬운 확장(easy-to-extend)의 기준에 어긋난다. 개발자는 기존 코드를 변경하지 않고 새로운 커서 스타일을 추가할 수 있어야 한다. 그는 새로운 생성 메서드와 (예: initTarget) 새로운 getter (target)를 추가함으로써 새로운 커서를 (예: Target 커서) 추가할 수 있다. 하지만 새 인스턴스를 캐시에 추가하려면 개발자가 Cursor 클래스 정의를 변경하여 새로운 TargetCursor 클래스 변수를 추가할 필요가 있다 (대신 전역 변수를 사용할 수 있지만). 커서가 사전에 저장되어 있다면 개발자는 클래스 정의를 변경할 필요가 없다.

이와 비슷하게 비주얼웍스의 ColorValue는 여러 개의 표준 constant를 캐시에 저장한다. 예를 들어 당신이 ColorValue blue를 실행할 때마다 서로 다른 인스턴스를 기대하지만 실제로는 동일한 인스턴스를 받을 수도 있다. 클래스는 31개의 표준 색상을 구현하며, 각각의 색상을 고유의 클래스 변수에 저장한다.

RegistrationRecord

Yelland (1996)는 Flyweight 패턴을 GUI 윈도우를 위한 실험용 비주얼웍스 프레임워크의 일부로 사용하는 것을 논하고 있다. 프레임워크는 정반대의 목표를 성취하므로 중요하다: GUI 윈도우는 호스트 (자체) 위젯을 사용하지만 다양한 윈도윙 시스템으로 이동이 가능하다 (매킨토시, MS-윈도우 3.x, 윈도우 95, 모티프 등). 프레임워크는 각 GUI 위젯을 두 개의 주요 부분, 스몰토크 객체와 호스트 위젯으로 구현한다. 각 스몰토크 위젯 객체는 그것이 응답할 이벤트마다 해당하는 호스트 위젯에 대한 의존성을 등록해야 한다; 이것은 Observer (305) 패턴이다. 관심이 있는 이벤트와 그에 상응하는 스몰토크 메시지는 특정 타입의 모든 위젯에 동일하다. 유일한 차이점은 어떤 스몰토크 위젯이 메시지를 수신할 필요가 있는지이다.

제시한 비주얼웍스 시스템은 두 개의 명시되지 않은 클래스로 의존성을 구현하는데, 이 클래스를 RegistrationRecord와 RegistrationPair라고 부르겠다. RegistrationRecord는 내부 상태를 캡슐화하는 Flyweight 클래스이다: 관심이 있는 이벤트, 해당 스몰토크 메시지, 이벤트 argument를 메시지 파라미터로 매핑. RegistrationPair는 스몰토크 위젯의 해당 호스트 위젯에 대한 의존성을 나타낸다. 이것은 이에 상응하는 외부 상태로 RegistrationRecord Flyweight를 캡슐화한다: 이벤트 메시지를 수신해야 하는 스몰토크 위젯.

관련 패턴

Flyweight 패턴 대 Singleton 패턴

Flyweight와 Singleton (91) 패턴은 자주 혼동되곤 한다. Singleton 클래스는 우선 제한된 인스턴스 수를 가진다. 제한된 수란 주로 하나를 뜻하지만 어떤 수든 고정되어 있다면 가능하다 (DP 128 참조). 반대로 Flyweight 클래스는 무한한 수의 인스턴스를 가질 수 있으나 어떤 두 인스턴스도 같은 값을 가질 순 없다. 예를 들어, UndefinedObject는 하나의 인스턴스만 가진다. Symbol은 무한한 수의 인스턴스를 가질 수 있으나 어떠한 두 인스턴스도 정확히 동일한 문자열을 포함하지는 않는다. 따라서 UndefinedObject는 Single이고, Symbol은 Flyweight이다.

클래스가 제한된 수의 인스턴스를 가진 것으로 보일 시 이 차이를 구별하기란 여전히 헷갈린다. Singleton일까, Flyweight일까? Singleton에서는 클래스가 인스턴스 수를 책임지고 제한한다. 예를 들어, 비주얼웍스의 ChangeSet 클래스는 무한한 수의 인스턴스를 가질 수 있지만 유일하게 유용한 인스턴스는 Project와 쌍을 이루는 것이다. 사실상 언제든 유용한 한 가지는 ChangeSet로서ㅡ최근 Project에 대한ㅡChangeSet는 ChangeSet current를 통해 전역적으로 이용이 가능하다. ChangeSet는 하나 이상의 인스턴스를 가질 수 있다 하더라도 Singleton에 해당하는데, 그 구현이 유용한 (또는 활성화된) 인스턴스의 수를 하나로 제한하기 때문이다. 반면 SmallInteger 클래스는 무한한 수의 인스턴스를 가질 수 있다. 그 범위가 제한된 것은 클래스의 내부 책임성 때문이 아니라 그것이 범위가 제한된 정수형 데이터를 감싸기 때문이다. 클래스의 범위는 데이터 타입과 둘 간의 커플링에 따라서만 제한되므로 SmallInteger는 Flyweight이다.

Singleton으로서의 Flyweight

FlyweightFactor는 (Flyweight가 아니라) 주로 Singleton (91)에 해당한다. 그 이유는 flyweight의 집합이 하나 밖에 없어서 시스템이 하나의 FlyweightFactory만 필요로 하기 때문이다. 이것이 바로 FlyweightFactory가 Flyweight의 클래스 측으로 구현될 수 있는 이유이다; 클래스에 하나의 클래스 측만 있지만 FlyweightFactory 인스턴스의 수는 절대로 하나 이상이 될 수 없다. FlyweightFactory가 구분된 클래스로서 구현되었다면 그 클래스는 진정한 Singleton 클래스가 될 수 있다. 이후에 시스템이 하나 이상의 FlyweightFactory 인스턴스를 필요로 할 경우 이를 허용하기 위해 클래스를 변환할 수 있다.

Flyweight 패턴 대 Proxy 패턴

Flyweight와 Proxy (213)는 매우 까다로운 패턴이긴 하나 주로 혼동되곤 한다. flyweight"란 이름을 들으면 개발자들은 실제 객체의 대체로 로드되는 가벼운(경량의) 객체를 생각할지도 모른다. 이는 사용자가 큰 객체의 큰 집합을 절대로 사용할 일이 없을 때 이러한 집합체로 로딩을 피하기 위한 공통 기법이다. 대신 경량의 대체 객체로 로드되고, 사용자가 실제로 사용하는 것은 실제 객체로 변환된다. 이는 완벽히 유효하면서도 공통된 패턴이지만 Flyweight는 아니다; Proxy 패턴에 해당한다. Flyweight는 다수의 클라이언트에 의해 투명하게 공유될 때 독립적 copy와 같은 역할을 하는 객체이다. Flyweight와 Proxy는 엄연히 다른 패턴이다; 속아 넘어가지 말자.

Flyweight 패턴과 Proxy, Adapter, Decorator 패턴들

Flyweight와 Flyweight로의 Proxy는 주로 혼합되어 사용되지 않는다. 아래 그림과 같이 두 개의 변수가 동일한 객체를 가리키고 있다고 치자. 첫 번째 변수 t1은 주제대상(subject)인 aRealSubject를 직접 가리킨다. 다른 변수 t2는 프록시인 aProxy를 통해 간접적으로 주제대상을 가리킨다. 코드는 두 변수 모두 동일한 주제대상을 가리키고 있는지 어떻게 알까?

Dpsc chapter04 Flyweight 05.png

코드가 두 변수를 비교하기 위해 이퀄을 사용할 경우, Proxy 클래스는 이러한 상황을 감지하고 (이중 가상인수를 필요로 할 수 있는 상황; Beck, 1996) true를 반환하기 위해 이퀄을 구현한다. 근본적으로 다른 변수가 (t1) 프록시와 동일한 주제대상을 가리키는 경우 변수는 동일하다.

하지만 주제대상이 Flyweight이고 클라이언트 코드가 이를 알 경우, 코드는 아마 더블 이퀄을 사용해 비교를 할 것이다. 두 개의 변수는 확실히 더블 이퀄은 아니기 때문에 클라이언트가 true를 기대하고 있을 때 검사 결과는 false를 반환할 것이다. 따라서 종종 Flyweight로 더블 이퀄을 사용 가능하지만 Proxy를 사용할 때는 더블 이퀄을 사용하지 못할지도 모른다. 이와 비슷하게 중간 객체가 Proxy (213) 대신 Adapter (105) 또는 Decorator (161)일 수도 있다. 다시 말하지만 더블 이퀄은 Flyweight 패턴과는 종종 사용되지만 그 외 이러한 패턴들과는 그다지 잘 작동하지 않을 것이다.

Notes

  1. Pool 변수들은 스몰토크에 전문화된 전역 변수로서, 그 사용이 특이하고 다행히도 드물다 (Ewing, 1994). 흥미롭게도, 어떠한 pool 변수든 그에 속한 항목들이 고유하는 방식으로 미루어 볼 때 항목들 자체가 Flyweight여서 FlyweightFactory의 인스턴스 없이 pool 변수 자체가 flyweight pool이라고 주장하는 이도 있겠지만, 패턴의 일반적 특성을 거의 없애버린 소수의 주장에 가깝겠다.