Object 2. 보다 안전한 객체

개요

5.0에 추가된 Object.defineProperty는 매우 강력한 기능으로 전반적인 자바스크립트의 문법을 변화시키고 코드에 대한 해석을 기존처럼 할 수 없는 컨텍스트를 제공합니다. 본격적인 설명으로 돌입하기 전에 아래의 코드를 살펴보면서 맛을 보죠.

var test = {};
(function(){
  var age = 18, isAdult = false;
  Object.defineProperty(test, 'age', {
    get:function(){
      return age;
    },
    set:function(v){
      if (v < 1) throw '';
      isAdult = v > 18;
      age = v;
    }
  });
  Object.defineProperty(test, 'isAdult', {
     get:function(){
       return isAdult;
     }
  });
})();

test.age = -1; //예외발생
test.age = 19;
test.isAdult == true //자동으로 설정됨

위의 간단한 예에서는 age속성을 값을 할당할 때 자동으로 isAdult가 갱신되도록 되어있습니다. 이제 test.age = 19 라는 간단한 식도 내부에 어떠한 로직을 담을 수 있다고 가정해야하기 때문에 모든 자바스크립트의 식은 마치 함수처럼 여러 개의 문을 포함할 수 있는 보다 함축적인 표현을 할 수 있게 됩니다. 따라서 아래와 같은 코드를 만나도 이제는 평범하지 않은 것입니다.

if (data.isLoaded) container.go;
  1. 만약 data.isLoaded가 getter에서 자신을 자원 전부를 체크하여 로딩되었는지 확인하는 중일 수 있고, 자원이 많다면 timeout도 발생할 수 있다.
  2. container.go는 getter가 호출되는 형태이므로 go의 getter에서 실제 필요한 문이 실행될 것이다.

이런 관점에서 보면 ES5는 얼마든지 기존의 문과 식의 관념을 변화시킬 수 있을 것입니다.

//파스칼스럽게 가볼까?
transactionManager.begin;
//트랜젝션과 관련된 일
transactionManager.end;

//루프를 재정의해보자.
loop.target = array;
loop.start = {
  type:'each',
  order:'index',
  each:function(el, idx, arr){..}
};

이러한 속성정의자는 3.0부터 내려오는 프로토타입, 스코프등과 결합하면 더욱 복잡한 양상으로 전개되는데 이번 포스팅에서는 Object.defineProperty의 기본에 익숙해지고, freeze, seal, preventExtensions 등의 메서드를 살펴봅니다. 이 후 포스팅에서 스코프와의 결합이나 프로토타입과의 결합을 별도로 자세하게 다루겠습니다.

정적시점과 실행시점

운영체제 입장에서 모든 프로그램은 메모리에 적재된 후에 실행됩니다. 따라서 원론적으로 실행시점만 존재하죠. 그럼 정적시점 혹은 컴파일시점이란 건 언제일까요?
우선 변수를 생각해보죠. 프로그래밍을 할 때 변수를 사용하는데, 이 변수라는 것의 실체는 값이 들어갈 메모리의 별명입니다.

보다 엄격하게 생각해보면 프로그래밍을 하고 있는 시점에서는 프로그램이 아직 운영체제에 로딩된 상태가 아닙니다. 근데 변수가 어떻게 메모리를 가리킬 수 있을까요?
당연히 없습니다. 따라서 변수는 앞으로 운영체제에 로딩이 된다면 확보할 메모리에 매핑할 가상 주소를 가리킵니다. 이러한 가상의 메모리와 변수와의 관계를 매핑한 것을 통칭 vtable이라 합니다.

이러한 vtable은 실제 프로그램이 운영체제에 로딩될 때 실제 주소로 치환되어 프로그램이 작동하게 됩니다. 하지만 프로그램은 vtable에 있는 변수 외에도 추가적인 메모리를 실행 중에 확보하게 되는데, 대표적으로 함수호출 시마다 생성되는 스택메모리공간이나 객체 생성 시 확보되는 힙메모리 공간 등이 있습니다.
정적시점으로 개발하는 것은 이러한 vtable에 사전에 정의되거나 함수, 클래스 등의 정의를 통해 실행시점에 확보될 메모리의 구체적인 형태를 사전에 정의해 두는 것을 의미합니다.

이에 비해 실행 중에 동적으로 형이 결정되거나 정적 시점에 예상할 수 없는 형태로 메모리가 확장되어 사용되는 형식의 개발을 실행시점중심 개발이라 합니다. 정적시점에 많은 것을 결정해두면 컴파일러가 이들 사이의 모순을 자동으로 계산해주기 때문에 대규모 개발에 많은 버그를 사전에 예방할 수 있습니다. 해서 되도록이면 정적시점에 많은게 결정되고 실행 시점에는 그 결정된 사항을 실행하는 구조가 바람직하다라는 기조는 계속 강조되왔습니다.

