객체지향 프로그래밍

객체 지향 프로그래밍

객체지향이 뭔데?

어려운 말이 정말 많다. 관계성 있는 객체들의 집합? 책임을 갖는 기계나 부품의 결합? 정도로 표현할 수 있겠다. (그나마 쉽게 말하면…)
객체지향이라는 의미 자체를 한마디로 정의하기가 정말 힘들다. 그래서 우리는 객체지향 프로그래밍이 의미하는 바를 짚어보면서 이해해보고자 한다.

클래스 기반과 프로토타입 기반

프로그래밍 언어를 자바스크립트로 처음 접한 사람은(저를 포함해서) 클래스 기반을 이해하기가 조금은 힘들 수 있다. 많은 사람들이 사용하는 Java, c++가 이에 해당하며 클래스에 정의된 메서드를 사용하여 여러가지 기능을 수행하는 언어이다. 클래스란 한 집단에 속해 있는 속성과 행위들을 정의한 것이다. 즉 클래슨느 객체 생성의 패턴이나 설계도 정도로 말할 수 있겠다.

반면 프로토타입 기반의 언어는 객체의 자료구조나 메서드 등을 동적으로 바꿀 수 있다. 즉 자바스크립트는 프로토타입 기반의 객체 지향 언어라고 할 수 있겠다. 예를 들어 객체 생성 방법만 해도 객체 리터럴, Object() 생성자 함수, 생성자 함수 등의 방법이 있다.

function Person(name) {
  this.name = name;

  this.setName = function (name) {
    this.name = name;
  }

  this.getName = function () {
    return this.name;
  };
}

var me = new Person('jang');
console.log(me.getName());

me.setName('Jang');
console.log(me.getName());

출력결과

jang
Jang

굳이 이럴 필요는 없다.(좀 인스턴가 동일한 메서드를 각자 가지고 있어 메모리 낭비이다.) 이 함수를 프로토타입을 이용하여 바꾸면

function Person(name) {
  this.name = name;

  Person.prototype.setName = function (name) {
    this.name = name;
  }

  Person.prototype.getName = function () {
    return this.name;
  }
};

var me = new Person('Jang');
var you = new Person('Lee');
var him = new Person('Park');

console.log(Person.prototype);

console.log(me);
console.log(you);
console.log(him);

출력결과

Person { setName: [Function], getName: [Function] }
Person { name: ‘Jang’ }
Person { name: ‘Lee’ }
Person { name: ‘Park’ }

함수프로토타입

setName, getName 함수를 프로토타입으로 이동시켜씩 때문에 모든 인스턴스가 참조할 수 있다.

아래 예제는 자바스크립트의 권위자인 더글라스 크락포드가 제안한 프로토타입에 객체에 메서드를 추가하는 방법이다.

참조 링크는 참조하기

/**
* 모든 생성자 함수의 프로토타입은 Function.prototype이다. 따라서 모든 생성자 함수는 Function.prototype.method()에 접근할 수 있다.
* @method Function.prototype.method
* @param ({string}) (name) - (메소드 이름)
* @param ({function}) (func) - (추가할 메소드 본체)
*/
Function.prototype.method = function (name, func) {
  // 생성자함수의 프로토타입에 동일한 이름의 메소드가 없으면 생성자함수의 프로토타입에 메소드를 추가
  // this: 생성자함수
  if (!this.prototype[name]) {
    this.prototype[name] = func;
  }
};

/**
* 생성자 함수
*/
function Person(name) {
  this.name = name;
}

/**
* 생성자함수 Person의 프로토타입에 메소드 setName을 추가
*/
Person.method('setName', function (name) {
  this.name = name;
});

/**
* 생성자함수 Person의 프로토타입에 메소드 getName을 추가
*/
Person.method('getName', function () {
  return this.name;
});

var me  = new Person('Lee');
var you = new Person('Kim');
var him = new Person('choi');

console.log(Person.prototype);
// Person { setName: [Function], getName: [Function] }

console.log(me);  // Person { name: 'Lee' }
console.log(you); // Person { name: 'Kim' }
console.log(him); // Person { name: 'choi' }

상속

자바스크립트는 전통적인 상속을 지원하지 않는다. 그래서 프로토타입 체인을 이용하여 상속을 구현해낸다. 즉 프로토타입을 통해서 객체가 다른 객체로 상속된다고 보면 되겠다. 방법에는 클래스 방식을 따라하는 Pseudo-classical 방식과 프로토타입 상속이 있다.

  1. 의사 클래스 상속 구현

var Parent = (function () {
  function Parent (name) {
    this.name = name;
  }

  Parent.prototype.sayHi = function () {
    console.log('Hi! ' + this.name);
  };

  return Parent;
}());

var Child = (function () {
  function Child(name) {
    this.name = name;
  }

  // 자식 생성자 함수의 프로토타입 객체를 부모의 인스턴스로 변경
  Child.prototype = new Parent();

  Child.prototype.sayHi = function() {
    console.log('안녕하세요! ' + this.name);
  };

  Child.prototype.sayBye = function() {
    console.log('안녕히가세요! ' + this.name);
  };

  return Child;
}());

