Prototypal Pattern vs Classical OOP in JS

Hansoo Kim·2020년 4월 27일
1

이 글은 유튜브 동영상 리뷰가 포함되어 있습니다.

개요
  1. 객체지향 프로그래밍의 정의
  2. 프로토타입기반 프로그래밍의 정의
  3. 이 둘의 차이점.
객체지향 프로그래밍의 정의

객체지향 프로그래밍은 실제 세계에 기반한 모델을 만들기 위해 추상화를 사용하는 프로그래밍 패러다임이다. 객체지향 프로그래밍은 modularity, polymorphism, encapsulation을 포함하여 이전에 정립된 패러다임들부터 여러가지 테크닉들을 사용한다.

객체지향 프로그래밍은 함수들의 집합 혹은 단순한 컴퓨터의 명령어들의 목록 이라는 기존의 프로그래밍에 대한 전통적인 관점에 반하여, 관계성있는 객체들의 집합이라는 관점으로 접근하는 소프트웨어 디자인으로 볼 수 있다. 객체지향 프로그래밍에서, 각 객체는 메시지를 받을 수도 있고, 데이터를 처리할 수도 있으며, 또다른 객체에게 메시지를 전달할 수도 있다. 각 객체는 별도의 역할이나 책임을 갖는 작은 독립적인 기계로 볼 수 있는 것이다.

객체지향 프로그래밍은 보다 유연하고 유지보수성이 높은 프로그래밍을 하도록 의도되었고, 대규모 소프트웨어 공학에서 널리 알려져 있다.

MDN의 글을 인용한 것이다. 너무나 많이 읽어봤던 익숙한 설명이다.

프로토타입기반 프로그래밍의 정의

프로토타입 기반 프로그래밍은 클래스가 존재하지 않는 객체지향 프로그래밍의 한가지 스타일로, 동작 재사용(behavior reuse, 클래스기반 언어에서는 상속이라고함)은 프로토타입으로서 존재하는 객체를 데코레이팅하는 과정을 통해 수행된다.

프로토타입 기반 언어의 원형적인 예는 David Ungar과 Randall Smith가 개발한 'Self'라는 프로그래밍 언어이다. 그러나 클래스가 없는 프로그래밍 스타일이 최근 인기를 얻으며 성장하였고, 자바스크립트, Cecil, NewtonScript, Io, MOO, REBOL, Kevo, Squeak 등의 언어에서 채택되어 왔다.

MDN의 글을 가지고 왔다. 결국 프로토타입 기반 프로그래밍은 객체지향 프로그래밍의 한가지 스타일이다. 영상에서는 Prototypal Pettern이 더 최근에 나온 패턴이고 a few advantages가 있다고 소개한다. 내가 이 영상을 리뷰한 이유는 아무리 찾아도 왜 자바스크립트는 오랫동안 사용했던 class를 사용하지 않고 prototype을 채택했던 걸까?에 대한 답을 찾기 어려웠기 때문이다.

Javascript prototype
let animal = {};
animal.name = 'Leo';
animal.energy = 10;

animal.eat = function (amount) {
  console.log(`${this.name} is eating.`);
  this.energy += amount;
};

animal.sleep = function (length) {
  console.log(`${this.name} is sleeping.`);
  this.energy += length;
};

animal.play = function (length) {
  console.log(`${this.name} is playing.`);
  this.energy -= length;
};

animal객체를 만들고 각종 property를 생성한다. 하지만 프로젝트에서 여러개의 animal이 필요하게 되었고 위 코드를 function으로 만들게 된다.

function Animal(name, energy) {
  let animal = {};
  animal.name = name;
  animal.energy = energy;

  animal.eat = function (amount) {
    console.log(`${this.name} is eating.`);
    this.energy += amount;
  };

  animal.sleep = function (length) {
    console.log(`${this.name} is sleeping.`);
    this.energy += length;
  };

  animal.play = function (length) {
    console.log(`${this.name} is playing.`);
    this.energy -= length;
  };

  return animal;
}

const leo = Animal('Leo', 7);
const snoop = Animal('Snoop', 10);

leosnoopAnimal function을 재사용해 만들었다. 하지만 eat, sleep, play 메소드가 Animal이 더 생길때마다 계속 정의되어 메모리에 저장된다는 문제가 있다. 이 문제를 해결하기 위해 animalMethods 객체를 생성하여 참조해주자!