헌데 여기서 중요한 포인트는 정적시점이냐 실행시점이냐라는 것이 아닙니다. 운영체제 입장에서는 모두 프로그램이 로딩된 후에 실행되는 것이지만 실행되자마자 먼저 메모리를 확보하여 vtable을 매핑한뒤 구동하는 식으로 실행시점의 단계를 나눠둠으로서 정적시점과 실행시점을 만들어냈다는 것입니다. 즉

정적시점과 실행시점은 일부러 만들어낸 작위적인 구분시점입니다.

따라서 특정한 절차를 도입하기만 한다면 얼마든지 실행시점을 더 나눠서 단계적으로 사용하는 전략을 꾸릴 수 있습니다. 이는 마치 운영체제입장에선 jvm이 하나의 어플리케이션일 뿐이지만, class입장에선 다시 jvm이 운영체제처럼 보이는 것과 비슷한 원리입니다. 운영체제의 실행시점에 실행되고 있는 jvm에서 다시 정적시점과 실행시점을 나눠서 개발할 수 있게 해주는 것이죠.

실행시점 밖에 없는 환경

위에 원론적인 얘기를 한 이유는 자바스크립트가 구동되는 환경 때문입니다. 자바스크립트는 운영체제위에 바로 구동되는 프로그램이 아니라 자바스크립트엔진 위에서 구동되는 스크립트입니다. 게다가 사전 컴파일 단계가 존재하지 않고 스크립트가 로딩되면서 곧장 실행되는 특성을 갖고 있습니다.
해서 모든게 실행시점인 상태죠. 자바스크립트가 3.0까지 클래스같은 구조물을 갖고 있지 않고 전부 유연한 해쉬맵 형태인건 어찌보면 당연하다고도 여겨졌습니다. 실행시점에 정의되는건 동적인 연결 즉 연결리스트형태의 관계연결만 지원하고 그 외에는 전부 해쉬맵상태로 객체를 만들어 모든 상황에 대응하도록 디자인 된 것이죠. 따라서 이런 동적인 연결은 매우 취약한데 대표적으로 prototype연결이 그러합니다.

var ClassA = function(){};
//최초 action의 정의를 prototype에 해줌
ClassA.prototype.action = function(){
  console.log('1111');
};

//인스턴스를 만들어서 실행
var test = new ClassA();
test.action(); // '1111'

//prototype객체를 교체함.
ClassA.prototype = {
  action:function(){
    console.log('2222');
  }
};

//여전히 이전의 prototype을 가리키고 있음.
test.action(); // '1111'

//헌데 이미 ClassA는 갈아탔음.
test instanceof ClassA // false

분명 new로 만들어진 test인데도 ClassA와의 페어링이 생성 후에 끊어집니다. 처음부터 이 문법의 의미는 그저 test.__proto__ 에 ClassA.prototype을 넣어줫을 뿐이기 때문입니다. 실행시점에 최적화된 자바스크립트는 상대적으로 적은 메모리를 쓰고 최대한 연결리스트를 활용하도록 설계되어있지만 이는 반대로 사람이 한땀한땀 정교하게 실행시점의 코드를 예상하고 통제해야한다는 것을 의미합니다.
또한 컴파일러가 있는 경우 컴파일시에 아예 정적시점에 대한 부정합을 조사하여 아예 컴파일을 실패시키는 방법으로 실행시점까지 갈 수 없게 방어하고 있는데 실행시점밖에 없는 자바스크립트에서는 이러한 구조가 불가능하므로 실행시점에 예외를 통해 실행을 중지 시키는 것 외엔 방법이 없습니다.
다음과 같은 방책이 그나마 실행시점에서 적용할 수 있는 최선의 방어입니다.

최대한 빨리 예외를 발생시킨다.

  • 릴리즈제품에서 예외가 일어나는 것도 곤란하기 때문에 예외를 일으키는 함수를 따로 정의하고 릴리즈시점에는 이를 대체하는 방식이 좋습니다.

정의가 이뤄지는 시점

그럼 짜피 컴파일러가 없고 컴파일타임이 없다는걸 인정한다는 가정 하에서 얘기해보겠습니다. 왜 이런 가정이 필요하냐면 현재는 자바스크립트 컴파일러도 실제로 많이 나와있기 때문입니다. emscripten이나 typescript, coffeescript는 최종 결과물을 자바스크립트코드로 만들어내는 컴파일러로서 컴파일단계에서 실패하면 자바스크립트코드 자체가 나오지 않아 실행이 불가능한 환경을 제공합니다. 하지만 본 포스팅은 자바스크립트 그 자체를 다루고 있으므로 이러한 컴파일러에 대해서는 다루지 않습니다.
대신 5.0에서는 보다 강력한 안정성을 제공하고 있는데 일종의 정의시점 안정성이란 것입니다. 아직까지 이를 지칭하는 공식용어는 없습니다만, 편의상 defineTime 즉 정의시점과 runtime 실행시점으로 나눠서 설명하도록 하겠습니다.
자바스크립트에서 정의가 이뤄지는 시점은 객체를 생성하는 시점과 깊은 관련이 있습니다. 실행시점에 객체를 생성하는데 이 객체는 보통 해쉬맵이므로 자유롭게 키,밸류를 설정할 수 있습니다만, 이러한 속성을 설정하는 시점을 작의적으로 나눠서 확정적으로 속성을 정의하고 그 이후에는 정의된 대로만 사용한다 라는 식으로 생각해볼 수 있습니다. 이는 다음과 같은 코드로 표현됩니다.