var child = new Child('Jang');
console.log(child);

console.log(Child.prototype);

child.sayHi();
child.sayBye();

console.log(child instanceof Parent);
console.log(child instanceof Child);

출력결과

Parent { name: ‘Jang’ }
Parent { name: undefined, sayHi: [Function], sayBye: [Function] }
안녕하세요! Jang
안녕히가세요! Jang
true
true

인스턴스 child의 프로토타입 객체는 Parent 생성자 함수가 생성한 인스턴스이다. 또한 Parent 생성자 함수가 생성한 인스턴스의 프로토타입 객체는 Parent.prototype이다. 따라서 child는 Parent 생성자 함수가 생성한 인스턴스와 Parent.prototype에 있는 모든 프로퍼티에 접근할 수 있다. 여기서 sayBye() 메서드는 Parent 생성자 함수 인스턴스에 위치한다.

의사상속

하지만 new 연산자를 사용하여 불필요한 객체를 계속 생성하여야 하고 new 키워드를 빼먹을 경우 this가 전역 객체에 바인딩되는 실수가 일어날 수 있다. 또한 프로토타입 객체를 인스턴스로 교체하면서 constructor가 깨지게 된다.

  1. 프로토타입 상속

그래서 결국은 자바스크립트의 프로토타입 체인을 이용한 상속을 이용하여야 한다.

var Parent = (function () {
  function Parent(name) {
    this.name = name;
  }

  Parent.prototype.sayHi = function() {
    console.log('Hi ' + this.name);
  };

  return Parent;
}());

var child = Object.create(Parent.prototype);
child.name = 'Jang';

child.sayHi();

console.log(child instanceof Parent);

출력결과

Hi Jang
true

프로토타입 상속

객체리터럴로도 사용 가능하다.

var parent = {
  name: 'Jang',
  sayHi: function() {
    console.log('Hi ' + this.name);
  }
};

var child = Object.create(parent);
child.name = 'kim';

parent.sayHi();
child.sayHi();

console.log(parent.isPrototypeOf(child));

출력결과

Hi Jang
Hi kim
true

Object.create 함수는 매개변수에 프로토타입으로 설정할 객체 또는 인스턴스를 전달하고 이를 상속하는 새로운 객체를 생성한다.
이 함수의 폴리필을 구현하는 함수가 있다.

function create_object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

비어 있는 함수 F를 생성하여 그 프로토타입의 프로퍼티에 매개변수로 전달받은 객체를 넣는다. 그 후 생성자 함수 F로 새로운 객체를 생성하고 반환하는 것이다.

캡슐화

캡슐화는 객체지향 프로그래밍에서 정말 중요한 개념이다. 그 정의는 필요한 여러가지 정보를 하나의 틀 안에 담는 것을 말한다.
관련있는 변수와 메서드를 클래스 같은 틀 안에 담는다고 생각하면 편하겠다. 또한 외부에 공개될 필요가 없는 정보는 은닉한다.
원래의 자바스크립트는 public이나 private 같은 키워드를 제공하지 않지만 캡슐화가 가능하다.

var person = function(arg) {
  var name = arg ? arg : ''; // 삼항 연산자

  return { getName: function() {
      return name;
    },
    setName: function(arg) {
      name = arg;
    }
  }
};

var me = person('Jang');

var name = me.getName();
console.log(name); // Jang

me.setName('kim');
name = me.getName();
console.log(name); // kim

person 함수가 객첼르 반환하면서 객체 내 메서드 getName, setName은 클로저로, private 변수에 접근할 수 있다.
이것이 모듈화이며 캡슐화와 은닉 정보를 제공한다.

단 주의할 점은

  1. private 멤버가 객체나 배열일 경우 쉽게 변경할 수 있다.

객체 반환 시 반환값은 얉은 복사를 통해서 private 멤버의 참조값을 반환하는 데 이는 외부에서도 private 값을 변경할 수 있음을 나타낸다. 이를 해결하려면 깊은 복사를 활용해야 한다.

  1. person 함수가 반환한 객체는 person 함수 객체의 프로토타입에 접근할 수 없다. = 상속을 구현하 수 없다.

위는 생성자 함수가 아니고 그냥 메서드를 담은 객체이기 때문이다.

var Person = function() {
  var name;

  var F = function(arg) { name = arg ? arg : ''; };

  F.prototype = {
    getName: function() {
      return name;
    },
    setName: function(arg) {
      name = arg;
    }
  };

  return F;
}();

var me = new Person('Jang');

console.log(Person.prototype === me.__proto__); // true

console.log(me.getName()); // Jang
me.setName('kim');
console.log(me.getName()); // kim

깊은 복사

객체지향 프로그래밍은 결국 재사용성,유지보수를 잘하기 위해 사용하려고 하는 것이다.