ORM 만들기
오브젝트 관계형 매퍼(ORM)는 보통 자바스크립트 언어 이외의 다른 언어에서 사용한다. 그러나 데이터 관리에 사용하는 유용한 기법인 ORM을 자바스크립트 애플리케이션 모델에도 활용할 수 있다. 예를 들어 ORM을 이용해 모델을 원격 서버와 묶을 수 있다. 모델을 원격 서버와 묶은 상태에서 모델 인스턴스를 변경하면 백그라운드 Ajax 요청을 서버로 보낸다(즉 인스턴스에 발생한 변화를 백그라운드로 서버에 전달한다). 또는 모델을 HTML 엘리먼트 인스턴스와 묶을 수도 있다. 그러면 인스턴스에 발생한 변화가 뷰에 반영된다.
본질적응로 ORM은 단지 데이터를 감싸는 오브젝트 계층일 뿐이다. 보통 SQL 데이터베이스를 추상화할 때 ORM을 사용하지만 우리가 살펴보려는 것처럼 자바스크립트 데이터 타입을 추상화할 때에도 ORM을 활용할 수 있다. 추가 계층을 만들면 직접 커스텀 함수와 프로퍼티를 계층에 추가할 수 있으므로 기본 데이터에 다양한 기능을 사용할 수 있다는 장점이 있다. 따라서 검증, 관찰, 영구 저장, 서버 콜백등의 기능을 추가할 수 있다. 게다가 이들 기능 대부분을 재활용할 수 있다.
프로토타입의 상속
Object.create()를 이용해 ORM을 만들려고 한다. 생성자 함수와 new 키워드를 사용하는 클래스 기반과는 약간 다른방법인 프로토타입 기반 상속을 활용할 것이다.
Object.create()는 프로토타입 오브젝트 하나를 매개변수로 받고, 지정한 프로토타입 오브젝트 형식의 새 오브젝트를 반환한다. 즉 Object.create() 에 오브젝트 하나를 전달하면 지정한 오브젝트를 상속받는 새로운 오브젝트를 반환한다.
if(typeof Object.create !== 'function') {
Object.create = function(o) {
function F() {}
F.prototype = o;
return new F();
}
}
이제 새 모델과 인스턴스 생성을 담당하는 Model 오브젝트를 만들자.
var Model = {
inherited: function() {},
created: function() {},
prototype: {
init: function() {}
},
create: function() {
var object = Object.create(this);
object.parent = this;
object.prototype = object.fn = Object.create(this.prototype);
},
init: function() {
var instance = Object.create(this.prototype);
instance.parent = this;
instance.init.apply(instance, arguments);
return instance;
}
};
create() 함수는 Model 오브젝트를 상속하는 새 오브젝트를 반환한다. 따라서 새 모델을 만들 때에는 create() 함수를 사용할 것이다. init() 함수는 Model.prototype(예를들어 Model 오브젝트의 인스턴스)을 상속받는 새 오브젝트를 반환한다.
var Asset = Model.create();
var User = Model.create();
var user = User.init();
ORM 프로퍼티 추가하기
Model에 추가한 프로퍼티는 모델을 상속받은 모두가 사용할 수 있다.
// 오브젝트 프로퍼티 추가
jQuery.extend(Model, {
find: function() {}
});
// 인스턴스 프로퍼티 추가
jQuery.extend(Model.prototype, {
init: function(atts) {
if(atts) this.load(atts);
},
load: function(attributes) {
for(var name in attributes) {
this[name] = attributes[name];
}
}
});
jQuery.extend() 는 이전에 load() 함수에서 수행했던 것과 비슷한 방법으로 'for 루프로 모든 프로퍼티를 직접 복사하던 기능'을 유틸리티 함수로 만든 것이다. 이제 오브젝트와 인스턴스 프로퍼티를 개별 모델로 전파할 수 있다.
앞으로 많은 프로퍼티를 추가해야 하므로 Model 오브젝트에 extend()와 include()도 만든다.
var Model = {
/* 생략... */
extend: function(o) {
var extended = o.extended;
jQuery.extend(this, o);
if(extended) extended(this);
},
include: function(o) {
var included = o.included;
jQuery.extend(this.prototype, o);
if(included) included(this);
}
};
// 오브젝트 프로퍼티 추가
Model.extend({
find: function() {}
});
// 인스턴스 프로퍼티 추가
Model.include({
init: function(atts) { /**... */},
load: function(attributes) { /*... */}
});
새 asset을 만들고 속성을 설정한다.
var asset = Asset.init({name: 'foo.png'});
레코드 영구 저장
이제 생성한 인스턴스 레퍼런스를 보관했다가 나중에 불러올 수 있도록 레코드를 영구 저장해야한다. Model의 records 오브젝트를 이용해 이 기능을 구현해보자. 저장할 인스턴스는 Model의 records 오브젝트에 추가하고, 반대로 삭제할 인스턴스는 records 오브젝트에서 제거한다.
// 저장된 asset을 포함하는 오브젝트
Model.records = {};
Model.include({
newRecord: true,
create: function() {
this.newRecord = false;
this.parent.records[this.id] = this;
},
destroy: function() {
delete this.parent.records[this.id];
}
});
기존 인스턴스는 어떻게 갱신할 수 있을까? 단순히 오브젝트 레퍼런스만 갱신하면 된다.
Model.include({
update: function() {
this.parent.records[this.id] = this;
}
});
인스턴스를 편리하게 저장할 수 있는 함수를 만들자. 먼저 인스턴스를 이미 저장했는지 확인해서 기존에 저장하지 않았다면 새로 만들어야 한다.
// 레코드를 포함하는 오브젝트를 저장하고 오브젝트를 레퍼런스를 보존한다.
Model.include({
save: function() {
this.newRecord ? this.create() : this.udpate();
}
});
그리고 ID로 asset을 찾는 find() 라는 함수를 구현하자.
Model.extend({
// ID로 검색 결과를 반환하거나 예외를 일으킨다.
find: function(id) {
return this.records[id]; // 또는 throw('Unknown record');
}
});
이렇게해서 기본 ORM을 만들었다. 이제 ORM을 사용할 수 있다.
var asset = Asset.init();
asset.name = 'same, same';
asset.id = 1
asset.save();
var asset2 = Asset.init();
asset2.name = 'but different';
asset2.id = 2;
asset2.save();
asset2.destroy();