Encapsulate Variable, 변수 캡슐화하기를 알아보자.

요약

코드

let defaultOwner = {firstName: "마틴", lastName: "파울러"};
let defaultOwnerData = {firstName: "마틴", lastName: "파울러"};
 
export function defaultOwner() {
    return defaultOwnerData;
}
 
export function setDefaultOwner(arg) {
    defaultOwnerData = arg;
}

배경

  • 함수는 데이터보다 다루기가 수월하다.
  • 반면에 데이터는 다루기가 까다롭다. 유효범위가 넓어 고쳐야할 곳이 많아진다.
  • 접근할 수 있는 범위가 넓은 데이터를 옮길 때는, 그 데이터로의 접근을 독점하는 함수로 캡슐화하는 것이 좋다.
  • 해당 함수를 통해야만 변경하고 사용할 수 있다면, 변경 전 검증이나 추가 로직을 쉽게 끼워넣을 수 있다.
  • 객체 지향에서 객체의 데이터를 항상 private 으로 유지해야 한다고 강조하는 이유가 여기에 있다.
  • 범위를 제한해야 한다.
  • 이보다 좋은 것은 불변 데이터를 사용하는 것이다.

절차

  1. 변수로의 접근과 갱신을 전담하는 캡슐화 함수들을 만든다.
  2. 정적 검사를 수행한다.
  3. 변수를 직접 참조하던 부분을 모두 적절한 캡슐화 함수 호출로 바꾼다. 하나씩 바꿀 때마다 테스트한다.
  4. 변수의 접근 범위를 제한한다.
    • 변수로의 직접 접근을 막을 수 없을 때도 있다. (컴파일러 오류?)
    • 이런 경우 이름자체를 바꿔버리면 에러가 뜨니 쉽게 찾을 수 있다.
  5. 테스트한다.
  6. 변수 값이 레코드라면 레코드 캡슐화하기를 적용할지 고려한다.

따라하기

let defaultOwner = {firstName: "마틴", lastName: "파울러"};
 
spaceship.owner = defaultOwner;
 
defaultOwner = {firstName: "레베카", lastName: "파슨스"};
  • 전역 변수에 데이터가 담겨있는 경우다.

  • 참조하는 코드가 있다.

  • 이번에는 갱신하는 코드도 있다.

1. 캡슐화를 위해 읽고 쓰는 함수를 정의하자.

let defaultOwner = {firstName: "마틴", lastName: "파울러"};
 
function defaultOwner() {
    return defaultOwner;
}
 
function setDefaultOwner(arg) {
    defaultOwner = arg;
}
 
spaceship.owner = defaultOwner;
 
defaultOwner = {firstName: "레베카", lastName: "파슨스"};

2. 참조하는 곳을 찾아 바꾼다.

let defaultOwner = {firstName: "마틴", lastName: "파울러"};
 
function defaultOwner() {
    return defaultOwner;
}
 
function setDefaultOwner(arg) {
    defaultOwner = arg;
}
 
spaceship.owner = defaultOwner();
 
setDefaultOwner({firstName: "레베카", lastName: "파슨스"});
  • 하나씩 바꿀때마다 테스트하자.

3. 모든 참조를 수정했다면 변수의 범위를 제한하자.

// defaultOwner.js
let defaultOwner = {firstName: "마틴", lastName: "파울러"};
 
export function defaultOwner() {
    return defaultOwner;
}
 
export function setDefaultOwner(arg) {
    defaultOwner = arg;
}

값 캡슐화하기

  • 위의 방법대로 하면 변수 자체에 접근해서 값을 바꾸거나 대입하는 것은 불가능하다.
  • 하지만 레퍼런스 타입의 값을 바꾸는 것이기 때문에 다음과 같은 행위는 제어할 수 없다.
const owner1 = defaultOwner();
assert.equal("파울러", owner1.lastName, "처음 값 확인");
const owner2 = defaultOwner();
owner2.lastName = "파슨스";
assert.equal("파슨스", owner1.lastName, "owner2를 변경한 후");
  • 이런 경우를 방지하기 위해 레퍼런스 타입의 값을 반환할 때는 항상 복제본을 반환하도록 하자.
// defaultOwner.js
let defaultOwnerData = {firstName: "마틴", lastName: "파울러"};
 
export function defaultOwner() {
    return Object.assign({}, defaultOwnerData);
}
 
export function setDefaultOwner(arg) {
    defaultOwnerData = arg;
}
  • 특히 리스트에 이 기법을 많이 사용한다.
  • 하지만 만약 누군가가 원본을 변경하기를 원한다면 어떻게 해야할까?
  • 즉, defaultOwnerData 의 내부 값을 변경하고 싶다. (firstName)
  • 이런 경우 레코드 캡슐화하기를 사용하자.
// defaultOwner.js
let defaultOwnerData = {firstName: "마틴", lastName: "파울러"};
 
export function defaultOwner() {
    return new Person(defaultOwnerData);
}
 
export function setDefaultOwner(arg) {
    defaultOwnerData = arg;
}
 
class Person {
    constructor(data) {
        this._lastName = data.lastName;
        this._firstName = data.firstName;
    }
 
    get lastName() {
        return this._lastName;
    }
 
    get firstName() {
        return this._firstName;
    }
}
  • 지금까지 게터에서 데이터를 복제하는 방법을 살펴보았다.
  • 그런데 세터에서도 복제본을 만든느 것이 좋다.
  • 복제가 주는 성능이 걱정될 수 있으나, 영향은 미미하다.
  • 오히려 원본을 사용할 경우에 생기는 문제가 더 많다.
  • 다만 지금 에시에서는 복제본을 만들 떄 depth가 1인 경우만 파악했다. 더 깊을 경우 복잡해진다.

Reference