var test = {}; //생성시점

//정의시점
test.field1 = null;
test.field2 = null;
test.method1 = function(){};
test.method2 = function(){};

//실행시점
test.field1 = 3;
test.field2 = 5;
test.method1();
test.method2();

5.0에서 생성시점에 관련되어 지원하는 기능은 이미 앞의 포스트에서 살펴봤습니다. 이젠 정의시점에 대한 5.0의 지원을 살펴볼 것인데 정의시점에서 정의한 속성들을 곰곰히 생각해보면 다음과 같은 사항이 관련되어 있습니다.

  1. 해당 키를 삭제할 수 있다(configurable)
  2. 그 키의 값을 바꿀 수 있다(writable)
  3. 그 키를 for in 등으로 열거할 수 있다(enumerable)

위와 같은 속성에 대한 통제는 컴파일언어에서 기본적으로 제공하는 기능이기도 합니다. 자바의 클래스 정의를 예로 들어보면 클래스에서 정의한 필드나 메서드는 제거할 수 없고 자세한 접근 스코프를 정의할 수 있습니다. 자바스크립트는 클래스가 없으므로 이를 정적 시점이 아니라 실행시점에 생성 후 정의하면서 실현하도록 한 것입니다.

Object.defineProperty

이러한 속성을 통제하기 위해 Object에는 새로운 정적 메서드가 추가되었는데 definePropery라는 넘으로 인자를 세개 받습니다.

var test = {};

Object.defineProperty( test, 'field1', {value:3} );

console.log( test.field1 ); //3

위의 예처럼
1. 설정하고자 하는 대상객체
2. 정의하려는 키의 이름
3. 설정객체

라는 인자인데 이중 3번에 해당되는 설정객체가 매우 생소한 녀석입니다. 3.0까지 자바스크립트에서 특정 객체가 필요할 때 그 객체가 네이티브타입이 아닌 경우는 매우 적었습니다. Array의 filter가 받아들이는 함수라던가 이벤트 리스너 등은 일반함수객체지만 특정 인자 또는 반환값을 요구하는 형태입니다. 하지만 그 대상이 객체인 경우 대부분 전용 객체를 정의하는게 일반적이었습니다. 5.0에서는 제이쿼리 등이 사용하고 있는 인자설정객체의 개념을 받아들여 특수한 인자용 객체타입을 내장하지 않고 일반 객체에 원하는 키가 있는지를 조사하는 식으로 사용되는 예가 꽤나 늘었습니다.
그 중 하나가 설정객체입니다.

설정객체

세 번째 인자로 전달되는 설정객체는 다음과 같은 속성키를 보내면 의미를 지니게 됩니다.

  • enumerable – for in 이나 Object.keys 에서 해당 속성을 노출시킬 것인가 아닌가를 결정합니다.
  • configurable – 해당 키를 delete연산자로 제거할 수 있는지와 다시 속성에 대한 설정을 바꿀 수 있는지 여부를 결정합니다.
  • writable – 키에 있는 값을 갱신할 수 있는지 여부를 결정합니다.
  • value – 키에 기본적으로 할당될 값을 정의합니다.
  • get – getter를 정의합니다. 인자없이 값을 반환하는 함수가 올 수 있습니다.
  • set – setter를 정의합니다. 인자로 값을 하나 받아들이며, 반환값은 없는 함수가 올 수 있습니다.

하지만 이게 처음 입문자로 하여금 Object.definedProperty를 매우 어렵게 느끼도록 합니다. 제이쿼리에서 애니메이션이나 ajax를 쓰는 경우도 비슷한데, 객체에 들어가는 키의 이름과 그 의미를 외워야하기 때문입니다. 인자객체의 최대 난점이죠. 작성된 코드를 보고 읽기는 편하지만 실제 사용하려면 개발문서를 보면서 개발하거나 아니면 외우고 있어야한다는 거죠. 모든 암기과목이 그렇듯 암기에는 요령이 있습니다. MDN문서에서는 위의 6가지 속성을 3가지 그룹으로 정의했습니다.

  1. 공통속성 – enumerable, configurable
  2. 직접 데이터를 지정하는 속성 – writable, value
  3. 간접 데이터를 지정하는 속성 – get, set

이렇게 3개의 그룹으로 나눈 이유는 직접데이터를 다루는 그룹의 속성과 간접데이터를 다루는 그룹의 속성은 동시에 올 수 없기 때문입니다. 공통속성은 양쪽으로 전부 적용할 수 있습니다. 즉 하나의 설정객체에 올 수 있는 최대 속성은 4가지 입니다. 그림으로 표현하면 집합다이어그램이죠.

