안녕하세요 belhyun입니다. 어제는 강남토즈2호점에서 JS 디자인패턴 스터디를 진행하였습니다. 전 3회때부터 참석하게 되었습니다. 대기자 명단에서 제 순서가 되었고 기분좋게 정식 스터디원이 되었습니다.
어제 배운내용을 머릿속에 남은 기억과 외국 blog(http://www.dofactory.com/javascript-patterns.aspx)를 참고하여 기술해 나가도록 하겠습니다.
어제 배운내용은 크게 iterate 패턴과, composite 패턴이였습니다. 먼저 iterate 패턴을 왜 사용하는지에 대한 언급을 해보도록 하겠습니다.
먼저 다음과 같은 예문을 보도록 하겠습니다.
function mc(num,cnt){
if(cnt-- == 0) return 1;
else return num*mc(num,cnt);
}
위의 식은 들어온 숫자(num)의 cnt만큼 곱한 숫자를 리턴하는 재귀표현식입니다. 우리가 보통 많이 쓰는 재귀표현식과 다르지 않습니다.
하지만 이 재귀표현식은 stack memory overflow가 발생하게 됩니다.
재귀식에서는 그 전단계를 기억하고 있어야 합니다. 그렇기 때문에 각 상태마다 메모리가 하나씩 잡히게 됩니다. return문을 통해서 다시 그 메모리가 해제되기 됩니다.
마치 피라미드형태와 같습니다. cnt의 숫자가 적으면 상관없지만 cnt의 숫자가 커지게 되면 각 상태마다 메모리를 가지게 되고 그렇기 때문에 memory overflow가 발생하게 됩니다.
위 재귀표현식을 다음과 같이 바꾸어 보겠습니다.
function mc(num,cnt,res){
if(typeof res == "undefined") res = 1;
if(cnt-- == 0) return res;
else{
return mc(num,cnt,num*res);
}
}
그 전과 달라진 점은 각각의 상태에 대한 결과 값을 저장하는 파라미터 res가 추가되었다는 점입니다. 전 재귀식에서 문제가 됬던 부분은 num * 식입니다. 이 식을 제거하고 결과값을 파라미터로 넘김으로써, 각 상태에 대한 메모리를 차지 하지 않고 res 메모리를 하나 잡아
결과값을 저장하는 형태입니다.
이렇게 함으로써 메모리 overflow를 방지할 수 있습니다.
위 두 예에서 보는바와 같이 재귀식은 지양되어야 합니다. 그렇다면 일반 반복식은 큰 문제가 없을까요? javascript에서 지원하는 while문도 무한히 반복하게 되면 javascript run time out error가 발생하게 됩니다. 이렇게 반복에 대한 이슈는 자바스크립트에서 매우 취약한 부분이라고 할 수 있습니다. 그렇기 때문에 iterate패턴을 적용하여 구현하게 되면 반복에 대한 이슈를 문제없이 해결할 수 있습니다.
무릇 디자인 패턴이라 함은 구상객체의 인터페이스가 있고 구상객체를 실제로 사용하는 클라이언트 구상객체의 모음을 가지는 컨텍스트로 이루어진다고 볼 수 있습니다.
하지만 javascript에서는 함수나 객체나 모두 object이기 때문에 구상객체를 생성하여 컨텍스트에서 조합하여 바로 사용할 수 있습니다. 굉장히 flexible하다고 볼 수 있는 것입니다.
그렇다면 이러한 개념에 기반하여 iterate 패턴에 대하여 알아보도록 하겠습니다.
iterate 패턴의 기본개념은 다음과 같습니다.
결합된 객체의 요소들을 그 내재된 표현의 노출없이 접근하는 방법을 제공하는 패턴이다. 즉 객체의 요소들을 접근하는데 있어 캡슐화된 접근으로 그 객체의 각 요소들을 접근할 수 있는 방법을 제공하는 패턴입니다.
이 패턴은 다음과 같은 특징을 가지고 있습니다.
1. 클라이언트가 객체들의 집합을 순회할 수 있도록 합니다.
2. 일반적 역할은 객체들의 집합을 순회하고 조작하는데 있습니다.
3. 위의 컬렉션은 간단한 배열부터 복잡한 트리까지 다양한 형태가 될 수 있습니다.
4. 객체를 순회하는데 있어 앞에서부터 뒤로, 뒤에서부터 앞으로 등 다양한 방법이 존재할 수 있는데 이 문제를 해결할 수 있습니다.
이어서 다이어그램을 보도록 하겠습니다.
먼저 클라이언트는 iterator 객체에게 다양한 요청을 할 수 있습니다. 실제 컬렉션의 아이템에 접근하는 녀석은 iterator이기 때문에 클라이언트 입장에서는 items에 관한 정보를 가지고 있을 필요가 없습니다. 그저 iterator에 요청을 하기만 하면 됩니다.
이제 sample code를 통해서 실제 구현을 알아보도록 하겠습니다.
var Iterator = function(items){
this.index = 0;
this.items = items;
}
Iterator.prototype = {
first: function(){
this.reset();
return this.next();
},
next: function(){
return this.items[this.index++];
},
hasNext: function(){
return this.index <= this.items.length;
},
reset: function(){
this.index = 0;
},
each: function(callback){
for(var item=this.first();this.hasNext();item=this.next()){
callback(item);
}
}
}
//로그 헬퍼 즉시호출함수
var log = (function() {
var log = "";
return {
add: function(msg){ log += msg+"\n";},
show: function(){ alert(log); log = "";}
}
})();
function run(){
var items = ["one", 2, "circle", true, "Apple"];
var iter = new Iterator(items);
for(var item = iter.first(); iter.hasNext(); item = iter.next()){
log.add(item);
}
log.add("");
log.show
iter.each(function(item){
log.add(item);
});
log.show();
}
위 식의 결과값은 one, 2, circle, true, Apple이 2번 나오게 됩니다.
최초 저는 Iterator 생성자 함수를 정의했습니다. 그 생성자 함수는 new 키워드로 객체를 생성 시, 자신과 똑같은 복사본을 가지지만 this는 본래 객체를 가리키는 prototype 객체 하나를 생성시키게 됩니다. 따라서 Iterator.prototype에서 index, items등에 접근 가능하게 됩니다. 다시 패턴적으로 봤을 때 클라이언트(run())에서는 내부적으로 iterator가 어떻게 구현되있는지 몰라도 단지 next, hasNext등을 호출함으로써 items에 있는 원소들을 순회할 수 있게 됩니다.
이러한 패턴을 이용하여 다음과 같은 이점을 얻을 수 있습니다.
1. 다양한 타입으로 이루어진 collection을 순회할 수 있다.
2. 클라이언트에서는 실제 iteration 로직을 가지고 있을 필요가 없다.따라서 모듈화, 캡슐화가 가능해진다.
3. 다양한 콜렉션 형태(배열, 해쉬..)를 다룰 수 있다.
다음은 composite 패턴에 대해서 알아보도록 하겠습니다.
composite 패턴은 전체 계층을 표현하기 위해 객체를 트리 형태로 결합하는 것을 의미합니다. composite 패턴은 클라이언트가 각각의 객체와 객체의 결합을 균일하게 다룰 수 있도록 해줍니다. composite 패턴은 기본 아이템과 객체의 콜렉션이 될 수 있는 프로퍼티로 이루어진 객체들의 생성을 허용해 줍니다. 콜렉션의 각각의 아이템은 다른 콜렉션을 가질 수도 있고, 복잡한 구조의 콜렉션을 가질 수도 있습니다. 트리 구조는 완벽한 composite 패턴의 예입니다.
다음은 다이어그램을 나타냅니다.
다이어그램에서 보시는 것처림 하나의 component들은 leaf 또는 composite를 가지게 됩니다. 또한 각각의 composite는 또 다른 composite나 leaf를 가질 수도 있습니다.
각각의 용어는 다음을 의미합니다.
component - composition(결합)에 있는 객체를 위한 인터페이스를 선언합니다.
leaf - leaf 객체를 표현합니다. 특징은 더 이상의 child node를 가질 수 없다는 점입니다.
composite - composition(결합)의 branche를 나타냅니다. 하위 component들의 collection(집합)을 유지합니다.
그럼 코드를 통해서 자세히 알아보도록 하겠습니다.
예제에서는 트리 구조는 Node 객체들로부터 생성됩니다. 각각의 노드들은 이름과 add, remove, getChild, hasChilderen 메소드를 가지고 있습니다. 메소드들은 Node의 프로토타입으로 추가됩니다. 이렇게 함으로써 모든 노드들이 해당 메소드를 공유할 수 있기 때문에 메모리의 절약을 가져올 수 있습니다.
마찬가지로 log 함수는 출력을 위한 헬퍼 함수입니다.
var Node = function(name){
this.children = [];
this.name = name;
}
Node.prototype = {
add: function(child){
this.children.push(child);
},
remove: function(child){
var length = this.children.length;
for(var i=0; i< length;i++){
if(this.children[i] === child){
this.children.splice(i, 1);//remove
return;
}
}
},
getChildren: function(i){
return this.children[i];
},
hasChildren: function(){
return this.children.length > 0;
}
}
//재귀적으로 sub tree를 순회합니다.
function traverse(indent, node){
log.add(Array(indent++).join("--") + node.name);
for (var i = 0, len = node.children.length; i < len; i++) {
traverse(indent, node.getChild(i));
}
}
// logging helper
var log = (function () {
var log = "";
return {
add: function (msg) { log += msg + "\n"; },
show: function () { alert(log); log = ""; }
}
})();
function run() {
var tree = new Node("root");
var left = new Node("left")
var right = new Node("right");
var leftleft = new Node("leftleft");
var leftright = new Node("leftright");
var rightleft = new Node("rightleft");
var rightright = new Node("rightright");
tree.add(left);
tree.add(right);
tree.remove(right); // note: remove
tree.add(right);
left.add(leftleft);
left.add(leftright);
right.add(rightleft);
right.add(rightright);
traverse(1, tree);
log.show();
}
옆의 그림은 결과 값을 나타냅니다.
traverse의 의해 comosite들이 순회하게 됩니다.
만약 leaf 노드까지 다달았다면 다시 상위 composite로 가고 그 다음 left -> right 방향으로 leaft 노드를 순회하게 됩니다. Array(indent++).join("--")은 indent 크기만큼 배열을 생성하여 "--"으로 연결하게 됩니다. 예를 들어 Array(3).join("--")은 "----"를 출력합니다. 이러한 composite 패턴은 다음과 같은 장점이 있습니다.
1. client에서는 leaf와 composite를 구분할 필요가 없기 때문에 관리하기 쉬워지게 됩니다.
2. leaf/composite로 구성하여 일관된 계층도를 유지할 수 있다.
3. 새로운 요소를 쉽게 추가할 수 있다. composite와 leaf는 기존에 존재하는 구조들과 독립적으로 동작하므로 새로운 요소가 추가되었다고 해서 프로그램의 변경이 필요하지 않습니다.
감사합니다.