Extreme Programming 혹은 Agile 방법론을 능숙하게 실행할 수 있거나 혹은 그 절차를 능숙하게 정의내릴 수 있는 프로그래머 혹은 아키텍트가 되려면, 대체 무엇을 알아야 하느냐. 이런 의문이 생기더군요.
그래서 몇 가지를 정리해봤습니다.
기술에 대한 열린 마음가짐 - 어떤 형태의 기술이던 열린 자세로 받아들이겠다, 는 마음가짐이 정말 중요한것 같습니다. 이런 자세가 없는 프로그래머는 어디서 일해도 실패하기 딱 좋습니다.
사람에 대한 열린 마음가짐 - 주변 사람들이 전부 자기와 같은 가치를 갖는 사람이라는 마음가짐을 가지고, 존중하는 자세가 필요합니다. 이런 자세가 없으면 왕따당하기 딱 좋지요...
기본적인 기술들에 대한 지식들 - 이런 지식들로는 뭐가 있을까요? 간단하게 꼽아보면 얼핏 생각나는 것만해도 UML, CVS, C++, Java, Design Pattern, Refactoring, Make, Ant, TDD, jUnit, CppUnit 등등이 있군요. 여기서 제가 잘 모르는 거로는 Ant가 있네요. -_-;
사실 UML과 C++, Java에 대해서는 오래 전부터 알고 있었습니다만, Design Pattern이나 Refactoring 기법, CVS에 대해서는 공부하기 시작한지 일년 남짓밖에 되질 않습니다.
하지만 개발을 진행하면 할수록, 상대적으로 저수준의 기술(C++, Java 등, 프로그래밍 언어레벨의 기술)보다는 그 상위의 개념에 대한 지식이 보다 절실하게 느껴지더군요. 사실 Extreme Programming에 사용되는 기술들은 프로그래밍 언어보다는 추상화 레벨이 높습니다. UML 같은 것은 'Language'라고는 하지만 실제로 구현될 시스템을 기술하는 기호언어에 가깝고, Design Pattern이나 Refactoring같은 기술들은 어떤 언어에도 적용될 수 있는 중립적인 기술이라 (물론 적용 형태는 언어에 따라 조금씩 달라질 수 있겠습니다만) 저수준의 지식이라고 하기는 힘들어요.
아키텍트라고 불리는 사람들에게 요구되는 것이 프로젝트 관리 능력이라고 본다면, 그런 역할을 맡는 사람들은 저수준의 프로그래밍 언어뿐 아니라, 보다 추상화 정도가 높은 언어(그런 언어들을 메타-프로그래밍 언어라고 부를 수 있을까요?)에도 능통할 필요가 있습니다. 사실 아키텍트는 단순한 개발자(주로 프로그래밍 언어만을 사용하여 소통하는) 뿐 아니라 다른 팀이나 고객들과도 소통을 할 의무가 있거든요. 그런 사람들에게 프로젝트의 궁극적인 지향점을 설명하려면, 보다 추상화 정도가 높은 언어로 개념을 다듬는 것이 필요하다는 거죠.
좀 일찍 이런 생각을 했으면 공부를 더 열심히 했을텐데... ㅋㅋ
최근에 관련해서 읽고 있는 (혹은 다 읽은) 책들은 다음과 같습니다.
익스트림 프로그래밍
사용자 스토리
린 소프트웨어 개발
테스트 주도 개발
실용주의 프로그래머
실용주의 프로그래머를 위한 버전 관리
Java 프로그래머를 위한 UML, 실전에서는 이것만 쓴다
책을 안읽은지 너무 오래되어서 -_- 여러 책들을 한꺼번에 읽으려니까 좀 정신사납긴 하군요. ㅋㅋ
오늘 다룰 패턴은 퍼사드 패턴 Facade Pattern 입니다. 위의 다이어그램은 이 패턴의 UML 다이어그램입니다. 뭐 다이어그램은 일견 복잡해 보이기도 합니다만, 사실 알고보면 이 패턴보다 단순한 패턴도 없습니다. 이 패턴에 대한 설명을 듣고 나면, 이런 생각을 하게 될지도 모를 정도죠.
뭐... 저도 그런 생각을 했었습니다. 하지만 '사기'는 아닙니다. :-P 이 패턴은 솔직히 너무 일상적으로 '사용되고 있는 패턴'이라서, '그런 것도 패턴이라고 해야 하나'하는 생각이 들 뿐입니다. 사실 패턴이라는 게 별거 아니거든요. '문제를 푸는 데 사용되는 정형화된 방식'일 뿐입니다. 널리 사용되는 클래스 설계 기법이라면, 어떤 것이든 패턴이 될 수 있습니다. 퍼사드 패턴은, 너무나 널리 사용되고 있어서 오히려 당황스러울 지경인 설계 기법을 정리한 것에 불과합니다.
퍼사드 패턴은, 복잡한 클래스들과 패키지들을 어떤 단순한 인터페이스 객체 뒤에 감춥니다. 여기에 가시면 예제도 나옵니다만, Java의 Date 클래스는 사실 사용하기가 좀 까다롭습니다. 그런 복잡한 사용 방법은 전부 다 감추고, 사용자로 하여금 단순히 '현재 날짜에 며칠을 더하면 언제가 되는지' 알려주는 인터페이스만 보여주고 싶다고 해 봅시다.
그러면 퍼사드 클래스를 설계해서, 그 클래스가 내부적으로 Date 클래스나 기타 관련 클래스들을 조작하게 한 다음, 퍼사드 클래스가 제공하는 인터페이스만 사용하도록 하면 됩니다. 결국, 이렇게 해서 만들어진 퍼사드 클래스는 Date 클래스에 대한 '대안적'인 인터페이스가 되는 것이죠.
뭔가 복잡한 기능을 애써서 구현했는데, 그 패키지를 사용할 유저가 '그런데 인터페이스가 너무 복잡해요. 내가 원하는 건 좀 더 단순한 인터페이스인데...'하고 투덜댔다고 해 봅시다. 그러면 그 '애써서 구현한 복잡한 패키지' 앞에 유저가 원하는 좀 더 단순한 인터페이스를 얹으면 됩니다.
따라서, 퍼사드 패턴은 어떻게 보면 어댑터 패턴하고도 좀 비슷한 면이 있습니다. 다만, '어댑테이션'할 기능의 규모가 좀 더 큰 경우에 보다 적합하다고도 할 수 있겠죠.
예전에 우리 팀 다른 연구원이 네트워크에 VPN을 셋업하는 관리 기능을 열심히 구현했었습니다. 그런데 그 인터페이스를 웹 사이트에 연동할 필요가 생겼었어요. 웹 사이트를 구현하는 쪽에서 불평이 심하더군요. 그 기능이 제공하는 RMI 인터페이스들이 너무 복잡하고 무겁다는 게 그 이유였습니다. -_- 그래서 할 수 없이, 그 기능들 앞에다가 WRAPPER를 살짝 덧씌워 SOAP 서버를 만들어 줘 버렸습니다. 이 서버는 딱 두 개의 인터페이스만 가지고 있었죠 -_-
그렇게 구현한 덕분에, 웹 사이트 구현하는 쪽에서는 해당 SOAP 인터페이스를 호출하는 로직만 작성하는 것으로 구현을 마칠 수 있었습니다. 퍼사드 클래스를 구현하는 쪽에서는 약간의 노가다를 했지만, 호출하는 쪽 입장에서 보면 여러가지로 신경쓸 것들이 줄어서 좋았고, 우리는 SOAP라는 대안적인 인터페이스를 가져서 좋았습니다. 모두가 해피해 진 거죠.
어떻게 보면 퍼사드 패턴이 갖는 의의는 '협업'이 갖는 의미가 무엇인지를 잘 보여준다는 데에 있는 것 같기도 해요. '우리는 기능을 모두 다 구현했으니 댁들은 그냥 거기에 맞추세요'라고 선언해서는 프로젝트가 성공하기 어렵습니다. 다른 쪽에서 '인터페이스가 너무 골치아파요'하고 불평한다면, 그런 요구에 답할 수 있는 좋은 방법이 무엇인지 고민해 보는 게, 올바른 태도일지 모른다는 것이죠.
어댑터 패턴 (adapter pattern)은 패턴 중에서도 가장 단순한 것들 중 하나입니다. UML 다이어그램을 보면 그 의미가 비교적 명료하게 파악되는, 쉬운 패턴이기도 하죠.
일련의 메소드들(위의 다이어그램에는 methodB() 하나만 명시되어 있습니다만)을 가진, 이미 구현되어 있는 클래스 Adaptee가 있다고 합시다. Client에서는 Adaptee에 구현되어 있는 기능을 이용하려고 합니다. 그런데 문제는, Client조차도 이미 구현이 끝나있는 상태라는 것이죠. 다음의 코드를 봅시다.
class Client { private SomeInterface worker;
public Client( SomeInterface iObj ) { worker = iObj; }
public void doWork() { worker.methodA(); } }
위의 코드에 따르면, Client는 SomeInterface라는 인터페이스에 따라 구현된 객체의 methodA()를 호출함으로써 자신의 doWork() 함수를 구현합니다. 이 코드는 이미 구현이 끝난 상태라서 더 이상 변경을 할 수 없습니다. 그런데 공교롭게도, Adaptee 클래스는 이 인터페이스에 따라 구현이 된 클래스가 아닙니다. 그러므로 Adaptee 클래스에 의해 만들어진 객체는 Client 클래스의 생성자의 인자로 전달이 될 수가 없고, 따라서 Client 클래스에 의해 만들어진 객체들은 Adaptee 클래스에 의해 만들어진 객체들과는 연동을 할 수 없습니다.
그런데 Adaptee 클래스의 methodB가 사실상 인터페이스 SomeInterface가 요구하는 메소드 methodA()와 동일한 역할을 하는 것이라고 가정한다면, 이런 문제는 Adapter 클래스를 도입함으로써 간단하게 해결이 될 수가 있습니다.
class Adapter implements SomeInterface { private Adaptee adaptee;
public Adapter(Adaptee aObj) { adaptee = aObj; }
public void methodA() { adaptee.methodB(); } }
어떻습니까? 이런 클래스가 있다면, Client와 Adaptee는 다음과 같이 연동이 가능합니다.
Adaptee a = new Adaptee(); ...
Adapter adapter = new Adapter(a); Client client = new Client( adapter ); client.doWork(); // doWork calls methodA of object 'adapter', and it calls methodB of object 'a'
클래스 어댑터 패턴의 UML 다이어그램은 위와 같습니다. 그런데 이 다이어그램을 자세히 보니, Adaptor에서 Adaptee 클래스들로 이어지는 선들이 전부 'inheritance' (혹은 generalization) 관계를 따르고 있군요. 결국 '다중 상속 (multiple inheritance)'이 가능해야 클래스 어댑터 패턴을 쓸 수 있다는 이야기가 되겠네요.
앞선 예제는 SomeInterface가 '클래스'가 아닌 '인터페이스' 였기 때문에, 다중 상속을 지원하지 않는 Java에서도 클래스 어댑터 패턴을 흉내낼 수 있었습니다. 하지만 SomeInterface가 '인터페이스'가 아닌 '클래스'라면 이야기는 다르죠. Java로는 더 이상 클래스 어댑터 패턴을 구현할 수 없습니다. (꼼수를 좀 부리면 가능할지도 모르죠 :-P) C++는 다중 상속을 지원하기 때문에 당연히 가능합니다.
지금까지 어댑터 패턴에 대해서 살펴보았습니다. 단순한 패턴이라 이해도 어렵지 않고, 더 설명할 것도 없어보이는 군요. 나중에 빼먹고 언급하지 않은 이슈가 있다면, 수정해서 보충하겠습니다. :-)
지난 시간에 이어서 미디에이터 패턴에 대한 이야기를 좀 더 써보도록 하죠. 오늘은 구현에 대한 이야기를 좀 중점적으로 해보도록 하겠습니다.
자. 그러면 이제 지난 시간에 이야기했던 요구사항을 코드로 옮겨봅시다. 대략적인 Pseudo-code 수준의 Java 코드이니까 이 코드가 입력만 하면 우아하게 컴파일되어 돌아갈거라는 기대는 하지 않도록 합시다. ㅋㅋ
자. 우선 관리자가 들고다닐 단말기는 뭔가 공통된 인터페이스를 준수하도록 만들면 좋을 것 같습니다.
class Mediator;
interface Handheld { public void setWarningMessage(String msg); public void registerMediator(Mediator m); }
경고 메시지를 해당 단말기에 보내는 데 쓰일 메소드도 추가해 두었습니다. 자. 그러면 Mediator 클래스에는 Handheld 객체들을 Mediator에 등록할 메소드가 필요하겠네요.
class Mediator { private List<Handheld> handheld_list = new List<handheld>();
public void registerHandheld(Handheld h) { handheld_list.add( h ); h.registerMediator( this ); }
public void raiseWarning(String msg) { for ( Handheld e: handheld_list ) { e.setWarningMessage( msg ); } } }
여기까지는 간단하군요. raiseWarning은 Mediator에 등록된 단말들에게 Warning 메시지를 전송하기 위해 필요한 메소드입니다. 이 메소드를 호출하는 주체는 아마 Sensor가 될 겁니다. 이 메소드 덕분에, SENDOR는 어떤 종류의 Handheld 객체들이 있는지 몰라도 해당 객체들에 경고 메시지를 전송할 수 있습니다. 물론 Mediator 객체를 통해서요. 이것이 바로 Mediator 패턴의 장점입니다.
자. 그러면 Handheld 인터페이스를 구현하는 클래스 하나를 만들어 볼까요?
class PDA implements Handheld { private Mediator mediator = null;
public PDA() { mediator = null; }
public void setWarningMessage(String msg) { System.out.println("[WARNING] " + msg); // 다른 형태의 출력 코드로 바뀔 수 있음 }
public void registerMediator(Mediator m) { mediator = m; }
public boolean shutdownCoolingPan(int panID) { if ( mediator == null ) { System.err.println("Not connected to Mediator"); return false; }
return mediator.shutdownCoolingPan( panID ); } }
위의 shutdownCoolingPan 메소드는 PDA 객체를 사용하는 사용자가 임의로 공장내의 냉각기 중 하나를 꺼버리고 싶을 때 사용하게 되는 메소드입니다. 그런데 이 메소드 코드 안을 보니 Mediator 클래스 안에 shutdownCoolingPan 메소드가 정의되어야 함을 알 수 있어요. 그럼 이제 Mediator 클래스를 고쳐야겠군요. shutdownCoolingPan 메소드가 에어컨들과 통신해야 할테니, 에어컨 클래스 같은 것들도 만들어야 하겠어요.
자. Mediator 패턴의 구현은 이런 식으로 진행됩니다. 구현의 나머지 부분은 비슷비슷하니까 생략하도록 하죠.
이처럼, Mediator 패턴의 구현에는 별다른 테크닉이 필요없습니다. 참여하는 객체들이 RMI 인터페이스를 만족하도록 만들면, 쉽게 네트워크 응용으로 이식할 수도 있습니다. (물론 자바의 경우에 그렇다는 거죠 ㅋㅋ) 다만 이런 식으로 구현하게 되면 Mediator 클래스에 구현되는 메소드의 개수가 증가하게 된다는 문제는 있을 수 있습니다. 하지만 메소드 각각의 구현을 좀 더 쉽게 가져갈 수 있다는 장점이 있죠.
메소드 개수가 증가하는게 싫어서 Observer 패턴과 유사한 방식으로 통신 인터페이스를 단순화시키게 되면, Mediator 패턴 같은 경우에는 참여하는 객체들의 역할이 다 제각각이라서 각각의 객체 안에 메시지 파싱을 위한 로직을 추가해야 하는 부담이 생길 수 있습니다. 결국 객체간 통신을 위해 필요한 코드가 너무 늘어나게 되고, 결국 성능이 저하하게 될 수도 있어요.
자. 그런 trade-off에 대한 판단은 읽는 분들의 몪으로 남기고, Mediator 패턴에 대한 설명은 이것으로 접도록 하겠습니다.
오늘 살펴볼 패턴은 Mediator 패턴입니다. 이 패턴은 UML 다이어그램만으로는 그 의미가 명확하게 드러나지 않는 패턴 중 하나입니다. 보시면 아시겠지만 좌측 상단에 Mediator가 있고, 그 인터페이스를 구현하여 만들어지는 ConcreteMediator가 좌측 하단에 자리합니다. 우측 상단에는 Colleague 인터페이스가 있고, 우측 하단에는 역시 그 인터페이스를 구현하여 만들어지는 다양한 Concrete 클래스들이 있지요. ConcreteMediator는 이들 ConcreteColleague들과 association 관계를 맺고 있는데, UML 다이어그램만 봐서는 이들 association이 정확하게 무엇 때문에 필요한지가 잘 드러나지 않습니다.
사실 Mediator 패턴은 클래스 간 인터페이스의 복잡도를 떨어뜨릴 목적으로 고안된 패턴입니다. N 개의 객체가 있고, 어떤 작업을 성취하기 위해서 이들 N 개의 객체들이 서로 협업할 필요가 있다고 해 봅시다. Mediator 패턴을 사용하지 않는 경우 이들 N개의 객체들이 서로 협업하도록 만들려면, 최악의 경우 N(N-1) 개의 객체간 링크가 필요할 것입니다. (Mesh 구조를 생각해 보시면 아마 이해가 쉬우실 것입니다.)
Mediator 패턴은 이런 N(N-1)의 복잡도를 떨어뜨리는 것이 목적입니다. Mediator객체를 중간에 놓고, 협업이 필요한 객체들이 Mediator를 통해 통신하도록 함으로써 N(N-1)의 링크 복잡도를 N의 수준으로 낮추는 것이 Mediator 패턴의 목적이죠. 만일 N 개의 클래스가 있는데, 이들 클래스에 의해 만들어진 객체들이 하나의 Mediator 객체와 통신하여 상호 연동할 수 있다면, 결국 객체간 링크의 개수는 N개로 감소하게 될 것입니다.
자. 그렇다면 Mediator 패턴을 코드로 옮길 때에는 과연 어떻게 하면 될까요? 앞서도 잠시 언급했지만, Mediator 패턴의 문제는 UML 다이어그램만 봐서는 대체 어떤 식으로 코드를 구현해야 할지가 잘 드러나지 않는다는 것입니다.
하지만 한가지 힌트는 있습니다. ConcreteMediator 객체를 보면 다른 ConcreteColleague 객체들에 대한 Association을 가지고 있으니, 적어도 Mediator 객체 안에 다른 참여 객체들을 참조할 수 있는 정보(C++의 경우에는 포인터나 레퍼런스, 자바의 경우에는 레퍼런스)는 있어야 한다는 것을 알 수 있겠죠.
그런데 그 정도 정보 말고 다른 정보가 있나요? 없습니다. -_-; 따라서 Mediator 패턴을 실제로 구현하는 것은 구현시 주어지는 요구사항에 크게 좌우됩니다. 우선, 다음과 같은 요구사항이 주어졌다고 가정하고, 그 문제를 Mediator 패턴을 통해 풀어보도록 하죠.
공장 내에 설치된 장비실 온도를 감지하는 센서가 있습니다. 이 센서로부터 수집된 정보는 관리 목적을 위해 DBMS에 계속해서 반영됩니다. (그래프 등을 그린다거나 할 때 유용합니다.) 그런데, 이 정보는 DBMS에 저장될 뿐 아니라, 공장 관리자들이 들고 다니는 단말 장치 (PDA와 Cell Phone 두 가지 종류가 있다고 합시다) 에도 전송되어야 합니다. 온도가 지정된 온도 이상으로 넘어가면, 단말 장치에는 위험 경고 메시지가 표시되어야 하고, 장비실의 여벌 에어컨이 추가로 가동되어야 하고, 장비의 가동 속도는 잠정적으로 늦추어져야 합니다. 관리자들은 단말 상에서 그런 상황들을 보고 장비를 끈다거나, 에어컨을 끄거나 켜는 등의 추가적인 작업을 수행할 수 있어야 합니다.
Mediator가 없는 상황에서 위의 요구사항을 만족시키기 위한 프로그램을 짠다고 생각해보죠. 일단 장비가 있고, 에어컨이 있고, 센서가 있습니다. 장비실에 있는 이 3종의 장치 사이에도 우선 인터페이스가 있어야 합니다. 센서는 DBMS와도 연동해야 하고, 관리자들이 들고다니는 단말들과도 연동해야합니다. 단말들은 DBMS와 연동해야 할 뿐 아니라, 에어컨이나 장비들과도 연동해야 합니다.
복잡하죠 -_-;;; 굳이 다이어그램이 없어도, 클래스 다이어그램 상에 그려지는 복잡한 링크들이 떠올라 마음이 답답해집니다. 그런 복잡한 링크들은 그냥 단순한 시뮬레이션용 프로그램상에서는 적절히 기능할지 몰라도, 실제 상황에서는 거의 무용지물입니다. 단말 안에 DBMS 연동, 에어컨 연동, 장비 연동을 위한 복잡한 프로토콜들을 다 우겨넣을 바보는 없지 않겠어요? 실제 상황이라면, 장비는 중앙의 누군가와만 통신해야 하고, 센서도 중앙의 누군가와만 통신해야 하고, 실제 제어는 그 '중앙의 누군가'가 전담해야 합니다. 그래야 단순히 Simulation 용도에 머무르지 않는, 좀 더 실제 상황에 근접한 클래스 다이어그램이 만들어질 수 있습니다.
그런데, 저기까지를 쳐 놓고 생각해보면, 과연 Mediator 패턴을 구현하는 데 있어 Mediator 인터페이스라는 것이 어떤 역할을 해야 하느냐가 좀 궁금해집니다. 인터페이스라는 것은 그 인터페이스를 준수하는 여러 객체들이 다른 객체들과 상호작용해야 하는 경우가 빈번할 때 필요하잖아요? 그런데 위의 시나리오를 처리하는 데는 단 하나의 Mediator 객체만 있으면 되는데다, Mediator 인터페이스가 있다고 해서 그 인터페이스가 쉽게 재사용될 것 같지는 않아요. 그렇다면, 단도직입적으로 Mediator를 클래스로 구현하는 편이 더 낫지 않을까요?
이 질문에 No라고 대답하신 분들은, 아마 코드를 다음과 같이 구성하실 분들일 겁니다.
enum NodeType { SENSOR, DBMS, GUI };
interface Mediator;
interface Colleague { public NodeType getNodeType(); public void receiveNotificationFromMediator(String notification); public void registerMediator(Mediator medi); }
class DBMSColleague implements Colleague { private Mediator med;
public DBMSColleague() { med = null; }
public NodeType getNodeType() { return NodeType.DBMS; }
public void receiveNotificationFromMediator(String notification) { ... // 일단 생략 }
public void registerMediator(Mediator m) { med = m; }
... // 이하 일단 생략
}
interface Mediaitor { public void registerColleague(NodeType type, Colleague c); public void receiveNotificationFromColleague(NodeType type, String msg); }
class FactoryMediator implements Mediator { public void registerColleague(NodeType type, Colleague c) { if ( type == NodeType.SENSOR ) { .... } // 이하 일단 생략 }
public void receiveNotificationFromColleague(NodeType type, String msg) { if ( type == NodeType.SENSOR ) { .... } // 이하 일단 생략 } }
아마 이런 식으로 구현이 진행되는걸 염두에 두셨을 겁니다. 그런데 이런 코드 어디서 많이 본 코드 아닌가요? 네, 맞습니다. Observer 패턴을 구현할 때 많이들 짜게 되는 코드죠. 사실 Observer 패턴과 Mediator 패턴은 여러가지로 비슷합니다. Observer 패턴에서 Event Provider가 Mediator 패턴에서는 Mediator에 대응되죠. 그런데 Observer 패턴은 Observer 객체들에 보내어질 메시지의 형태가 비교적 정형화되어 있다는 점에서 Mediator 패턴과는 좀 다릅니다. 위에서 요구사항을 기술하면서 보았듯이, Mediator 패턴의 Colleague 객체들은 역할이 다 달라요. 물론 관리자들이 들고 다니는 단말기 같은 객체야 Observer 패턴에 어떻게 잘 엮을 수 있겠지만, DBMS나 센서 같은 것은 좀 곤란하죠.
거기다, 옵저버 패턴처럼 객체간에 주고받아야 할 인자의 형태를 과도하게 단순화해버리면, 또다른 문제가 생길 수도 있습니다. 가령 위에 보면 Colleague 인터페이스에 receiveNotificationFromMediator라는 함수가 정의되어 있는데, DBMS나 단말이나 센서나 전부 이 함수를 통해 자신에게 오는 메시지를 수신하게 되면, 결국 인자로 전달되는 값을 파싱해야 한다는 귀찮은 상황에 직면하게 되어버리죠. 거기다 Colleague 객체가 DBMS인 경우, 단말인 경우, 센서인 경우 각각에 대해서 전송되는 데이터도 다 다른 형식일텐데, 그러면 끔찍하게 많은 양의 파싱 코드들을 작성해야 하지 않겠어요? 거기다 Concrete Mediator 클래스들을 구현할 때, 위에서처럼 NodeType의 값이 무엇이냐를 검사하는 분기문이 필요하게 될 수도 있으니, 결과적으로는 if-else를 너무 과도하게 사용하게 될지도 모른다는 문제도 있어요.
그러니, 옵저버 패턴처럼 너무 과도하게 일반화된 코딩 방법을 Mediator 패턴에 적용하는 것은 그다지 좋은 생각이 아닐지도 몰라요. 특히 여러 가지 다른 용도의 서로 다른 클래스들이 상호연동해야 하는 상황에서는, 더더욱 그럴지도 모르죠.
자. 그러면 다시 원래 물음으로 돌아가 봅시다. Mediator는 꼭 인터페이스여야 할까요? 일단 위에서 한 이야기도 있고 하니, 위의 Mediator 인터페이스 코드에서 Mediator<->Colleague 객체 간 정보 전송을 위해 필요했던 함수 receiveNotificationFromColleague는 빼버리도록 하죠. 어쨰 함수 하나만 달랑 있으니 무척이나 심심하군요. 굳이 Mediator를 인터페이스로 하지 않고 클래스도 하더라도 괜찮겠다는 생각이 점점 더 굳어집니다. :-( 실제로 이 링크에 가보시면 Mediator를 그냥 클래스로 구현한 사례를 보실 수도 있어요. 그러니 일단 이번 글에서는, Mediator를 클래스로 구현해 버리도록 하겠습니다.
비지터 패턴은 디자인 패턴 중에서도 이해하가가 좀 난감한 패턴중의 하나입니다. 왜 그런지를 생각해 보니... 사실 Java나 C++에 익숙한 프로그래머들 중에 이런 패턴을 사용하여 프로그램을 작성하는 프로그래머는 별로 없다고 봐도 될 것 같거든요. 대부분의 경우, OO 프로그래머들은 "객체를 조작하는 메소드는 해당 객체에 대한 클래스에 정의한다"는 철칙에 따라 프로그래밍을 해 나갑니다.
하지만 비지터 패턴은 프로그래머들로 하여금, 문제를 조금 다른 방식으로 바라볼 것을 주문합니다. 이런 상황을 한 번 생각해 보죠.
객체의 구조는 잘 알려져 있고, 쉽게 변화하지 않는다. 하지만 객체에 적용될 오퍼레이션(메소드)이 어떤 것이 있을지는 지금 잘 모르겠다. 프로그래밍을 해 나가는 와중에 요구사항이 변화하여 새로운 오퍼레이션이 구현되어야 하는 상황이 빈번하게 벌어질 지도 모른다.
이런 상황에 적합한 디자인 패턴이 바로 비지터 패턴입니다. 그래서, 비지터 패턴은 다소 비-객체지향적인 방식의 프로그래밍을 요구하게 될 때도 있습니다. 그 점에 대해서는 나중에 예제를 통해서 살펴보도록 하구요. 우선은 비지터 패턴의 UML 다이어그램부터 잠시 참고하시도록 할까요? 이 패턴의 UML 다이어그램은 은 아주 훌륭한 블로그 중 하나인 http://younghoe.info/165 에 가 보시면 나와 있으니까, 참고하시면 되겠습니다.
자. 그러면 다시 문제로 돌아가 봅시다. 문제는, "객체의 구조는 고정적인데 그 객체에 적용되어야 하는 오퍼레이션은 정해져 있지 않으므로, 그 객체에 둘 수 없다"는 것입니다. 이 대목이 바로 비-객체지향적인 부분이죠. 그렇다면, 그 객체(Element라고 합시다)에 적용되어야 하는 오퍼레이션은 어디에 두어야 할까요? OO에서는 메소드가 오퍼레이션이고, 메소드는 객체 안에 두어야 하니까, 결국 다른 객체에 정의하여야 할 것입니다. 편의상, 이 '다른 객체'를 Visitor, 즉 Element에 대한 '방문자'라고 지칭합시다.
그렇다면, Element에 적용되어야 하는 연산이 Visitor 안에 정의되어 있을 테니까, Element로 하여금 어떤 연산을 하도록 만들려면, Element에게 Visitor를 넘겨주어야 하겠군요. 그렇다면 아마 Element는 다음과 같은 인터페이스를 만족하여야 할 것입니다.
interface Element { public void accept(Visitor vObj); }
그렇다면, Element 객체는 누군가 accept 메소드를 호출해서 Visitor 객체를 넘겨주면, 그 객체에 정의된 어떤 함수를 호출해서, Visitor 객체가 Element 객체 대신 모종의 연산을 하도록 만들면 되겠군요. 그렇다면 모르긴 몰라도, Element를 implement하는 클래스의 accept 메소드 안에는, 다음과 같은 코드가 들어있지 않을까요?
class ConcreteElementA implements Element { public void accept(Visitor vObj) { vObj.visit(this); } }
자. 그렇다면 만약에 Visitor가 인터페이스라고 가정한다면 (당연히 그래야할 것 같지만) 그 인터페이스 안에는 다음과 같은 메소드가 정의되어 있어야 하겠군요.
interface Visitor { public void visit(Element eObj); }
그런데 정말 이렇게 해도 되나요? Visitor 인터페이스가 위와 같이 정의되어 있는 경우, 이 인터페이스를 implement해서 Element에 저장된 int 값을 hexadecimal 형태로 찍어주는 메소드를 추가하려면 아마 다음과 같이 해야 할 겁니다.
class HexReportVisitor implements Visitor { public void visit(Element eObj) { int i = eObj.getIntValue(); ... // 이하 생략 } }
그런데 이런 식으로 구현을 하게 되면, 결국 인터페이스 Element에 굉장히 많은 메소드(위의 코드에서 사용된 getIntValue()와 같은)가 정의되어야 합니다. 문제는 그 메소드들이 '인터페이스'와는 별 관련이 없는, 객체에 저장되는 값을 추출하는 목적으로 정의되는 메소드들(소위, 'accessor' 메소드들)이라는 것이고, 그런 메소드들을 Element에 추가하게 되면 결국 ConcreteElementA 와 같은 클래스들의 구현이 그런 메소드들에 의해서 제약을 받게 되죠.
그런 구현을 선호하시는 분들도 있을 수 있고, 아닌 분들도 있을 수 있습니다. 하지만 인터페이스의 본래 목적에 비추어 보면, 그런 구현을 좋아하지 않는 분들이 더 많을 것 같아 보여요. 그러니, Visitor 인터페이스는 아마 다음과 같이 정의되는 편이 더 나을 겁니다.
interface Visitor { public void visit(ConcreteElementA aObj); public void visit(ConcreteElementB bObj); ... }
이런 식으로 구현하게 되면, 결국 앞서의 HexReportVisitor 클래스는 다음과 같이 구현되어야 마땅하겠죠.
class HexReportVisitor implements Visitor { public void visit(ConcreteElementA aObj) { int i = aObj.getIntValue(); ... // 이하 생략 }
public void visit(ConcreteElementB bObj) { String s = bObj.getValue(); ... // 이하 생략 } }
이렇게 하면 각각의 visit 메소드가 인자로 전달되는 객체의 타입을 구별하기 때문에 그 객체의 타입에 맞는 작업을 정의할 수가 있게 됩니다. ConcreteElementA 객체가 'visit' 될 때 실행되는 visit함수와 ConcreteElementB 객체가 'visit'될 때 실행되는 visit 함수가 달라지는 거지요. 그러니 당연히 그 구현도 다르게 가져갈 수 있습니다.
따라서 굳이 Element 인터페이스 안에 getIntValue() 같은 메소드 선언부를 다 때려 넣을 필요가 없게 됩니다. 그러니 ConcreteElementA, ConcreteElementB, ... 등등의 클래스 간에 심각한 연관성이 없어도 되지요.
물론 Visitor라는 인터페이스를 정의하고 사용해야 하는 관점에서 보면, visitor 인터페이스 안에 저렇게 인자의 타입에 따라 달라지는 수많은 visit 메소드를 정의해야 하는 것이 불만족스러울 수도 있어요. 하지만 지금은 Element의 하위 타입들이 비교적 고정되어 있는 상황을 가정하고 있는데다, 이렇게 하지 않으면 instanceOf 메소드를 남발하는 코드를 작성하게 되는 비극적인 상황에 처하게 될 위험성도 있어서, 이정도로 타협하고 넘어가는 것이 좋아보이기도 합니다.
자. 아무튼 이제 이정도면 앞서 보았던 UML 다이어그램의 의미는 그럭저럭 납득이 되셨으리라 생각됩니다. 위에 보았던 UML 다이어그램과 완벽하게 일치하는 예제를 보여드린 것은 아닙니다만, 이정도로도 Visitor 패턴이 의미하는 바에 대해서는 설명이 되었을 것 같습니다.
container.add( new ConcreteElementA( ... ) ); container.add( new ConcreteElementB( ... ) );
...
HexReportVisitor hexReporter = new HexReportVisitor(); TextReportVisitor textReporter = new TextReportVisitor();
for( Element e: container ) e.accept( hexReporter );
// or
for( Element e: container ) e.accept( textReporter );
즉, 필요에 따라서 container 안에 저장된 객체에 대해서 아무 visitor나 호출할 수 있습니다. 그러면 각 객체에 대해서, 지정된 visitor 안에 정의된 메소드가 호출되겠죠. 다른 메소드를 적용하고 싶으면, 다른 visitor를 호출하면 됩니다. 위에서는 hexReporter와 textReporter의 두 가지 visitor를 정의한 다음에, 상황에 따라서 그 중 하나를 호출했습니다.
이런 식으로 코딩할 수 있다는 것은, 결국 객체와 그 객체에 적용될 수 있는 연산을 분리시키는 효과를 낳습니다. 분리된 연산 하나 하나는 별도의 Visitor 클래스에 정의됩니다. 따라서, 프로그래머는 필요에 따라 새로운 Visitor 클래스를 생성하여 새로운 연산을 추가할 수 있습니다.
하지만 보시다시피, Visitor 클래스는 Element 인터페이스를 implement 하는 concrete 클래스들의 implementation에 굉장히 종속적으로 작성되는 경향을 가지게 됩니다. Element 인터페이스를 implement하는 클래스들은 Visitor 클래스들의 구현을 위해 자신의 정보를 accessor 함수들을 통해 남김없이 전달할 수 있어야 하고, 결과적으로 Visitor 클래스들의 구현은 그런 정보들에 종속적으로 작성될 수 밖에 없는 것이죠.
하지만 어차피 객체의 'representation'과 'operation'을 Element-Visitor로 떼 낸 만큼, 그런 종속성 쯤이야 적당히 감수를 해야겠죠.
아무튼, 저는 프로그래밍하면서 이런 패턴을 적용해야 할 정도로 요구사항이 심각하게 변화하는 상황에 처한 적이 없어서 나름 다행이라고 생각합니다만 ㅎㅎ 앞으로는 어떨지 모르겠어요. 뭐... 어떤 알바를 하느냐에 따라서 어떤 프로그래밍을 하게 되느냐 하는 것은 꽤 심각할 정도로 달라지게 되니까 말이죠.
아무튼, 이 정도로 Visitor 패턴에 대한 글을 마무리 짓겠습니다. 나중에 시간나면 세미나용 자료도 작성해서 올려보도록 하겠습니다.
네트워크 정보의 좋은 소스셔서 감사합니다 ,내가이 기사를 읽고 오랫동안 귀하의 사이트를 추적 유지되었습니다, 내가 더 관심을 지불할 것입니다 흥미로운 읽을 수 있습니다,기사가 가장 고전적인 스타일 중 하나가, 내가 한 번 읽으면, 내가 그들과 사랑에 깊이되었습니다이며, 좀 더 완벽한 작품을 기대 http://www.mycanadagooseparka.com/
http://www.mlnic.com
네트워크 정보의 좋은 소스셔서 감사합니다 ,내가이 기사를 읽고 오랫동안 귀하의 사이트를 추적 유지되었습니다, 내가 더 관심을 지불할 것입니다 흥미로운 읽을 수 있습니다,기사가 가장 고전적인 스타일 중 하나가, 내가 한 번 읽으면, 내가 그들과 사랑에 깊이되었습니다이며, 좀 더 완벽한 작품을 기대
http://www.getcheapbeatsbydre.com/
네트워크 정보의 좋은 소스셔서 감사합니다 ,내가이 기사를 읽고 오랫동안 귀하의 사이트를 추적 유지되었습니다, 내가 더 관심을 지불할 것입니다 흥미로운 읽을 수 있습니다,기사가 가장 고전적인 스타일 중 하나가, 내가 한 번 읽으면, 내가 그들과 사랑에 깊이되었습니다이며, 좀 더 완벽한 작품을 기대
http://www.stylishuggboots.com/
네트워크 정보의 좋은 소스셔서 감사합니다 ,내가이 기사를 읽고 오랫동안 귀하의 사이트를 추적 유지되었습니다, 내가 더 관심을 지불할 것입니다 흥미로운 읽을 수 있습니다,기사가 가장 고전적인 스타일 중 하나가, 내가 한 번 읽으면, 내가 그들과 사랑에 깊이되었습니다이며, 좀 더 완벽한 작품을 기대
http://www.buybeatsdreheadphone.com
네트워크 정보의 좋은 소스셔서 감사합니다 ,내가이 기사를 읽고 오랫동안 귀하의 사이트를 추적 유지되었습니다, 내가 더 관심을 지불할 것입니다 흥미로운 읽을 수 있습니다,기사가 가장 고전적인 스타일 중 하나가, 내가 한 번 읽으면, 내가 그들과 사랑에 깊이되었습니다이며, 좀 더 완벽한 작품을 기대
댓글을 달아 주세요
안녕하세요!!! 좋은 글 감사합니다. 자주올게요!!
2011/01/28 01:15 [ ADDR : EDIT/ DEL : REPLY ]감사합니다. 또 오세요 ^^
2011/01/28 12:00 [ ADDR : EDIT/ DEL ]