descriptor

공식적인 용어는 value와 writable을 쓰는 경우 data descriptor라 하고 get, set을 쓰는 경우는 accessor descriptor 라고 합니다.

  • 이걸 어떻게 번역해야할까 고민이 많이 되었는데 데이터기술, 접근기술 이라고 해두었습니다(MDN에 제가..ㅎㅎ)

위의 설정객체는 2진형태의 플래그를 조합하는 숫자로도 설정할 수 있는데 이 경우 물론 get, set은 함수를 받아야하므로 불가능합니다만 나머지는 처리할 수 있습니다. 자세한 예제는 MDN에 있습니다.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty/Additional_examples

(이런 짓을 하는 가장 큰 이유는 설정객체가 필요없으므로 잦은 설정시 훨씬 가볍게 속성을 처리할 수 있다는 정점 때문인듯합니다만, 짜피 설정객체를 스코프에 캐쉬잡고 해도 매일반이라 현실적인 효과는 그다지..)

설정객체의 기본값

위에서 다룬 설정객체의 키에 대해서 익숙해지도록 간단히 연습해보죠.

var test = {};

// 기본값을 설정하고 오직 읽기전용(non-writable)으로 쓰는 경우
Object.defineProperty(test, "a", {
    value:50,
    writable:false, //쓰기불가!
    enumerable:false, //열거도 하지마!
    configurable:false //이제 이 설정은 바꿀 수 없다!
});

console.log( test.a == 50 ); //true
test.a = 5; //strict모드에서는 예외발생. 아니면 예외는 발생하지 않음
console.log( test.a == 50 ); //true, a에는 할당이 불가능하므로 여전히 처음 50이 보존되어있음

위의 설정을 통하면 기존의 test.a = 50 과는 다르게 변경불가능한 상수를 만들 수 있습니다. 헌데 enumerable, configurable, writable의 기본값은 false고 value, get, set의 기본값은 undefined입니다. 이를 이용하면 동일한 코드를 다음과 같이 간단하게 만들 수 있습니다.

var test = {};
Object.defineProperty(test, "a", {
    value:50 //나머지를 생략했지만 writable, configurable, enumerable이 false다
});

즉 false인 속성들은 그저 생략해도 된다는 것입니다. 이는 기존의 3.0에서 사용하는 일반적인 키설정과 비교해보면 상당히 다른 기본값입니다.

var test = {};

test.a = 50;

위와 같은 코드는 Object.defineProperty를 이용한다면 다음과 같이 표현됩니다.

var test = {};

Object.defineProperty(test, 'a', {
    value:50,
    writable:true,
    configurable:true,
    enumerable:true
});

3.0문법에서 사용하던 기본 설정은 전부 직접적인 데이터기술(data descriptor)로 취급되고 기본값이 대부분 true가 되어버립니다.

설정객체의 프로토타입 체이닝 문제

여기서 한 단계 더 나가면 설정객체가 직접 value, writable등의 키를 갖지 않고 프로토타입으로 상속받는 경우도 인정된다는 것을 알아야합니다. 예를 들어 다음의 설정객체를 생각해보죠.

//설정객체용 클래스
var Descriptor = function(){}
Descriptor.prototype.writable = true; //쓰기 있음
Descriptor.prototype.enumerable = true; //열거 가능
Descriptor.prototype.configurable = false; //재설정이나 키삭제는 불가

//새로운 설정객체 생성
var setting = new Descriptor();
setting.value = 50; //기본값을 50으로 설정

//대상객체 생성
var test = {};

//적용하자.
Object.defineProperty(test, 'a', setting);
/* a키의 설정은 다음과 같다.
value:50
writable:true
enumerable:true
configurable:false
*/

따라서 설정객체가 프로토타입체이닝에 속한 경우는 체이닝상의 모든 속성을 다 뒤져서 6개의 키에 해당되는 게 있는지 일일히 검사해야합니다. 이 문제는 결국 모든 객체가 상속하는 Object.prototype까지 타고 올라오기 때문에 이를 조기에 막을 필요가 있습니다.

//만약 Object.prototype을 오염시킨다면..
Object.prototype.writable = true;

var test = {};
Object.defineProperty(test, 'a', {
    value:50 //기본값으로 writable이 false일 것을 예상하지만..
});


//실제로는 Object.prototype.writable키가 존재하므로 체이닝에 의해 true가 할당되어
test.a = 60;
console.log(test.a); //60 이 할당되어있다.

이렇듯 Object.prototype조작을 통한 기본값 교란이 가능해집니다. 이를 방지하려면 초반에 Object.prototype을 수정할 수 없게 freeze시킬 필요가 있습니다.

//우선 Object.prototype을 얼려버리면 교란할 수 없다.
Object.freeze(Object.prototype);