const animalMethods = {
  eat(amount) {
    console.log(`${this.name} is eating.`);
    this.energy += amount;
  },
  sleep(length) {
    console.log(`${this.name} is sleeping.`);
    this.energy += length;
  },
  play(length) {
    console.log(`${this.name} is playing.`);
    this.energy -= length;
  },
};

function Animal(name, energy) {
  let animal = {};
  animal.name = name;
  animal.energy = energy;

  animal.eat = animalMethods.eat;
  animal.sleep = animalMethods.sleep;
  animal.play = animalMethods.play;

  return animal;
}

const leo = Animal('Leo', 7);
const snoop = Animal('Snoop', 10);

메소드들이 계속 정의되어 메모리를 낭비하는 문제를 해결한 코드다. 하지만 이 코드에도 문제가 있다. animal.poop처럼 새로운 메소드가 필요하게 되면 계속 바인딩을 시켜줘야하는 문제점이 있다. 이 문제를 해결하기 위해 Object.create, prototype이 등장한다.

const animalMethods = {
  eat(amount) {
    console.log(`${this.name} is eating.`);
    this.energy += amount;
  },
  sleep(length) {
    console.log(`${this.name} is sleeping.`);
    this.energy += length;
  },
  play(length) {
    console.log(`${this.name} is playing.`);
    this.energy -= length;
  },
};

function Animal(name, energy) {
  let animal = Object.create(animalMethods);
  animal.name = name;
  animal.energy = energy;

  return animal;
}

const leo = Animal('Leo', 7);
leo.eat(); // leo is eating 출력
const snoop = Animal('Snoop', 10);
snoop.eat(); // snoop is eating 출력

Object.create(animalMethods)animalMethodsprototype으로 하는 객체를 생성하는 명령이다. prototype에 대한 내가 찾은 가장 자세한 설명 링크 참조. 링크를 정독해보면 알 수 있듯이 마치 class 기반에서 상속 하는 것처럼 prototype기반 언어에서는 객체에서 prototype의 구현체인 __proto__에 상속받은 객체를 넣어 접근 할 수 있게 된다. 하지만 아래 코드는 class와 prototype 기반 프로그래밍의 확연한 차이다.

...
const leo = Animal('Leo', 7);
leo.eat(); // leo is eating 출력
const snoop = Animal('Snoop', 10);
snoop.__proto__.eat = function () {
  console.log('spoiled');
};
snoop.eat(); // spoiled 출력

class기반 OOP 언어에서는 기본적으로 class에 의해 생성된 객체는 해당 클래스의 속성을 바꿀 수 없다. 하지만 prototype은 제약 없이 아주 위험한 자유도를 갖고 점이 내가 생각한 class와의 가장 큰 차이점이다. 심지어 이런 변화가 runtime에서 이루어 질 수 있다는 점이 과연 장점이 될 수 있는 자유도인가 생각을 하게 만든다. 과연 prototypal programmingclassical programming보다 나은 장점이 있을까에 대한 의문을 조금은 풀어준 을 소개한다. 일단 내가 OOP가 확 와 닿을만한 대규모 프로젝트(OOP는 대형 소프트웨어를 효율적으로 빌딩하기 위해 고안 되었다고 한다.)를 해 본 경험이 없기 때문에 이런 글을 찾아봤다.

글의 질문은 내가 너무나도 궁굼해 했던

그래서 대체 클래스 기반 OOP와 프로토타입 기반 OOP의 장단점이 뭐야??

였다. 가장 자신의 경험이 담기고 길게 달아준 mikera의 답변을 정리해 보겠다.

자바에서 RPG 게임을 구현할때 양쪽 접근법을 꽤 경험했다. 나는 원래 클래스 기반의 OOP를 사용해서 설계했지만 결국 이것은 잘못된 접근이라는 것을 깨달았다. 클래스 계층이 확대되면서 도저히 유지보수를 할 수 없는 상태에 이르렀다. 그래서 전체 코드를 프로토타입 기반 코드로 바꿨다. 결과는 훨씬 더 좋고 관리하기 쉬웠다.

