지난 호에 이어
필자들은 지난 호까지 2회에 걸쳐 패턴의 사용 경험을 기술하여 왔다. 그러나 지난 호까지 투고했던 내용들은 널리 알려진 시스템의 사례는 아닌, 필자들의 개인 경험에 의존한 사례들이었다. 패턴을 적용하면서 보고 느껴왔던 점들을 적은 글이기에 나름대로의 흥미 있는 주제가 되었으리라 기대한다.
그러나, 필자들이 만든 코드가 아닌 다른 유명한 시스템에서도 디자인 패턴이 정말 쓰이고 있는 것인가? 라고 의문을 가질 분들도 틀림 없이 있다. ( 한국 사람들은 원래 그런 것에 관심이 많아서 “어 여기서 쓰고 있네” 그래야지 좋다는 것을 깨닫는 습성이 있기도 하다. 필자들 또한 그렇다. – 개인 소견 ).
많은 디자인 패턴이 도입되어 있는 시스템에는 JAVA가 있다. JAVA에서 사용되는 여러 클래스들에는 분명히 디자인 패턴의 기법들이 도입되어 있다. 물론 JAVA VM을 만들고 JAVA를 설계한 사람들이 모두 디자인 패턴이라는 것을 의식하고 사용하였다는 것은 분명 아닐 것이다. JAVA 소스 코드나 VM소스 코드의 어느 구석을 찾아 보아도 디자인 패턴이라는 어휘는 한마디도 나오지 않는다. 이번 기사를 쓰기 위해서 VM의 일부분을 뒤지고 찾아 보았지만 디자인 패턴이라는 단어는 찾을 수 없었다.
그러나 자바의 부분들을 자세히 찾아보면 디자인 패턴의 덩어리라는 것을 알 수 있다. 특히나 자바의 환경은 JAVA에서 기본으로 제공하는 클래스들로 구성되어 있다는 사실과 이들 클래스들이 나름대로의 역할을 수행하고 상호 연동되는 과정에서 패턴이 사용된다. 알게 모르게 많은 부분들이 패턴으로 구현되어 있다는 사실은 이전에도 언뜻 생각이 들었지만, 이번 호의 원고를 준비하면서 정말 많은 부분들이 패턴으로 구현되어 있구나 라고 생각하게 되었다.
하나의 이론을 설명하는데 있어서 가장 좋은 방법은 분명 적절한 사례를 드는 것이다. 패턴의 원고를 기고하면서 이러한 실질적인 사례를 들고자 필자들은 많은 노력을 기울여 왔다. 그러나 그 노력이 필자들이 구현하였던 부분에 국한 되었다는 점 또한 사실이다. 이번 호에서는 JAVA에서 내부적으로 사용하고 있는 디자인 패턴의 사례를 들어 패턴이라는 것이 분명 널리 쓰이고 있다는 사실을 알리고자 한다.
JAVA 패턴을 설명
앞에서도 이야기 하였지만, 디자인 패턴은 자바에서도 널리 사용된다. 구체적으로 어떻게 사용되고 있기에 널리 사용되고 있다는 말을 할 수 있는가? 이때의 역할은 무엇이며 자바 내에서 어떻게 사용되고 있는가?
이러한 의문점들은 JAVA에서 사용된 패턴의 적절한 예제를 보면 하나 둘 씩 풀려나갈 것이다. 자바는 C의 속성을 지니고 있는 C++과는 달리 애초부터 인터페이스, 상속 관계 등의 개념을 가지고 만들어진 객체지향 언어이기 때문에 더 많은 부분이 디자인 패턴을 이용하여 구성되어 있다. 구체적으로 어떠한 부분이 이렇게 패턴으로 구성되어 있을까?
이번호의 기사는 이의 설명을 위해서2가지 부분으로 나누었다. 하나는 패턴 중심으로 패턴을 설명하고 이 패턴이 어떻게 자바에서 사용되고 있는 가에 대한 부분이며, 다른 하나는 기능 중심에서 특정 기능이 어떻게 패턴을 사용하는가에 관한 방법이다.
패턴 중심의 자바 패턴 설명
패턴 중심의 자바 패턴 설명은 몇몇의 중요한 패턴이 자바에서 어떤 용도로 사용되고 있는가 하는 관점에서 접근한다. 이 부분에서는 모두 4개의 패턴을 설명하려고 한다.
- Immutable Pattern
- Adaptor Pattern
- Bridge Pattern
- FlyWeight Pattern
각각의 패턴들을 기본적인 설명과 함께, 자바에서의 적용 사례와 사용하기 위한 개념을 설명하려 한다.
Immutable Pattern
이뮤터블 패턴
“이뮤터블 패턴은 다른 패턴들과 근본적으로 다르다. 이뮤터블 패턴은 적절한 곳에 적용될 때, 안정성과 유지 보수성이 비약적으로 향상된다.” – from Pattern in JAVA.
이뮤터블 패턴의 개념은 한마디로 표현할 수 있다. 한번 만든 객체는 변형하지 않으며, 변형이 발생할 때는 새로운 객체를 만든다는 것.
JAVA의 STRING객체는 String이 변형될 때 새로운 객체가 만들어 진다는 사실을 알고 있는 독자들도 있을 것이다. 이를 간단히 설명하기 위해 다음을 보자.
String A = ‘HELLO’;
String B = ‘HI’;
String C = A.concat(B);
2개의 스트링을 하나로 만드는 작업인 concat을 수행하는 시점에서 A객체에 B의 내용이 덧붙여 C에 할당되는 것이 아니라, 새로운 스트링 객체가 만들어져 C로 할당된다.
한번 생성된 스트링 객체는 결코 변하지 않는다. 객체는 변하지 않으며 다만 새로운 객체가 만들어질 뿐이다. 이뮤터블 패턴은 다음과 같은 구조로 구성된다.
1. 그림 1 - Immutable Pattern
외부의 클라이언트 객체는 읽기 전용의 인터페이스만 사용할 수 있으며, 몇몇의 허가된 클래스만이 본 객체를 변화시킬 수 있다. 객체에 변화가 발생하면 이뮤터블 패턴은 기존 객체의 내용을 변화시키지 않고 새로운 객체를 만들어 변화된 값을 할당한다. 이러한 방식으로 멀티 쓰레드에서 발생하는 여러 가지 문제들을 원천적으로 해결할 수 있다.
“어, 변형 시에 무조건 객체를 새로 생성하는 것이 무슨 패턴이야?” 라고 의문점을 표시하는 독자들도 있으리라 생각된다. 이러한 의문점처럼 이뮤터블 패턴은 복잡한 클래스 구조를 가지는 패턴은 아니다.
오히려 객체의 사용 전략에 관한 방법론에 가깝다. 한가지 분명한 것은 이뮤터블 패턴은 몇몇 장소에서 매우 유용하다는 것이다. 이들 장소 중 가장 유용한 곳은 바로 JAVA의 String객체 표현이 아닐까 한다.
Immutable in JAVA
“스트링 클래스의 인스턴스는 이뮤터블이다. 스트링 객체가 표현하는 문자들은 객체가 생성될 때 결정된다. 스크링 클래스는 스트링 객체가 표현하는 문자들을 변경하기 위한 어떠한 메서드도 제공하지 않는다. 스트링 객체에 존재하는 toLowerCase, subString등의 메서드들은 새로이 만들어지는 문자들을 새로이 생성된 스트링 객체에 할당되어 반환된다.” – from Pattern in JAVA
Immutable Pattern이 사용되는 곳은 단연코 String 객체이다.
String 객체에서 스트링의 일부를 잘라내거나, 대소문자의 변경과 같은 스트링에 변형이 발생하면 새로운 스트링 객체가 생성되고 변형된 결과는 새로 생성된 스트링 객체에 반영된다.
다음의 함수들을 보자.
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
char buf[] = new char[count + otherLen];
getChars(0, count, buf, 0);
str.getChars(0, otherLen, buf, count);
return new String(0, count + otherLen, buf);
}
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > count) {
throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {
throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
}
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
위의 2가지 함수들은 JDK의 String 클래스의 소스 코드에 포함되어 있는 2가지 함수이다.
위에서 보듯이 concat과 subString의 2가지 스트링을 변형시키는 함수는 내부적으로 새로운 스트링 버퍼를 구성하고 스트링 객체를 만드는 여러 작업들을 수행하고 마지막에는 반드시 새로운 스트링을 만들어서 반환한다.
사용하려면?
이뮤터블 패턴을 어떠한 곳에 사용할 수 있을까? JAVA의 STRING객체는 Immutable 패턴을 사용하기 위한 매우 적절한 예제이다. 그러나 필자들은 지금까지 프로그램을 짜면서 이뮤터블을 사용할만한 적절한 예제를 찾은 적이 없다.
적절한 예제를 찾을 수 없었던 원인중의 하나는 JAVA에서는 가비지 컬렉션이 자동으로 발생하기 때문에 이를 사용할 수 있지만, C++이나 다른 가비지 컬렉션을 지원하지 않는 언어에서는 사용하기 힘들다는 문제점이 있다. 새로이 생성한 객체와 이전의 객체를 어느 시점에서 메모리에서 삭제할 것인가? 이 문제를 명확하게 처리하지 않으면 Immutable객체는 사용하기 쉽지 않다.
Adaptor Pattern
어댑터 패턴
“어뎁터 클래스는 인터페이스를 구현을 통하여 클라이언트에 알려지지 않은 클래스 인스턴스를 억세스 가능하게 한다. 어뎁터 객체는 어떠한 객체가 인터페이스를 구현하였나에 관계 없이 인터페이스에 의해 정의된 기능을 제공한다.” – from Patterns in JAVA
간단히 말하자면 객체를 인터페이스와 구현 클래스로 분리하여 여러 개의 객체들이 하나의 인터페이스에 의해서 사용될 수 있도록 정의하는 것이다. 왜 이렇게 하는 것일까? 프로그램들을 재사용하기 위한 방법 중의 하나는 인터페이스와 구현 클래스의 분리이다. 기능을 이용하기 원하는 클라이언트 클래스들은 오직 인터페이스만을 사용한다.
인터페이스와 구현 클래스의 분리는 최상의 캡슐화 효과를 가져온다. 단지 인터페이스만을 사용하는 클라이언트 클래스에서는 인터페이스를 구현한 구현 클래스의 내부적인 상태에 접근할 방법이 없다.
2. 그림 2 - Adapter Pattern
위의 그림에서 어뎁터는 인터페이스를 제공하여 외부에 노출시킨다. 정작 어뎁터 클래스 자신은 작업을 수행하기 위해서 어뎁티(Adaptee) 클래스를 사용한다. 이 부분은 어뎁터 클래스가 단순한 클래스 인터페이스 분리와 다르다는 것을 암시한다. 어뎁터 클래스에는 구현이 거의 존재하지 않으며 이름 그대로의 어뎁터 역할을 담당한다. 어뎁터는 클라이언트 클래스에 기능을 제공하기 위한 중간 클래스의 역할을 담당한다.
결과적으로는 기능을 구현한 어뎁티 클래스와 기능을 사용하는 클라이언트 클래스는 상호 독립적으로 존재하게 된다. 그리고 어뎁터 클래스를 이용하여 어떤 메서드가 호출될지를 다이나믹하게 설정할 수 있다. 데이터 베이스 데이터를 가지고 오는 클래스가 구현되어 있고 이를 어뎁터를 사용하여 이용하였을 때, 데이터 베이스가 아닌 XML 과 같은 다른 데이터 소스에서 정보를 가져올 때도 클라이언트가 어뎁터만 변경하면 동일한 인터페이스로 접근할 수 있다.
Adaptor in JAVA
“자바 API는 당장 사용할 수 있는 어떠한 공용 어뎁터 객체도 포함하지 않고 있다. 다만, java.awt.event.WindowAdapter와 같은 직접적으로 사용되기 보다는 간접적으로 상속받아 사용될 수 있게 하기 위한 클래스들을 포함한다. 이는 WindowListener와 같은 여러 개의 메서드를 선언하는 어떤 이벤트 리스너가 존재한다는 사상이다. 많은 경우에 있어서 모든 메서드들이 구현될 필요는 없다. WindowListener인터페이스는 모두 8가지의 서로 다른 종류의 윈도우 이벤트에 따라 호출되기 위한 8개의 모서드를 선언한다. 이중에서 1개내지 2개의 이벤트만이 주로 관심의 대상이 된다. 관심의 대상이 아닌 이벤트에 상응하는 인터페이스들은 do-nothing 구현 방법으로 처리된다. WindowAdapter 클래스는 WindowListener 인터페이스를 구현하지만, 이를 모두 Do-Nothing방식으로 구현한다. WindowAdapter 클래스를 상속받는 어뎁터 클래스는 관심 대상이 되는 주 이벤트를 처리하는 인터페이스만 구현하면 된다. 나머지는 do-Nothing 으로 상속되어 처리된다.
addWindowListener(new WindowAdapter() {
public void windowClosing(WindoeEvent e) {
exit();
} // windowClosing(WindowEvent)
};
위의 코드 예제에서 익명 어뎁터(Anonymous Adapter)클래스는 WindowAdapter 클래스를 상속 받는다. 이는 단지 windowClosing 매서드만을 구현한다. 나머지 7개의 이벤트는 WindowAdapter 클래스의 do-Nothing 구현으로 처리된다.” – from Pattern in JAVA
AWT,SWING의 경우 Window UI, Button, Box등의 객체 들과 관련되어 많은 부분에서 패턴이 사용되고 있다. 어뎁터 패턴도 역시 AWT의 윈도우 어뎁터에서 사용된다.
3. 그림 3 - Adapter Pattern on JAVA
상기의 그림에서 유저가 정의한 PublicWindowAdapterImplementation클래스 부분이 어뎁터를 구현하는 클래스이다. 이 클래스는 Abstract Class인 WindowAdapter를 상속 받아 인터페이스를 구현한다. 구현은 다른 적절한 클래스를 호출하여 정의된다. 여기서 클라이언트 어플리케이션은 이벤트를 생성하는 곳이며, 생성된 이벤트에 의해서 인터페이스가 호출되면 어뎁터를 구현한 PublicWindowAdapterImplementation클래스의 메서드가 수행된다.
사용하려면?
어뎁터 패턴을 사용하는 것은 코드의 재활용성을 높일 수 있다. 그러나 어뎁터를 사용하는 것은 물리적으로는 서로 다른 기능을 논리적인 개념의 인터페이스로 묶어 사용하는 것으로 적절한 인터페이스를 정의하는 것이 쉽지는 않다. 예를 들어 XML에서 데이터 억세스하는 부분과 DB에서 데이터 억세스하는 것을 하나로 통합하기 위해서 어뎁터로 구성하는 부분에서 공용 인터페이스로 정의할 수 있는 부분도 있으나 그렇지 못하고 데이터 소스에 따라 별도로 분리하여 정의되어야 할 부분도 있다. 이러한 차이점을 극복하고 적절한 인터페이스를 정의한다는 것은 쉽지 않은 일이다. 그렇다고 공통되는 부분만을 인터페이스로 구현하면 그렇지 않은 부분은 어떻게 적절히 설정할 것인가? 이 부분의 해결에는 경험과 노하우가 필요하다.
Bridge Pattern
브리지 패턴
“브리지 패턴은 추상의 계층관계와 상응하는 추상의 구현이 존재할 때 유용하다. 추상화 클래스와 구현 클래스를 여러 개의 구분된 클래스로 나누어 구현하지 않고, 추상클래스와 구현클래스를 별개의 클래스로 구현하여 이들이 다이나믹하게 조합될 수 있게 한다.” – from Patterns in JAVA
브리지 패턴은 추상화에 관계된 클래스들이 이들 추상 객체를 구현하는 클래스들과 분리될 수 있도록 한다. 추상 클래스들의 계층관계가 존재할 때, 이들을 구현하는 클래스들의 계층관계도 존재한다.
4. 그림 4 - Bridge Pattern
위의 그림에서 추상 객체들간의 계층관계는 이를 상속 받아 구현하는 객체들의 추상관계에 그대로 반영된다. 이 같은 브리지 패턴은 하나의 클래스만이 추상관계를 이용하여 독립적으로 분리되는 개념이 아닌, 일련의 클래스의 집합들이 동등한 계층관계유지하며 유지된다는 점에서 어뎁터 패턴과의 차별성을 지닌다.
한마디로 추상 객체를 1개가 아닌 여러 개를 정의하고, 이들을 사용하여 프로그램을 구성한 다음, 이들을 구현하는 여러 개의 구현 클래스를 만들어 프로그램을 완성한다. 구현 클래스는 하나의 상황에 대한 것만이 아니며, 여러 개의 상황에 대해서 여러 벌의 구현 클래스 집합이 적절하게 구성된다.
예를 들어, 데이터 억세스를 위한 일련의 추상(Abstract)클래스들을 정의하였을 때, 이를 상속 받는 XML 데이터 억세스 클래스들과 DB 데이터 억세스 클래스들의 2가지 클래스 집합을 구현할 수 있다. 추상 클래스가 비록 여러 개 이지만, 2가지 클래스 집합에서 어느 집합을 사용하는가에 따라 클라이언트의 코드 변화 없이 기능 변경이 가능하다.
Bridge in JAVA
“자바 API는 java.awt 패키지를 포함하고 잇다. 이 패키지는 Component클래스를 포함한다. Component클래스는 모든 GUI컴포넌트들의 공용 로직을 켑슐화하는 추상 클래스이다. 콤포넌트 클래스는 Button, List, TextField와 같은 서브 클래스들을 가지고 있으며, 이들은 운용환경에 의존적인 GUI 컴포넌트들을 캡슐화한다.
Java.awt.peer 패키지는 ComponentPeer, ButtonPeer, ListPeer, TextFieldPeer등의 인터페이스들을 가지고 있으며, 이들 인터페이스는 컴포넌트 클래스의 하위 클래스들이 구현될 때 요구되는 운용 환경에 의존적인 메서드들이 선언되어 있다. 컴포넌트 클래스의 하위 클래스들은 이들을 구현하는 객체들을 생성하기 위해 Abstract Factory 패턴을 사용한다. Java.awt.toolkit 클래스는 Abstract Factory의 역할을 담당하는 추상 클래스이다. 운용환경은 구현 클래스들을 생성하기 위한 Concrete Factory 클래스와 구현 객체를 제공한다.” – from Pattern in JAVA
다음의 클래스 다이어그램은 자바 소스에서 추출한 것이다.
5. 그림 5 - Bridge Pattern On JAVA ( Button on AWT )
위의 클래스 다이어그램에서 추상 객체인 Component와 ComponentPeer를 구현 객체인 Button과 ButtonPeer가 상속 받아 실질적인 동작을 담당한다. 여기서 주목할 만한 점은 Component와 ComponentPeer의 관계가 Button과 ButtonPeer라는 상속받은 객체에서도 동일하게 유지된다는 점이다.
사용하려면?
추상(Abstract) 클래스들의 메커니즘을 미리 정의하고, 이를 상속 받아 구현하는 클래스들을 여러 개 구현한다는 것은 간단하지 않다. 일단 추상 클래스들의 레벨에서 위와 같이 동작하는 것을 생각하기에는 힘들다.
“Pattern in JAVA”책에서 나온 예제로는 센서들의 집합을 예로 들었다. 2개 회사의 센서들을 사용하는 경우를 예를 들어 설명한다. 센서는 여러 종류가 있지만 이들간의 상호 연관관계를 브리지 패턴을 이용하여 추상관계로 이끌어 내어, 센서 집합의 사용을 구현하는 회사와 독립적으로 가져갈 수 있게 하였다.
이러하듯, 브리지 패턴은 둘 이상의 객체의 연관관계가 추상화 될 필요가 있을 때 유용하다. 반면, 이런 관계는 의식적으로 구현하려고 노력하지 않으면 생각하는 것이 쉽지 않기 때문에 적용이 힘들다는 단점도 있다.
FlyWeight Pattern
플라이 웨이트 패턴
“동일한 정보를 가지고 있는 객체의 인스턴스들이 상호 교환되어 사용될 수 있다면, 플라이 웨이트 패턴은 하나의 객체로 정보를 공유하여 여러 개의 객체 인스턴스로 인한 비용 소모를 피할 수 있다.” – from Patterns in JAVA
동일한 정보를 가지는 객체들을 재활용하여 객체의 수를 최소화 하자는 것이 플라이 웨이트 패턴의 개념이다. 이들 공용 객체는 공용 플라이 웨이트 풀(POOL)에 저장되며, 이를 사용하는 클라이언트 객체는 해당 객체를 객체 풀(POOL)에서 찾아 레퍼런스 만을 유지한다. 만약 적절한 객체가 존재하지 않으면 객체를 생성하고 이를 객체 풀(POOL)에 저장한다.
6. 그림 6 - FlyWeight Pattern
위의 그림에서 플라이 웨이트 객체들은 공용적으로 사용될 객체(SharedConcreteFlyweight)과 공용적으로 사용되지 아니할 객체(UnsharedConcreteFlyweight)의 2가지의 플라이 웨이트 객체로 구현된다. 서버에서는 이들 객체를 풀에 넣고 재활용한다. 플라이 웨이트를 사용하는 클라이언트 클래스는 모든 플라이 웨이트 객체를 팩토리 클래스를 이용하여 생성한다. 생성시점부터 도일한 정보를 가지는 객체가 존재하는지의 여부를 플라이 웨이트 풀에서 확인한다.
플라이 웨이트 패턴에서 흔히 나오는 예제는 워드프로세스의 구현에서 문자 집합 부분이다. 문자 객체는 한 문자 당 1개만이 생성되며, 이를 사용하는 문자열이나 문장에서는 문자 객체에 대한 레퍼런스만을 유지하며 새로이 생성하지 않는다.
FlyWeight in JAVA
“자바는 문자열 어휘로 스트링 객체를 표현하는데 플라이 웨이트 패턴을 사용한다. 만약 프로그램내에 동일한 문자순서로 구성된 어휘들이 존재한다면, 자바 가상 머신은 동일한 스트링 객체로 이들 어휘들을 표현한다. 스트링 클래스의 intern 메서드에서 모든 문자 어휘들을 표현하는 스트링 객체들을 관장한다.” – from Patterns in JAVA
자바에서 사용하는 플라이 웨이트 패턴은 스트링 객체의 생성부분에서 나타난다. 하나의 스트링을 생성하라는 요청이 들어오면 String객체의 intern이라는 메서드를 통하여 스트링을 생성하는데, 이는 자바 VM의 String Flyweight Pool을 검색하여 동일한 스트링이 있는지를 확인한다. POOL에 동일 스트링이 존재하면 존재하는 스트링을 그대로 반환한다. 그렇지 아니하면 POOL에 새로운 스트링을 생성하여 저장하고 새로운 스트링을 반환한다.
String은 매우 빈번하게 생성되는 객체이며, 생성시에 Intern은 매번 호출 되어야 한다. 속도의 향상을 위해서 Intern은 내이티브(Native) 매서드로 구현되어 있다.
JVM내부의 String Native Implementation ( from Win32 JDK1.2.2 Source )
Hjava_lang_String *
internString(Hjava_lang_String *str)
{
Hjava_lang_String *result;
struct Classjava_lang_String *strObj;
int length;
unicode *chars;
int index;
string_bucket_type *bucket;
sys_thread_t *self = sysThreadSelf();
HEAP_LOCK(self);
strObj = unhand(str);
chars = unhand(strObj->value)->body + strObj->offset;
length = strObj->count;
index = string_hash_fun(chars, length) % HASH_TABLE_SIZE;
for (bucket = string_hash_table[index]; bucket; bucket = bucket->next) {
if (bucket->string == str ||
stringEqual(bucket->string, chars, length)) {
result = bucket->string; // 스트링 객체의 존재 여부를 HashSet에서 확인
goto unlock;
}
}
if (free_string_buckets) {
bucket = free_string_buckets;
free_string_buckets = free_string_buckets->next;
n_free_string_buckets--;
} else {
bucket = (string_bucket_type *)sysMalloc(sizeof(string_bucket_type));
if (bucket == NULL) {
result = NULL;
goto unlock;
}
}
bucket->string = str;
bucket->next = string_hash_table[index];
string_hash_table[index] = bucket;
result = bucket->string; // String객체를 HashSet에 추가.
unlock:
HEAP_UNLOCK(self);
return result;
}
위에 나온 소스 코드는 Win32 JDK내부의 자바 VM의 스트링 객체 처리 부분에서 Intern함수를 구현한 C언어 소스 코드이다. 위의 예제에서 보듯, 일단 새로운 스트링 생성의 요청이 들어오면 Pool의 HashSet을 이용하여 객체가 존재하는지를 확인한다. JDK의 소스 코드가 C++이 아닌 C로 구현된 함수들로 이루어 져 있다. 한가지 명심할 것은 패턴을 어떤 언어로 구현하는가는 중요한 것이 아니다. 중요한 것은 패턴의 개념을 명백히 이해하고, 그 개념을 도입하여 프로그램을 만드는 일이다.
사용하려면?
자바의 String객체는 플라이 웨이트 패턴의 매우 적절한 예제이다. 플라이 웨이트 패턴을 사용하기 위해서는 반복적으로 빈번하게 사용되면서도 공유될 수 있는 객체들이 존재해야 한다. 워드 프로세서에서 스트링 처리를 위한 문자 객체나, HTML 구문 처리를 위한 ELEMENT객체 등은 플라이 웨이트 패턴이 사용될 수 있는 장소이다.
많은 경우에 있어서 플라이 웨이트 패턴이 이뮤터블 패턴과 함께 적절히 사용된다는 점을 감안하면 프로그램 작성시에 플라이 웨이트 패턴을 적용할 만한 부분을 찾기는 쉽지 않은 일이다.
기능 중심의 패턴 사용 설명
다음은 JAVA의 패키지와 기능 별로 디자인 패턴이 어떻게 사용되고 있는가를 소개한다. 소개하는 부분은 다음의 3가지 부분이다.
- IO Package에서 IO 처리
- RMI에서 Proxy패턴의 사용
- Collection객체에서의 Single Threaded Execution
각 부분에서의 어떤 기능이 패턴을 어떠한 방식으로 사용하고 있는 가를 설명한다.
IO 패키지 ( java.io )
IO 패키지는 자바에서 가장 많이 사용되는 동시에 사용법이 간단하여 가장 쉽게 이해할 수 있는 패키지중의 하나이다. 심지어 “Hello World”를 화면에 처음 출력해 보는 초보자에게도 필요한 패키지이다. 이처럼 친숙한 IO 패키지내에 숨어 있는 디자인 패턴을 점검해 보기로 하자.
이 패키지는 기둥이 되는 네가지의 추상 클래스(Abstract Class)가 존재한다. 즉 InputStream과 Reader, OutputStream과 Writer가 그것이다. InputStream과 OutputStream은 바이트를 처리의 기본 요소로 하며 Reader와 Writer는 캐릭터를 기본 요소로 한다. 좀더 자세히 살펴 보기 위해 그림 7의 InputStream 클래스 계층도를 보자. 이 클래스 계층도에서는 deprecation된 메소드인 StringBufferInputStream, LineNumberInputStream은 제외하였다.
7. 그림 7 - InputStream Class Diagram
최상위에는 InputStream가 존재하는데 이 InputStream은 바로 Template Method 패턴을 따르는 추상 클래스이다.
[Box 1] Template Method 패턴 삽입
추상 클래스인 InputStream에서 abstract로 선언되어진 메소드는 단 하나 read 메소드이다.
public abstract class InputStream {
….
public abstract int read() throws IOException;
…
}
예를 들어 skip() 메소드와 read(byte[], int, int) 메소드는 내부적으로 read() 메소드를 사용하여 구현되어 있다. 나머지 메소드들은 Template Method에 속하며 read 메소드는 서브 클래스에서 구현해야 될 Primitive Method라고 할 수 있다. 이처럼 Template Method 패턴을 사용하게 되면 알고리즘과 같은 공통적인 절차를 독립화시킬 수 있게 되는 장점이 생긴다.
이제 하위 클래스들을 점검해 보도록 하자. InputStream을 바로 상속받고 있는 클래스들은 SequenceInputStream, PipedInputStream, FileInputStream, ByteArrayInputStream, FilterInputStream, ObjectInputStream이 있다. 이중 FilterInputStream은 추상 클래스로 존재하며 다시 FilterInputStream을 상속받는 서브 클래스로는 BufferedInputStream, PushbackInputStream, DataInputStream이 있다.
이때 잠깐 살펴 볼 것이 왜 InputStream을 직접 상속받지 않고 FilterInputStream을 상속받는 클래스들이 존재하는가에 대한 것이다. 그 해답은 FilterInputStream의 구조를 들여다 보면 어느정도는 풀린다. InputStream은 추상 클래스인 반면 FilterInputStream은 일반 클래스라는 차이점이 우선 눈에 띈다. 그러나 더 중요한 차이는 FilterInputStream의 생성자에 있다. FilterInputStream의 유일한 생성자는 다음과 같이 정의된다.
public FilterInputStream(InputStream in)
즉 FilterInputStream에서는 입력받은 InputStream을 사용해서 모든 행위가 이루어진다. 일종의 delegation이라고 보면 된다. 따라서 기존에 이미 있는 InputStream을 기반으로 행위가 벌어지기 때문에 Filter를 앞에 붙여 놓은 것이라고 할 수 있다. FilterInputStream을 상속받는 세개의 클래스 모두 독립된 데이터 소스를 만들기 보다는 이미 만들어져 있는 데이터 소스를 변형하는 역할을 하고 있음을 확인해 보기 바란다.
다시 패턴 이야기로 가보자. PipedInputStream은 PipedOutputStream과 더불어 Producer-Consumer 패턴을 이룬다. Producer-Consumer 패턴은 Producer와 Consumer, 그리고 Queue라고 불리는 버퍼 이렇게 세가지 요소로 구성되어 있는데, 자바 구현에서는 Queue와 Consumer의 역할을 PipedInputStream이 수행하고, Producer 역할은 PipedOutputStream이 수행하게 된다.
[Box 2] Producer Consumer 패턴 삽입
PipedOutputStream은 내부적으로 PipedInputStream을 지니고 있어서 write()에 의해 데이터가 도착할때마다 PipedInputStream의 receive()를 호출하여 버퍼에 데이터를 저장하게 된다. PipedInputStream의 read() 메소드에서는 다음의 코드에서 볼 수 있듯이 데이터의 유무에 따라 대기 여부를 결정한다.
public synchronized int read() throws IOException {
……………
while (in < 0) {
// no data
……
/* might be a writer waiting */
notifyAll();
try {
wait(1000);
} catch (InterruptedException ex) {
throw new java.io.InterruptedIOException();
}
}
int ret = buffer[out++] & 0xFF;
……………….
return ret;
}
Reader 클래스의 계층도는 그림처럼 InputStream의 계층도와 아주 유사하다. 차이점을 정리해 보면 다음과 같다.
l Character중심인 Reader계열에서는 DataInputStream과 ObjectInputStream에 해당하는 클래스가 없다.
l Byte 스트림에서 Character Encoding을 해주는 InputStreamReader가 추가된다. 이에 따라 FileReader는 InputStreamReader를 상속하게 된다.
l BufferedReader가 Reader를 바로 상속한다.
OutputStream과 Writer의 계층도는 InputStream과 Reader와 유사하므로 이글에서는 따로 다루지 않겠다.
8. 그림 8 - Reader Class Diagram
가끔 자바를 처음 배우기 시작한 프로그래머들에게서 다음과 같은 코드를 본다.
public void doOperation(FileWriter writer)
{
String data = ………; // data allocation
writer.write(data);
}
아무 이상이 없는가? 물론 동작에는 문제가 없다. 그러나 활용성이 떨어진다. 바로 FileWriter를 사용했기 때문이다. doOperation내에서 사용되는 writer가 만일 FileWriter의 특정한 메소드를 사용하지 않는다면 doOperation의 signature는 doOperation(Writer writer) 로 바꾸어주는 것이 좋다. 그렇게 되면 FileWriter뿐만 아니라 다른 Writer를 상속받은 클래스들을 가지고도 doOperation을 호출할 수 있게 되기 때문이다.
한걸음 더 나아가 메소드를 디자인할 때 가급적 클래스 계층도의 상위 클래스 즉 Super클래스(혹은 인터페이스)를 패러미터로 사용하게 되면 그 메소드는 광범위하게 쓰일 수 있게 될 것이다. 이것이 바로 객체 지향 언어에서의 다형성(Polymorphism)의 힘이다.
RMI 와 Proxy 패턴
요즘은 RMI를 사용해서 직접 프로그래밍하는 예는 거의 없다. 그것은 Sun이 제공하는 RMI 런타임의 Scalability가 떨어져서이기도 하지만 EJB가 서버쪽의 자바 컴포넌트 표준으로 거의 굳어진 것이 더 큰 이유가 될 것이다. 그렇다면 RMI는 완전히 사라졌을까? 엔터프라이즈 레벨의 컴포넌트 표준인 EJB나 임베디드 시스템 레벨의 표준인 JINI 모두 기본 사상은 RMI에서 출발했다고 할 수 있다. EJB 내부에서 사용되는 프로토콜중의 하나는 JRMP(RMI의 트랜스포트 프로토콜로써 EJB가 지원하는 다른 하나의 프로토콜은 IIOP임)이며 EJB의 클래스 설계는 홈 인터페이스를 제외하면 RMI와 유사하다. JINI는 내부적으로 RMI 매커니즘을 사용하여 구현되어 있다.
RMI에서는 인터페이스에서부터 스텁 클래스(stub class)와 스켈리턴 클래스(skeleton class)가 rmic에 의해 자동 생성된다. 마치 EJB에서 ejbc(Weblogic EJB Container의 경우)라고 불리는 내부 구현 클래스 자동 생성툴이 있는 것처럼 말이다.
이제 HelloWorld라는 RMI 객체를 예를 들어 보자. HelloWorld RMI 객체의 어느 부분이 프록시 패턴의 어느 부분에 해당하는지 확인해 보도록 한다.
[Box 3] Proxy 패턴 (Structural Pattern) 삽입
9. 그림 9 - HelloWorld RMI Object Class Diagram
위의 그림에서처럼 HelloWorld라는 RMI 객체를 만들었다고 치자. 클라이언트 입장에서는 HelloWorld 하나만을 사용하게 된다. 그러나 내부적으로는 HelloWorld_Stub이라는 스텁이 구동되는 것이고 다시 이 스텁은 원격지의 HelloWorld_Skeleton을 호출하게 된다. 다시 HelloWorld_Skeleton은 HelloWorldImpl을 사용하여 비로소 클라이언트가 요청한 일을 수행하게 된다. 그렇다면 프록시는 무엇이었을까? 바로 rmic에 의해 자동생성된 HelloWorld_Stub인 것이다. 프록시 패턴과 HelloWorld 예제와의 매핑 관계를 정리해 보면 다음과 같다.
l Subject <-> HelloWorld
l Proxy <-> HelloWorld_Stub
l RealSubject <-> HelloWorld_Skeleton, HelloWorldImpl
그리고 RMI는 서로 상이한 어드레스 스페이스의 자바 객체를 호출할 수 있게 해 주므로 프록시 패턴중에서 원격 프록시에 해당한다. 프록시 패턴이라는 개념은 분산 환경에서 두고 두고 쓰이므로 이번 기회에 패턴을 잘 익혀 두면 활용할 기회가 많이 있을 것으로 생각된다.
Collection과 Single Threaded Execution 패턴의 관계
Collection Framework을 정리하면 다음의 테이블로 나타낼 수 있다. 인터페이스와 구현이 분리되어 유연하게 디자인되어 있음을 확인해 볼 수 있다. Set과 List 인터페이스는 Collection 인터페이스를 상속하는데 반해 Map 인터페이스는 상속하는 인터페이스가 존재하지 않음에 유의한다.[1]
|
구현 | ||||
Hash Table |
Resizable Array |
Balanced Tree |
Linked List | ||
인터페이스 |
Set |
HashSet |
|
TreeSet |
|
List |
|
ArrayList |
|
LinkedList | |
Map |
HashMap |
|
TreeMap |
|
JDK 1.0에서부터 존재하던 Hashtable과 Vector는 단일 쓰레드만이 접근할 수 있는 구조로 되어 있었다. 이처럼 단일 쓰레드만의 접근을 허용하는 것을 Single Threaded Execution 패턴(이하 줄여서 STE 패턴이라 함)이라고 한다. 자바에서 STE 패턴은 synchronized 메소드나 synchronized 블록을 사용해서 구현되어진다.
Collection Framework에 속한 Set, List, Map은 모두 STE 패턴을 따르도록 디자인되어 있지 않으므로 STE를 따르도록 하려면 다음처럼 해줘야 한다.
Collection c = Collections.synchronizedCollection(myCollection);
...
synchronized(c) {
Iterator i = c.iterator();
while (i.hasNext())
foo(i.next());
}
List, Set, Map 인터페이스를 명시하고 싶은 경우 각각 synchronizedList(), synchronizedSet(), synchronizedMap()을 사용도록 한다.
이번호의 마무리.
지금까지 자바 클래스와 자바 VM에서 사용된 패턴들의 예제를 보아왔다. 분명 자바에서도 많은 부분이 디자인 패턴을 이용하여 구현되었다. 이는 클래스에 기반 한다는 자바의 속성때문이기도 하다.
이 글은 2명의 필자가 함께 쓰고 있다. 이번 호도 2명의 필자가 함께 쓰고 있지만 이중에서 이번호의 마무리 글을 쓰고 있는 “나”는 현재 이모션의 개발 실장으로 있다. 잠시 나의 입장에서 마무리 글을 써 보려고 한다.
이모션의 개발 실장인 나는 최근에 개발부의 연구원 2명으로부터 2가지 흥미 있는 질문을 받았다. 우리나라에서는 MS-SQL이나 ORACLE, Windows 2000과 같은 훌륭한 소프트 웨어를 만들수가 없느냐는 것과 앞으로 계속 프로그래머의 길을 가려면 무엇을 어떻게 공부해야 하는가에 관한 질문이었다.
이들 질문은 새삼스러운 것은 아니었다. 글을 쓰는 나도 진작부터 그것을 고민하고 있었다. 그러나 다시 한번 새삼스럽게 젊은 연구원 2명으로부터 그러한 질문을 받게 되니 감회가 새로 왔다. 그리고 그들에게 나 자신이 생각해왔던 바를 이야기 해 주었다.
“나도 그 문제를 고민해 왔습니다. 프로그래머의 길을 10년이고 20년이고 가고 싶다고 생각하는 것은 저도 동일합니다. 한국에서 멋진 소프트웨어를 만들고 싶기도 하고.
그것을 아십니까? 객체 지향의 개념이 나온 지 이미 20년이 다 되었다는 사실을 말입니다. 그러나 아직 프로그래머 중에는 객체 지향의 개념을 가지고 프로그램을 만드는 사람은 그다지 많지 않습니다. COM+, EJB의 시대를 말하지만 정작 객체 지향의 개념을 이해하고 개념에 따라 프로그램을 구성하는 사람은 많지 않았습니다. 저는 수십 명의 프로그래머들과 이들이 만든 많은 코드들을 보아왔습니다.
다른 프로그래밍 선진국에는 많은 프로그래머들이 있습니다. 그들은 적어도 우리보다는 안정성 있고, 신뢰성 있는 제품을 만들어 내는 것 같습니다. 나사의 우주 정거장 건설 프로젝트의 프로그램 소스 라인이 몇 라인일까요. 무려 10억 라인이나 된다고 들었습니다. 지금은 더욱 많을 것입니다. 이러한 프로젝트가 오차 없이 동작하려면 얼마나 많은 노력과 고민을 하였을까요. 단순히 잘짜는 것만으로는 부족합니다.
체계적으로 이론을 공부하고 지속적인 관심을 가져야 합니다. 그래서 스스로가 확신을 가질 수 있는 코드를 만들어야 합니다. 자신이 구현한 코드에 만든 자신이 자신감을 가지고 있어야 합니다. 코드를 만들 자신감을 가지려면, 코드가 동작하는 원리를 명백하게 이해하고 코드를 작성해야 합니다. 개념이 확립되지 않은 상태에서 주먹 구구식으로 코드를 구현하면 언젠가는 문제를 일으킵니다.”
위의 대답을 하여 주면서 디자인 패턴의 이야기를 하고 싶었다. 개념의 설립과 동작 원리의 이해를 위한 최상의 방법중의 하나는 디자인 패턴을 이용하는 것이라고 말하고 싶었으나, 아직 그런 말을 할 단계는 아니어서 위의 대답으로 마무리 지었다. 패턴 도입 효과의 좀더 객관적인 자료를 준비하고 패턴을 언급하여야겠다는 생각에서 였다. 이제, 자바 VM과 기반 클래스에서 디자인 패턴이 사용된 사례를 이번 호에 기고하여, 나에게 질문을 한 이들에게 실질적 증거에 근거한 패턴의 유용성을 설명할 수 있게 되어 조금은 흐믓한 느낌이다.
필자의 생각은 이렇다. 프로그램을 수학에 비교하자만, 디자인 패턴은 인수분해나 사인/코사인 함수에 해당한다고. 자신이 좀더 수준 있는 개발자가 되기 위한 단계로 디자인 패턴은 반드시 익혀야 하며, 이를 익힌 프로그래머는 익히지 않은 프로그래머 보다 월등한 프로그램 도구를 가지는 것이다. 개발이라는 전쟁터에 나가는 일반 프로그래머가 권총을 가지고 나갈 때, 패턴을 익힌 프로그래머는 자동소총을 들고 가는 것과 같다. JAVA에서의 예를 보라. 정말 많은 부분에 패턴이 사용되고 있다. 이번 호에 소개한 부분은 그 일부에 지나지 않으며 다음 호에서도 자바에서의 디자인 패턴 사용을 계속 다룰 것이다. 비단 자바 뿐만이 아니다, 샌프란시스코, COM등의 많은 부분에 패턴이 사용되고 있다. 이들은 패턴이라는 무기를 들고 전쟁에서 지금까지 승리하여 왔던 것이다.
박스 1 Template Method 패턴
클래스들을 작성해 나가다 보면 공통적인 행위(Operation, Method)를 갖는 클래스 집합에 도달하는 경우가 많다. 능력이 있고 경험많은 개발자들은 디자인 단계에서도 이 클래스집합을 잡아 낼 수도 있으나 대부분은 구현 단계에서 드러나게 되어 있다. 왜냐면 디자인 단계에서 알 수 있는 클래스들의 모습은 스펙에서 유추된 것이기 때문에 구현단계의 클래스의 모습과는 동떨어져 있는 경우가 많다. 이러한 공통적인 행위를 갖는 집합은 보통 하나의 추상 클래스에 공통적인 행위들을 뽑아 내는 기법을 쓴다. 여기까지가 우리가 일반적으로 알고 있는 상속(Inheritance)이다.
Template Method 패턴에서의 추상 클래스내의 메소드는 Primitive Method(아래 그림에서의 primitiveOperation1, primitiveOperation2)와 Template Method(아래 그림에서의 templateMethod)로 나뉜다. Primitive Method는 각 서브 클래스들마다 다른 행위들을 하는 메소드들을 나타내며 abstract로 선언된다. Template Method는 이러한 Primitive Method들을 내부적으로 사용하여 알고리즘이나 절차들을 기술하는데에 사용된다.
정리해 보면 변하지 않는 알고리즘이나 절차들은 Template Method내에서 기술하고 이때 서브 클래스마다 상이하게 동작할 수 있는 Primitve Method들을 사용한다. 이 Template Method 패턴을 사용하게 되면 물론 상속의 이점인 코드 중복을 막는다는 면도 가지게 되지만 가장 큰 이점은 새로운 행위의 변화가 생겼을 때 Primitive Method를 갖는 구현 클래스만 추가하면 된다는 점이다. 기존의 절차형 언어(Procedural Language)에서는 새로운 행위의 추가 때문에 알고리즘자체의 변화(분기문의 추가라든지, 배열값의 추가등)를 가했어야 되었던 것에 비하면 유지보수가 매우 쉬워짐을 알 수 있을 것이다.
[그림] Template Method 패턴 클래스 다이어그램
박스 2 Producer-Consumer 패턴
[그림 Producer Consumer Sequence Diagram]
Producer-Consumer 패턴에서 Producer는 데이터의 생성(a data source)을 담당한다. 생성된 데이터는 Consumer에 의해 소비된다(a data sink). Queue는 중간의 버퍼역할을 담당하게 된다. Producer는 데이터를 Queue에 push()한후 다음 작업을 계속하게 된다. Consumer는 Queue에서 pop()을 시도하는데 만일 데이터가 없다면 Producer에 의해 생성될때까지 대기하게 된다.
Queue에서는 보통 버퍼 크기가 한정되어 있고 이 버퍼 크기를 넘어선 데이터가 들어오게 되면 에러가 나게 된다. 모든 데이터를 주고 받는 모듈에는 이 Producer-Consumer 패턴이 사용된다.
이 패턴을 사용할 때 염두에 두어야 할 것은 Consumer가 blocking I/O로 동작한다는 점이다. 즉 pop()이 호출된 후 이용가능한 데이터가 없다면 데이터가 생길때까지 대기하게 된다. 이러한 대기 상태를 막기 위해서는 Queue에 이용가능한 데이터가 있는지 검사해 보는 size()나 available()같은 메소드를 만들어 pop()을 호출하기 전에 조건 검사를 하도록 한다.
박스 3 Proxy 패턴
프록시(Proxy) 패턴에서 클라이언트는 직접 구현 클래스 즉 위의 그림에서는 RealSubject를 접근하지 않는다. 대신 프록시라고 불리는 대리자를 통해서 접근하게 된다. 위의 다이어그램에서는 프록시도 Subject라는 인터페이스를 통해서 구현되었다.
프록시 패턴은 굉장히 광범위하게 적용되고 있는데 GoF 책에서는 모두 네가지로 프록시 패턴을 분류하고 있어서 여기에 소개한다.
l 원격 프록시(remote proxy) – 자바 RMI에서처럼 다른 어드레스 스페이스를 지닌 객체에 대한 로컬 대리자를 제공한다.
l 가상 프록시(virtual proxy) – 실제 객체의 생성 비용이 높은 경우 가상 프록시를 먼저 생성한 후 실제 객체가 사용될 때까지 실제 객체의 생성을 늦출 때 사용한다.
l 보호 프록시(protection proxy) – 실제 객체에대한 직접적인 접근을 제한할 때 보호 프록시를 통하도록 한다.
l 스마트 레퍼런스 (smart reference) – reference를 대치하는 것으로 부가적인 작업을 한다. 예를 들어 ATL에서의 스마트 레퍼런스는 스스로에 대한 참조 카운트를 유지하며 0가 되었을 때 자동으로 free가 된다.
'SE' 카테고리의 다른 글
UML[2/4] (0) | 2008.07.25 |
---|---|
UML[1/4] (0) | 2008.07.25 |
Pattern[4/4] (0) | 2008.07.25 |
Pattern[2/4] (0) | 2008.07.25 |
Pattern[1/4] (0) | 2008.07.25 |