혹은 설정객체에 명시적으로 4개의 키를 전부 선언하면 체이닝이 되지 않으므로 안전해집니다.

미묘한 configurable

이 설정은 한 번 객체 키에 대한 속성에 대해 정의하고 나면 변경할 수 없다는 것과 그 키를 삭제할 수 없다는 걸 동시에 처리해주는 플래그입니다. 기본은 다음과 같이 사용되죠.

var test = {};

Object.defineProperty(test, 'a', {
  configurable:true, //재변경가능
  value:3, //기본값
  writable:true //할당가능
});

test.a == 3 //기본값반영
test.a = 5; //할당도 가능

Object.defineProperty(test, 'a', {
  value:50,
  writable:false
});

test.a == 50 //새로운 기본값으로 설정됨

근데, configurable이 false일때도 고칠 수 있는 속성이 하나 있는데 writable이 true인 경우 false로 바꾸는 건 허용된다는 것입니다. 즉 아래의 코드는 가능합니다.

var test = {};

Object.defineProperty(test, 'a', {
  configurable:false, //재변경불가!
  value:3, 
  writable:true //할당가능
});

//변경불가지만 writable을 false로 할 수는 있다!
Object.defineProperty(test, 'a', {
  writable:false //할당불가
});

Getter와 Setter

설정객체를 접근기술(accessor descriptor)로 사용하는 경우는 get, set속성을 정의합니다. 이 경우 데이터기술(data descriptor) 소속인 writable이나 value와 같이 사용되면 예외가 발생하게 됩니다.
이 Getter와 Setter야 말로 5.0의 백미가 되는 부분으로 기존에는 함수의 호출만 함축적인 문을 실행할 수 있었으나, 이제는 식이나 값이 문을 실행할 수 있는 컨텍스트가 되는 근본이 됩니다.

getter

get속성에 할당할 함수는 아무런 인자를 받지 않고 하나의 값을 반환하는 콜백함수입니다. 가장 간단하게는 스코프변수와 결합하는 형태를 생각해볼 수 있습니다.

var test = {};
var realA = 50; //진짜 a

Object.defineProperty(test, 'a', {
  get:function(){
    return realA;
  }
});

test.a == 50

get은 근본적으로 함수입니다. 따라서 자바스크립트 함수의 모든 특성을 다 갖게 됩니다. 객체별로 스코프변수를 따로 가져가려면 매번 새로운 함수를 만들어야하므로 getter라는 “get을 만들어주는” 함수를 하나 생각해볼 수 있습니다.

var getter = function(v){
  return function(){
    return v;
  };
};

var test = {};
Object.defineProperty(test, 'a', {
  get:getter(50)
});
Object.defineProperty(test, 'b', {
  get:getter(30)
});

test.a == 50
test.b == 30

간단한 스코프의 결합으로 완전한 상수를 만들어내게 됩니다. 하지만 그저 읽기전용으로 쓸거라면 {value:50}이나 {value:30}으로도 충분하겠죠. setter와 함께 쓰기 위한 목적이 더 큽니다.

setter

자바스크립트의 할당연산자에 대응하도록 콜백함수가 호출되는 setter는 인자가 하나인 함수를 인자로 받습니다. 이러한 setter의 기능이 할당연산자를 함수호출화시켜버립니다. 예를 들어 언제나 배열의 복사본을 받아들이는 간단한 예제로 보죠.

var test = {};
(function(){
  var a;
  Object.defieProperty(test, 'a', {
    set:function(v){
      if (v && typeof v == 'object') {
        a = JSON.parse(JSON.stringify(v)); //객체면 복사본으로!
      } else {
        a = v;
      }
    },
    get:function(){return a;}
  });
})();

var arr = [1,2,3];
test.a = arr;

test.a === arr //false

위의 예에서 코드 상으로는 단순히 할당한 것처럼 보이지만 내부적으로는 사본이 스코프변수 a에 저장되어 복사본으로 처리됩니다. 이 setter는 활용범위가 매우 다양해서 오히려 막연하니 유형별로 간단히 사례를 정리해보죠.

바인딩에 활용하기

기존에는 DOM에서 자바스크립트 객체나 값에는 리스너를 통해 바인딩할 수 있었지만 자바스크립트에서 값의 변화가 생겼을때 DOM객체로 바인딩할 방법이 없었습니다. 하지만 setter를 이용하면 매우 간단하게 실현됩니다.

<input type="text" id="test"/>
var test = {};
(function(){
  var dom, value;

  //바인딩할 대상
  dom = document.getElementById('test');

  Object.defieProperty(test, 'value', {
    set:function(v){
        //value키가 연동하도록 setter구성
        dom.value = value = v;
      }
    },
    get:function(){return value;}
  });

  dom.onchange = function(){
    //dom에서도 변화를 value에 반영해줌.
    value = this.value;
  };
})();

test.value = "abcde"; //input에도 반영됨

값에 대한 검증처리

