ES6

안녕하세요. API 개발팀 이정준 입니다. ECMAScript6 에서 class가 추가되었습니다. Class Function과 Factory Function의 차이를 알아보고, 어떤 Function을 사용할지 생각해 봅시다.

1.기본 사용 방법

기본 사용 방법은 다음과 같습니다.

  • Class Function
class TodoModel {
  constructor(data) {
    this.todos = [];
    this.data = data;
  }
    
  addData() {
    console.log(`${data} addData`); 
  }

  add() { console.log('add'); }
}

const todoModel = new TodoModel('input');
todoModel.addData();        // input addData
  • Factory Function
function TodoModel(data){
  const todos = [];

  function addData() {
    console.log(`${data} addData`);
  }

  function add() { console.log('add'); }

  return Object.freeze({
    addData,
  });
}

const todoModel = TodoModel('input');
todoModel.addData();        // input addData

2. 캡슐화(Encapsulation)

내부 변수 또는 감추고 싶은 함수에 접근이 가능 여부 입니다.
캡슐화가 안되면 보안에 이슈가 생길 수 있습니다.

  • Class Function
const todoModel = new TodoModel('input');
console.log(todoModel.todos);       // []
console.log(todoModel.data)         // input
todoModel.add();                    // add
  • Factory Function
const todoModel = TodoModel('input');
console.log(todoModel.todos);       // undefined
console.log(todoModel.data)         // undefined
todoModel.add();                    // todoModel.add is not a function

기본적으로 function은 캡슐화가 되지만 class는 캡슐화가 되지 않습니다.
그러나 nodejs 12.0.0 버전부터는 Private class fields를 사용하면 class도 캡슐화를 할 수 있습니다.

  • Private class fields
class TodoModel {
  #todos;

  constructor(data) {
    this.#todos = [];
    this.data = data;
  }

  addData() {
    console.log(`${data} addData`);
  }

  #add() { console.log('add'); }
}

const todoModel = new TodoModel('inputData');
console.log(todoModel.todos);       // undefined
todoModel.add();                    // todoModel.add is not a function

3. 불변성(Immutable)

정의된 함수를 변경할 수 있는지를 말합니다.
보안이나 코드 이해를 위해서는 함수가 변경되지 않는 것이 좋습니다.
특수한 상황에 따라서는 함수 변경이 필요할 수도 있으나 권장하는 방식은 아닙니다.

  • Class Function
todoModel.add = function() {
  console.log('a new add');
}
todoModel.add();            // a new add
  • Factory Function
todoModel.add = function() {
  console.log('a new add');
}
todoModel.add();            // add

Class Function에서는 함수를 변경할 수 있으나 Factory Function에서는 변경되지 않습니다.
이 부분은 static을 사용하면 개선할 수 있으나 인스턴스화 되지 않으므로 사용법에 주의해야 합니다.
그리고 static은 method를 정적으로 만드는 것이기 때문에 상황에 따라서는 static을 사용하는 것이 맞지 않을 수 있습니다.

  • static
class TodoModel {
  #todos;

  constructor(data) {
    this.#todos = [];
    this.data = data;
  }

  addData() {
    console.log(`${data} addData`);
  }

  static add() { console.log('add'); }
}

TodoModel.add();            // add
TodoModel.add = function() {
  console.log("a new add");
}                           // Invalid left-hand side in assignment

4. 상속과 구성 (Composition and inheritance)

class에서는 상속을 사용하지만 factory에서는 구성을 만들어 사용합니다.
예시를 통해 비교해 보면 다음과 같습니다.

  • Class Function
class Person {
  eat() {
    console.log('I am eating');
  }
  breathe() {
    console.log('I am breathing');
  }    
  swim() {
    console.log('I am swimming');
  } 
}
class Wizard extends Person {
  trick() {
    console.log('I am doing a trick');
  }
}
const harry = new Wizard();
const ron = new Wizard();

// Harry can:
harry.eat();          // I am eating
harry.breathe();      // I am breathing
harry.swim();         // I am swimming
harry.trick();        // I am doing a trick

// Ron can:
ron.eat();        // I am eating
ron.breathe();    // I am breathing
ron.swim();       // I am swimming
ron.trick();      // I am doing a trick
  • Factory Function
const Person = () => {
  return {
    eat: () => {
      console.log('I am eating');
    },
    breathe: () => {
      console.log('I am breathing');
    },
    swim: () => {
      console.log('I am swimming');
    },
  };
};
const Trick = () => {
  return {
    trick: () => {
      console.log('I am doing a trick');
    },
  };
};

const Wizard = () => {
  return {
    eat: Person().eat,
    breathe: Person().breathe,
    trick: Trick().trick,
  };
};
const Muggle = () => {
  return {
    eat: Person().eat,
    breathe: Person().breathe,
    swim: Person().swim,
  };
};

// Harry can:
const harry = Wizard();
harry.eat();            // I am eating
harry.breathe();        // I am breathing
harry.trick();          // I am doing a trick

// Ron can:
const ron = Muggle();
ron.eat();            // I am eating
ron.breathe();        // I am breathing
ron.swim();           // I am swimming

class는 상속을 받기 때문에 상속 받은 모든 method를 사용해야 하지만, factory는 구성을 하기 때문에 선별적으로 사용할 수 있습니다. factory와 같이 사용하려면 새로운 class를 생성하거나 class person에서 swim()을 제거한 뒤 새로운 class로 상속 받아야 합니다.

5. this

class에서는 this 문법을 사용할 수 있으나 factory function에서는 this 문법을 사용할 수 없습니다.
this 문법을 사용할 때에는 컨텍스트 손실 문제가 발생할 수 있기 때문에 사용할 때 유의해서 사용해야 합니다.

  • 컨텍스트 손실 문제란?
    context가 손실되는 문제로 아래 예시를 참고하시기 바랍니다.
class TodoModel {
    constructor(){
        this.todos = [];
    }
    
    reload(){ 
        setTimeout(function log() { 
          console.log(this.todos);
        }, 0);
    }
}
todoModel.reload();                   //undefined

6. 메모리

class의 모든 메서드는 프로토 타입 객체에서 한번 생성되고 모든 인스턴스에서 공유됩니다.
그런데 수 많은 동일한 객체를 만들 때 factory 함수의 메모리 비용이 많이 필요합니다.
메모리 테스트 결과는 아래와 같습니다.

The memory cost (in Chrome)
+-----------+------------+------------+
| Instances | 10 methods | 20 methods |
+-----------+---------------+---------+
| 10        | 0          |  0         |
| 100       | 0.1Mb      |  0.1Mb     |
| 1000      | 0.7Mb      |  1.4Mb     |
| 10000     | 7.3Mb      | 14.2Mb     |
+-----------+------------+------------+

7. 결론

메모리 사용에 있어서는 Class Function이 효율적입니다.
그러나 개발하는 내용에 따라 Factory Function이 보다 안전할 수도 있기 때문에 Class Function을 사용할지, Factory Function을 사용할 지는 사용 용도에 맞춰서 결정할 수밖에 없을 듯 합니다.