장점

  1. It's trivial to create new classes - 여러개의 물약을 모두 다 class로 만들어 관리하는것보다 prototype을 상속받고 몇가지를 바꾸기만 하면 된다. => 사소한 것까지 모두 클래스를 만들어 관리하면 파일도 많아지고 유지보수가 힘들어 진다는 뜻인 것 같다.
  2. It's possible to build and maintain extremely large numbers of "classes" with comparatively little code - 클래스보다 프로토타입을 적용 했을 때 코드량이 많이 줄었다는 뜻인 것 같다.
  3. Multiple inheritance is easy - 다중상속이 쉬워진다는데…….. 왜 쉬워지지 ㅠㅠ??
  4. You can do clever things with property modifiers

…등등 글 참고!

단점

  1. You lose the assurances of static typing - class만큼 객체를 강제하지 않기 때문에 로직에서 핸들링 되는 object가 제대로 동작하는지 확신하기 위해선 더 정확한 테스트가 필요하다.
  2. There is some performance overhead - map검색이 강제되기 때문에 성능 측면에서 약간의 비용을 지불해야 한다고 한다. 정확히 무슨뜻인지 ㅠㅠ..
  3. Refactorings don't work the same way - IDEs / refactoring tools에서 class기반 코드처럼 다 잡아주지 않기 때문에 불편하다.
  4. It's a bit alien - 많은 사람들이 class기반에 익숙하기 때문에 프로토타입 기반의 코드를 틀렸다고 생각하거나 이상하게 생각한다.

이분은 Java의 HashMap으로 프로토타입 기반의 앱을 설계했다고 한다.

이제 차이점을 추상적으로 이해한 것 같으니 위 prototype코드를 new연산자를 사용하는 방식으로 바꿔보자!

function Animal(name, energy) {
  let animal = Object.create(Animal.prototype);
  animal.name = name;
  animal.energy = energy;

  return animal;
}
Animal.prototype.eat = function (amount) {
  console.log(`${this.name} is eating`);
  this.energy += amount;
};
Animal.prototype.sleep = function (length) {
  console.log(`${this.name} is sleeping`);
  this.energy += length;
};
Animal.prototype.play = function (length) {
  console.log(`${this.name} is playing`);
  this.energy -= length;
};
const leo = Animal('Loe', 7);
const snoop = Animal('Snoop', 10);

자바스크립트 function이라면 누구나 가지고 있는 prototype 프라퍼티에 공용 매서드들을 넣어줌으로써 메서드를 공유 할 수 있다. 그리고 new 연산자를 사용해 위 코드를 더욱 간략하게 쓸 수 있도록 바꾼 코드가 아래 코드다.

function AnimalWithNew(name, energy) {
//  let this = Object.create(AnimalWithNew.prototype);
  this.name = name;
  this.energy = energy;
//  return this;
}
const leo = new AnimalWithNew('Leo', 7);
const snoop = new AnimalWithNew('Snoop', 10);

위 코드와 동일한 코드다. new연산자는 위 AnimalWithNew함수의 주석 부분을 내부적으로 해결해줌으로써 객체화를 더욱 쉽게 해준다. 이 코드를 ES6의 class문법으로 바꾸어 보자.

class Animal {
  constructor(name, energy) {
    this.name = name;
    this.energy = energy;
  }
  eat(amount) {
    console.log(`${this.name} is eating`);
    this.energy += amount;
  }
  sleep(length) {
    console.log(`${this.name} is sleeping`);
    this.energy += length;
  }
  play(length) {
    console.log(`${this.name} is playing`);
    this.energy -= length;
  }
}

classprototype방식의 syntactic sugar이다.

const leo = new Animal('leo', 7);
const snoop = new Animal('snoop', 10);
leo.eat(); // leo is eating
leo.__proto__.eat = function () {
  console.log('spoiled');
};
leo.eat(); // spoiled
snoop.eat(); // spoiled
정리

class와 prototype를 추상적으로만 이해하고 있는 단계에서 조금은 구체화를 시킨 것 같다. 아무래도 이런 프로그래밍 패러다임을 깊이 고려할 만큼의 큰 어플리케이션을 만나지 못한 이유가 가장 큰 것 같다. 그래도 이젠 const arr = [1, 2, 3];이 코드가 const arr = new Array(1, 2, 3)이 코드와 같고 우리가 map, slice, reduce등 유용한 메서드들을 바로 이용 할 수 있는 이유는 Array함수의 prototype에 모두 정의되어 있기 때문이라는 것을 알게 되었다!

0개의 댓글