별도의 밸리데이션 함수를 구성할 필요없이 이제 값의 할당 자체가 검증하도록 처리할 수 있습니다. 만약 검증에 실패한다면 예외를 발생시키거나 로그를 출력할 수 있을 것입니다.

var test = {};
(function(){
  var array;
  Object.defieProperty(test, 'array', {
    set:function(v){
      if (Array.isArray(v)) { //배열일때만 받아줌.
        array = v;
      } else {
        throw '배열이 아님'; //아니면 예외처리
      }
    },
    get:function(){return array;}
  });
})();

test.array = [1,2,3]; //ok!

test.array = 3; //예외발생

getter, setter의 조합

보통 get과 set은 쌍을 이루어 사용하는 경우가 많습니다. 만약 접근기술(accessor descriptor)에 set만 있고 get이 없다면 할당은 되지만 조회하면 기본값이 undefined가 됩니다. 반대로 get만 구현하면 읽기전용이 되어버리죠.
set이 정의되지 않은 경우에 할당을 시도하면 strict모드에서는 예외가 발생하고 일반 모드에서는 아무런 이상없이 진행되지만 실제 값을 갱신되지 않는 식으로 반응합니다.

var test = {};
Object.defineProperty(test, 'a', {
  get:function(){return 3;}
});

test.a = 33; //아무런 반응없이 통과됨.

test.a == 3 //실제로는 아무 반응 없음.

(function(){
  'use strict'; //strict모드 활성
  test.a = 5 //예외발생
})();

크롬의 경우 에러메서지는 “Cannot set property 키이름 of 대상객체 which has only a getter” 정도로 나옵니다.

Object.defineProperties와 Object.create의 두번째인자.

defineProperty가 객체의 키 하나하나마다 설정하는 메소드라면 defineProperties는 그저 한번에 설정객체를 묶어서 보내는 편의성 메소드입니다. 별거 아니니 간단히 코드로 살펴보겠습니다.

var test = {};
Object.defineProperty(test, 'a', {value:30});
Object.defineProperty(test, 'b', {value:50});

//두번에 걸쳐서 정의하는게 불편하다면 아래와 같이 하자
var test2 = {};
Object.defineProperties(test, {
  a:{value:30},
  b:{value:50}
});

하나의 객체로 보낼 수 있다는 건 사실 편리성 그 이상입니다. 그 객체에 필요한 속성을 프리셋으로 만들어두고 일괄로 적용할 수 있기 때문이죠.
5.0에서는 하나의 객체가 갖는 정체성을 프로토타입체이닝과 상세한 본인에게 정의된 키로 달성하려 합니다.
그 두 개의 축중 하나가 바로 defineProperties에 오는 설정객체 셋입니다. 자동차를 정의하려 할 때

  1. 인스턴스마다 연료량과 주행거리를 갖고 있다면 이는 defineProperties의 설정객체 셋으로 표현할 수 있을테고
  2. 주행하기 등의 메소드는 모든 인스턴스가 공통으로 갖고 있는 프로토타입체인대상으로 볼 수 있을 것입니다.

Object.create는 그러한 이유로 프로토타입체이닝 대상객체와 defineProperties의 설정객체 셋을 인자로 받습니다.

var carProto = {
  run:function(v){
    if (this.fuel > v) {//연료가 된다면
      this.fuel -= v;
      this.distance += v;
    }
  },
  refuel:function(v){
    if (this.max >= this.fuel + v) {//최대연료량이내라면
      this.fuel += v;
    }
  },
  max:50
};
var carProperties = {
  fuel:{writable:true, value:carProto.max}, //만땅출고 ^^
  distance:{writable:true, value:0}
};

var car1 = Object.create(carProto, carProperties);
car1.run(20);
car1.distance == 20
car1.fuel == 30

car1.refuel(10);
car1.fuel == 40

Object.preventExtensions

하…이제 좀 Object.defineProperty에서 빠져나왔습니다. 이후 Object와 관련된 포스팅에서 더욱 깊이 다룰테니 이번엔 이 정도에 마감하고 이제 다른 정적메소드를 살펴보겠습니다.
Object는 해쉬맵구조로 자유롭게 키를 추가하거나 삭제할 수 있는데 더 이상 객체에 키를 추가할 수 없도록 막는 것이 바로 이 메서드입니다.
일단 확장금지가 된 객체는 키를 추가해도 아무런 반응을 안하거나 strict모드에서는 예외를 발생시키게 됩니다. 위에서 다룬 defineProperty의 경우 확장 금지된 객체에 적용하려고 하면 항상 예외가 발생합니다.
하지만 확장만 막았기 때문에 키의 삭제는 여전히 자유롭습니다.

var test = {a:3};

Object.preventExtensions(test);

test.b = 5; //추가되지 않고 예외발생안함

(function(){
  'use strict';
  test.b = 5; //예외발생
})();

Object.defineProperty(test, 'b', {}); //예외발생

//하지만 삭제는 가능함!
delete test.a;
  • 5.0에서는 상관없지만 표준적으로 __proto__ 에 접근할 수 있는 ES6.0에서는 __proto__ 에 대한 할당도 예외로 처리됩니다….만 6.0에서 다루도록 하죠 ^^;

쨌든 키를 추가는 못하지만 삭제는 할 수 있기 때문에 객체에 대한 안정성을 담보하기는 어렵습니다. 제 개인적으로 상대적인 활용빈도가 낮았습니다.

Object.seal

위의 preventExtensions에 한단계 더 나아가 객체의 키를 추가할 수 없을 뿐만 아니라 삭제도 못하게 해주는 메소드입니다. 이제 좀 안정성이 생긴 느낌이죠. 원래 파기된 ES4.0규격에는 sealed Object라는 개념이 있었는데, 그 일부가 메소드로 반영되었습니다. 이 개념은 다시 ES6.0의 class문에 이어지게 됩니다.

(음. ES6.0 표준이 확정발표되어 자꾸 6.0과 섞어서 얘기하게 되는군요 ^^ 5.0에 집중하겠습니다!)

객체의 키를 추가도 삭제도 못하지만 그 키에 있는 값은 바꿀 수 있습니다. 이는 마치 자바에서 정의한 클래스의 인스턴스처럼 사전에 정의한 필드만 사용하도록 강제하는 효과가 있습니다. 불안정한 범용객체에서 목적에 적합한 전용객체로 사용할 수 있게 합니다.

//학생을 하나 만들고
var student = {grade:5, class:3, id:1242422, name:'hika'};

Object.seal(student); //봉인하자.

student.grade += 1; //학년이 오르고
student.class = 2; //반이 바뀐다.

student.nick = 'dev'; //이상한 키를 추가하려고해도 효과가 없음

delete student.id; //정의된 키를 지울 수도 없다.

굉장히 유용하고 seal이야말로 이 포스팅 초반에 다루고 있던 정의시점의 안정성확보에 핵심적인 역할을 수행하게 됩니다.

seal과 defineProperty의 미묘함

seal을 실행하면 해당 객체의 모든 키는 삭제할 수 없게 되고 새로운 키를 추가할 수 없게 됩니다. 근데 그건 defineProperty의 configurable이 하는 일이기도 합니다. seal을 객체적용하면 마치 모든 키에 configurable이 적용된 것처럼 delete를 할 수 없게 되는데, 여기서 미묘한 점은 바로 configurable의 기능이 두가지라는 것입니다.
첫 번째는 delete를 막는 기능입니다. 이 기능은 seal과 일치하죠. 두 번째가 문젠데 두 번째 기능은 재설정을 불가능하게 하는 기능입니다. 과연 seal을 통해 봉인된 객체의 키는 재설정이 불가능한 것인가가 바로 궁금한 점입니다.
결론부터 말하자면 매우 특수하게 되어버린다는 것입니다. 이 특수한 상황을 이해하기 위해서는 위에서 설명한 configurable = false인 경우 writable은 false로만 고칠 수 있던걸 먼저 상기해야합니다.

  1. seal된 객체는 configurable:false인 상태와 매우 유사하다.
  2. 하지만 configurable:false일때 writable은 false로 바꿀 수 있었던 것처럼, seal상태에서도 writable은 false로 바꿀 수 있다.
  3. 접근기술(accessor description)으로 정의된 get, set의 경우는 변경불가.
  4. 헌데 seal의 경우는 value도 변경할 수 있다.
  5. 하지만 writable:false인 경우는 value를 변경할 수 없다.

특수한 건 바로 4, 5번이죠.
4번의 경우 writable:false 외에도 value도 바꿀 수 있다는 게 defineProperty로는 할 수 없는 일입니다.
5번의 경우는 defineProperty의 경우 wrirable:false라도 configurable:true라면 value는 얼마든지 재설정할 수 있는데, seal한 후에는 예외가 발생하는 점이 특이합니다.

var test = {];

//일반키 a설정
test.a = 30;

//데이터기술(data descriptor)을 통해 b설정
Object.defineProperty(test, 'b', {
  writable:true,
  value:50
});

//접근기술(accessor descriptor)을 통해 c설정
Object.defineProperty(test, 'c', {
  get:function(){return 70;}
});

//객체봉인
Object.seal(test);

//b키의 value변경 - 4번 상황
Object.defineProperty(test, 'b', {
  value:70
});
test.b == 70 //true 잘됨!

//b키의 writable을 false로 만들자 - 5번 상황 준비
Object.defineProperty(test, 'b', {
  writable:false
});
//b키의 value를 writable:false상황에서 변경시도 - 5번 상황
Object.defineProperty(test, 'b', { 
  value:90
});
//예외가 발생함 - Cannot redefine property: b

//접근기술에 의한 속성은 일체 변경 불가
Object.defineProperty(test, 'c', {
  get:function(){return 5;}
});//예외발생

다이어그램이나 순서도로 표현하면 좀 더 이해하기 편하시겠지만..여기까지..=.=;

Object.freeze

객체를 꽁꽁얼려주는 freeze는 seal에서 한층 강화되어 아예 값의 변화나 속성의 변화를 일체 허용하지 않습니다…..라고 끝내면 참 좋겠지만 여기에는 세 가지 구멍이 존재합니다. 그냥 얼리는거야 호출하면 끝이니 가볍게 건너뛰고 3가지 구멍에 대해서 알아보겠습니다.

1. 프로토타입을 통한 우회

그 객체는 얼렸지만 그 객체의 프로토타입을 얼리지 않으면 키는 자유롭게 추가됩니다.

//프로토타입
var parent = {b:30}

//인스턴스 생성
var test = Object.create(parent);

//얼리자
Object.freeze(test);

//하지만 프로토타입체인은 일어나고..
test.b == 30 //true

//그 값을 바꿀 수도 있고..
parent.b = 70;
test.b == 70 //true

이를 방지하려면 프로토타입도 얼려버릴 수 밖에 없습니다. 하지만 프로토타입은 체이닝 되므로 결국 Object.prototype까지 전부 얼려버리지 않으면 안심할 수 없겠죠. 이를 자동화할 방법은 5.0에서는 없습니다만 6.0의 __proto__ 를 사용하면 가능합니다.

var freeze = function(target){
  do{
    if (!Object.isFrozen(target)) {
      Object.freeze(target);
    }
  while(target = target.__proto__)
};

freeze(test);

2. 참조값을 통한 우회

freeze는 그 객체의 키에 할당된 값을 변경할 수 없게는 하지만 참조하고 있는 객체까지 얼려주는 것은 아닙니다. 즉 다음과 같이 키에 객체가 들어가 있으면 얼마든지 변경할 수 있죠.

var arr = [1,2,3];
var test = {arr:arr};

Object.seal(test);

test.arr[2] = 5; //참조객체는 얼리지 않았으므로 변경가능!

이를 막으려면 모든 키를 반복적으로 조사하여 모두 얼려버리는 수 밖에 없습니다. MDN에는 재귀함수를 통한 샘플코드가 deepFreeze이라는 함수명으로 개제되어있습니다.
하지만 재귀함수는 위험하니 간단히 큐구조를 이용한 루프로 고쳐보죠.

var deepFreeze = function(target){
  var stack = [target]; //스택생성
  while (target = stack.pop()){ //target을 현재 대상으로 보고
    for(var k in target){
      //소유한키고, null이 아닌 객체라면
      if (target.hasOwnProperty(k) && k && typeof target[k] == 'object') {
        //얼리지 않았다면 얼리자
        if (!Object.isFrozen(target[k])) Object.freeze(target[k]);
        //객체이므로 이넘도 조사가 필요하니 스택에 추가한다.
        stack.push(target[k]);
      }
    }
  }
};

deepFreeze(test);

이해를 돕기 위해 속성만 검사했지만 실무에서는 위에서 작성한 프로토타입을 얼리던 로직과 병합한 함수를 사용합니다(__proto__는 6.0로직이긴 하지만 현시점에서 IE11을 포함한 전 PC브라우저와 모바일브라우저가 지원하고 있습니다)

3. 접근기술(accessor descriptor)를 통한 우회

얼리기 전의 속성을 접근기술을 통해 getter, setter화 한 경우는 얼린 후에도 자유롭게 수정이 가능합니다.

var test = {};

(function(){
  var a = 10;
  Object.defineProperty(test, 'a', {
    get:function(){return a;},
    set:function(v){a = v;}
  });
})();

Object.freeze(test);

test.a = 50;

test.a == 50 //true

이 부분은 오히려 이용해야할 우회방법입니다. 객체를 얼려서 외부 변화에 안전하게 만들면서도 원하는 속성은 게터 세터를 이용해 수정하게 만들 수 있습니다. 이 기능을 잘 이용하면 정의시점에서 거의 정적시점클래스와 동등한 수준의 안정성을 확보할 수 있습니다. 이 부분은 다른 포스팅에서 보다 깊게 다루기로 하고 이 번에는 우회사례로 소개하는 정도로 마치겠습니다.

  • preventExtensions, seal, freeze은 6.0과 5.0의 차이가 존재합니다. 5.0에서는 객체 외의 것을 인자로 넘기면 예외가 발생하지만 6.0에서는 기본값을 비롯해 무엇을 넘겨도 예외가 발생하지 않도록 변경되었습니다.

결론

결국 이렇게 긴 포스팅에서 살펴본 메소드는 고작 defineProperty, defineProperties, preventExtensions, seal, freeze 정도입니다. 하지만 이 메소드들은 자바스크립트 문법을 완전히 변경할 수 있으며, 정의시점에 안정적인 객체구조물을 만들 수 있는 강력한 토대를 제공합니다.
다음 포스팅에서는 이를 보다 적극적으로 활용하는 방법을 고찰해보겠습니다.