Object Oriented Programming (OOP)

soom·2021년 1월 28일
2
post-thumbnail

OOP

Object-Oriented Programming (객체 지향 프로그래밍)

  • 프로그램 디자인 철학 중 하나로써, 이전 절차지향 프로그래밍 언어와는 다른 프로그래밍 세트를 쓴다. (예 : Class)
  • OOP는 스스로 지속이 가능한 객체들로 이루어져 있으며, 재사용성을 제공한다.
  • 객체지향언어에서는 모든게 객체이다.
    즉, 프로그램을 구성하는 모든 요소(변수, 함수, 키워드 등)가 객체이다.

Prototyping based Language

자바스크립트는 멀티-패러다임 언어로 명령형(imperative), 함수형(functional), 프로토타입 기반(prototype-based) 객체지향 언어다. 비록 다른 객체지향 언어들과의 차이점에 대한 논쟁들이 있긴 하지만, 자바스크립트는 강력한 객체지향 프로그래밍 능력들을 지니고 있다. 간혹 클래스가 없어서 객체지향이 아니라고 생각하는 사람들도 있으나 프로토타입 기반의 객체지향 언어다.

자바스크립트는 클래스 개념이 없고 별도의 객체 생성 방법이 존재한다. ES6에서 등장한 class도 내부는 prototype으로 구성 되어있다.

  1. literal (객체 리터럴)
var obj1 = {};
obj1.name = 'Lee';
  1. new 생성자 함수
function F() {}
var obj3 = new F();
obj3.name = 'Lee';
  1. Object.create(); (Object.() 생성자 함수)
var obj2 = new Object();
obj2.name = 'Lee';

자바스크립트는 이미 생성된 인스턴스의 자료구조와 기능을 동적으로 변경할 수 있다는 특징이 있다. 객체 지향의 상속, 캡슐화(정보 은닉) 등의 개념은 프로토타입 체인과 클로저 등으로 구현할 수 있다.

클래스 기반 언어에 익숙한 프로그래머들은 이러한 프로토타입 기반의 특성으로 인해 혼란을 느낀다. 자바스크립트에서는 함수 객체로 많은 것을 할 수 있는데 클래스, 생성자, 메소드도 모두 함수로 구현이 가능하다.

ECMAScript 6에서 새롭게 클래스가 도입되었다. ES6의 Class는 기존 prototype 기반 객체지향 프로그래밍보다 Class 기반 언어에 익숙한 프로그래머가 보다 빠르게 학습할 수 있는 단순하고 깨끗한 새로운 문법을 제시하고 있다. ES6의 Class가 새로운 객체지향 모델을 제공하는 것이 아니며 Class도 사실 함수이고 기존 prototype 기반 패턴의 Syntactic sugar이다.

{} Object Literal

체를 생성하는 첫번째 방법인 literal으로 가장 간단하게 변수에 직접 객체를 작성해 넣는 것이다. 방법은 아래와 같다.

const headphone = {
  volume: 0,
  volumeUp: function() {
    if (this.volume < 10) {
      this.volume++;
    }
  },
  volumeDown: function () {
    if (this.volume > 0) {
      this.volume--;
    }
  }
};

장점: 간단하게 작성할 수 있다.
단점: 외부에서 내부 속성에 접근할 수 있기 때문에 객체의 기능을 임의로 조작할 수 있다.

단점을 이해하기 위해 객체 리터럴로 생성된 headphone의 속성으로 volume, volumeUp, volumeDown 세 가지가 있다고 가정해보자.

headphone.volume;
headphone.volumeUp();
headphone.volumeDown();

위의 두 메소드를 실행하면 headphone의 볼륨이 0 - 10 사이에서 커지거나 작아지게 될 것이며, headphone.volume 으로 현재 볼륨을 확인까지 할 수 있을 것이다. headphone의 모든 속성에 접근할 수 있기 때문이다.

그런데 만약 사용자가 메소드 사용 없이 아래와 같은 볼륨 조작을 하게 된다면 어떻게 될까.

headphone.volume = 1000;

headphone의 모든 속성에 접근할 수 있기 때문에 당연한 얘기로 헤드폰의 볼륨은 1000으로 변경된다.

헤드폰 볼륨을 1000으로 사용해도 문제 없다면 뭐 크게 들리고 좋을 수도 있겠지만, 사용자로 하여금 메소드로만 볼륨 조절을 가능하게 하여 의도적인 혹은 실수로 인한 볼륨 임의 조작을 막고 싶다면 어떻게 할 수 있을까?

Encapsulation - 캡슐화

캡슐화를 통해 특정 속성을 외부에 노출시키지 않고 내부에서만 조작하는 방법
속성과 기능을 한 곳에 모아두는 것. 복잡도를 줄이고 재사용성을 높임.

다른 사람이 작성한 코드를 쓰기 위해서는 코드 자체가 잘 정리되어 있어야만 할 것이다. 관련된 데이터와 알고리즘이 하나의 묶음으로 정리되어 있어야한다. 이것을 '캡슐화'라고 한다.

IIFE(Immediately Invoked Function Expression: 즉시 실행 함수)로 객체를 반환하여 scope 사용하기

즉시 실행 함수에 노출시키지 않을 데이터를 변수에 담아 scope로 묶어놓고 반환되는 객체 메소드에서만 접근할 수 있도록 하는 방법이다.

const headphone = (function () {
  let volume = 0;
  
  return {
    volumeUp: function() {
      if (volume < 10) {
        volume++;
      }
    },
    volumeDown: function () {
      if (volume > 0) {
        volume--;
      }
    }
  };
})();

위의 예시에서 headphone의 속성을 확인해보면 volumeUpvolumeDown 두 메소드 뿐이지만 함수가 선언되는 시점에 스코프가 생성되므로 클로저를 사용해 함수의 스코프 내에 접근할 수 있다
따라서 headphone의 메소드(volumeUpvolumeDown)를 사용하게 되면 상위 스코프의 변수인 volumn에 접근해 조절할 수 있다.

하지만 즉시실행함수가 반환하는 객체에 headphone.volume은 포함되지 않았기 때문에 외부에 노출되지 않아 객체 리터럴 방식에서처럼 임의 조작이 불가능하게 된다.

그런데 만약 이렇게 캡슐화된 헤드폰을 대량 생산해야하는 경우가 생긴다면 어떨까?

"new Operator" function

new 연산자는 사용자 정의 객체 타입 또는 내장 객체 타입의 인스턴스를 생성한다.

다시 말해 생성자 함수를 정의하고new 연산자와 함께 생성자 함수를 호출해서 생성한 객체를 인스턴스라 부른다. 생성자 함수의 자식이라고 이해해도 된다.

// 생성자 함수(Constructor)
function Person(name) {
  // 프로퍼티
  this.name = name;

  // 메소드
  this.setName = function (name) {
    this.name = name;
  };

  // 메소드
  this.getName = function () {
    return this.name;
  };
}

// 인스턴스의 생성
var me = new Person('Lee');
console.log(me.getName()); // Lee

// 메소드 호출
me.setName('Kim');
console.log(me.getName()); // Kim

위 예제는 잘 동작한다. 하지만 이 예제는 문제가 많다. Person 생성자 함수로 여러 개의 인스턴스를 생성해보자.

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

console.log(me);  // Person { name: 'Lee', setName: [Function], getName: [Function] }
console.log(you); // Person { name: 'Kim', setName: [Function], getName: [Function] }
console.log(him); // Person { name: 'Choi', setName: [Function], getName: [Function] }

위와 같이 인스턴스를 생성하면 각각의 인스턴스에 메소드 setName, getName이 중복되어 생성된다. 즉, 각 인스턴스가 내용이 동일한 메소드를 각자 소유한다. 이는 메모리 낭비인데 생성되는 인스턴스가 많아지거나 메소드가 크거나 많다면 무시할 수 없는 문제이다.

이같은 문제를 해결하려면 다른 접근 방식이 필요한데 그 해답은 프로토타입이다.

Prototype Chain & Method Definition

모든 객체는 프로토타입이라는 다른 객체를 가리키는 내부 링크를 가지고 있다. 즉, 프로토타입을 통해 직접 객체를 연결할 수 있는데 이를 프로토타입 체인이라 한다.

프로토타입을 이용하여 생성자 함수 내부의 메소드를 생성자 함수의 prototype 프로퍼티가 가리키는 프로토타입 객체로 이동시키면 생성자 함수에 의해 생성된 모든 인스턴스는 프로토타입 체인을 통해 프로토타입 객체의 메소드를 참조할 수 있다.

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('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' }

Person 생성자 함수의 prototype 프로퍼티가 가리키는 프로토타입 객체로 이동시킨 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' }

자세한 내용은 프로토타입 관련글 참고: https://velog.io/@soom/Prototype

Inheritance — 상속

캡슐화 없이 생성자 함수 사용하여 대량 생산하기
부모(base class)로부터 자식(derive class)이 부모의 속성 을 상속 받음. 불필요한 코드를 제거해줌.

코드의 재사용성을 높이기 위한 방법으로, 이미 작성된 부모 클래스를 이어받아서 새로운 자식 클래스를 생성하는 방법이다. 자식 클래스는 부모 클래스의 모든 속성과 기능을 물려 받을 수 있다.

추가로 만약 자식 클래스에만 필요한 기능이 있다면 추가 또는 변경할 수 있다.

다른 사람이 만든 클래스가 내 코드에서는 맞지 않는 경우가 종종 있다. 이런 경우에 상속을 사용해 다른 사람의 클래스를 상속받은 후에 자신이 필요한 부분을 변경하여서 사용할 수 있다. "상속은 기존의 코드를 재사용하는 강력한 기법이다"

자바스크립트는 기본적으로 프로토타입을 통해 상속을 구현한다. 이것은 프로토타입을 통해 객체가 다른 객체로 직접 상속된다는 의미이다. 이러한 점이 자바스크립트의 약점으로 여겨지기도 하지만 프로토타입 상속 모델은 사실 클래스 기반보다 강력한 방법이다.

자바스크립트의 상속 구현 방식은 크게 두 가지로 구분할 수 있다. 하나는 클래스 기반 언어의 상속 방식을 흉내 내는 것(의사 클래스 패턴 상속. Pseudo-classical Inheritance)이고, 두번째는 프로토타입으로 상속을 구현하는 것(프로토타입 패턴 상속. Prototypal Inheritance)이다.

의사 클래스 패턴 상속 (Pseudo-classical Inheritance)

의사 클래스 패턴은 자식 생성자 함수의 prototype 프로퍼티를 부모 생성자 함수의 인스턴스로 교체하여 상속을 구현하는 방법이다. 부모와 자식 모두 생성자 함수를 정의하여야 한다.

// 부모 생성자 함수
var Parent = (function () {
  // Constructor
  function Parent(name) {
    this.name = name;
  }

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

  // return constructor
  return Parent;
}());

// 자식 생성자 함수
var Child = (function () {
  // Constructor
  function Child(name) {
    this.name = name;
  }

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

  // 메소드 오버라이드
  Child.prototype.sayHi = function () {
    console.log('안녕하세요! ' + this.name);
  };

  // sayBye 메소드는 Parent 생성자함수의 인스턴스에 위치된다
  Child.prototype.sayBye = function () {
    console.log('안녕히가세요! ' + this.name);
  };

  // return constructor
  return Child;
}());

var child = new Child('child'); // ①
console.log(child);  // Parent { name: 'child' }

console.log(Child.prototype); // Parent { name: undefined, sayHi: [Function], sayBye: [Function] }

child.sayHi();  // 안녕하세요! child
child.sayBye(); // 안녕히가세요! child

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

Child 생성자 함수가 생성한 인스턴스 child(①)의 프로토타입 객체는 Parent 생성자 함수가 생성한 인스턴스(②)이다. 그리고 Parent 생성자 함수가 생성한 인스턴스의 프로토타입 객체는 Parent.prototype이다.

이로써 child는 프로토타입 체인에 의해 Parent 생성자 함수가 생성한 인스턴스와 Parent.prototype의 모든 프로퍼티에 접근할 수 있게 되었다. 이름은 의사 클래스 패턴 상속이지만 내부에서는 프로토타입을 사용하는 것은 변함이 없다.

이를 그림으로 나타내면 아래와 같다.

의사 클래스 패턴은 클래스 기반 언어의 상속을 흉내내어 상속을 구현하였다. 구동 상에 문제는 없지만 의사 클래스 패턴은 아래와 같은 문제를 가지고 있다.

의사 클래스 패턴 상속 단점

1. new 연산자를 통해 인스턴스를 생성한다.

이는 자바스크립트의 프로토타입 본질에 모순되는 것이다. 프로토타입 본성에 맞게 객체에서 다른 객체로 직접 상속하는 방법을 갖는 대신 생성자 함수와 new 연산자를 통해 객체를 생성하는 불필요한 간접적인 단계가 있다. 클래스와 비슷하게 보이는 일부 복잡한 구문은 프로토타입 메커니즘을 명확히 나타내지 못하게 한다.

게다가 생성자 함수의 사용에는 심각한 위험이 존재한다. 만약 생성자 함수를 호출할 때 new 연산자를 포함하는 것을 잊게 되면 this는 새로운 객체와 바인딩되지 않고 전역객체에 바인딩된다. (new 연산자와 함께 호출된 생성자 함수 내부의 this는 새로 생성된 객체를 참조한다.)

이런 문제점을 경감시키기 위해 파스칼 표시법(첫글자를 대문자 표기)으로 생성자 함수 이름을 표기하는 방법을 사용하지만, 더 나은 대안은 new 연산자의 사용을 피하는 것이다.

2. 생성자 링크의 파괴

위 그림을 보면 child 객체의 프로토타입 객체는 Parent 생성자 함수가 생성한 new Parent() 객체이다. 프로토타입 객체는 내부 프로퍼티로 constructor를 가지며 이는 생성자 함수를 가리킨다. 하지만 의사 클래스 패턴 상속은 프로토타입 객체를 인스턴스로 교체하는 과정에서 constructor의 연결이 깨지게 된다.

즉, child 객체를 생성한 것은 Child 생성자 함수이지만 child.constructor의 출력 결과는 Child 생성자 함수가 아닌 Parent 생성자 함수를 나타낸다. 이는 child 객체의 프로토타입 객체인 new Parent() 객체는 constructor가 없기 때문에 프로토타입 체인에 의해 Parent.prototypeconstructor를 참조했기 때문이다.

console.log(child.constructor);  // [Function: Parent]

3. 객체리터럴

의사 클래스 패턴 상속은 기본적으로 생성자 함수를 사용하기 때문에 객체리터럴 패턴으로 생성한 객체의 상속에는 적합하지 않다. 이는 객체리터럴 패턴으로 생성한 객체의 생성자 함수는 Object()이고 이를 변경할 방법이 없기 때문이다.

var o = {};
console.log(o.__proto__ === Object.prototype); // true

프로토타입 패턴 상속 (Prototypal Inheritance)

프로토타입 패턴 상속은 Object.create 함수를 사용하여 객체에서 다른 객체로 직접 상속을 구현하는 방식이다. 프로토타입 패턴 상속은 개념적으로 의사 클래스 패턴 상속보다 더 간단하다. 또한 의사 클래스 패턴의 단점인 new 연산자가 필요없으며, 생성자 링크도 파괴되지 않으며, 객체리터럴에도 사용할 수 있다.

생성자 함수를 사용한 프로토타입 패턴 상속은 아래와 같다.

function Headphone() {
  this.volume = 0;
  
  Headphone.prototype.volumeUp = function () {
    if (this.volume < 10) {
      this.volume++;
    }
  }
  
  Headphone.prototype.volumeDown = function () {
    if (this.volume > 0) {
      this.volume--;
    }
  }
}

const headphoneA = new Headphone();
const headphoneB = new Headphone();

생성자 함수에 재사용할 메소드를 prototype으로 만든 후 new 키워드를 사용하여 원하는 만큼 instance를 생성하면 된다.

생성된 instanceprototype chain을 통해 생성자 함수의 메소드를 중복된 코드 없이 사용할 수 있게 된다.

하지만 이렇게 되면 객체 리터럴 방식과 마찬가지로

headphone.volume = 1000;

위와 같은 코드로 외부에서 내부 값을 변경할 수 있게 되는데 객체 리터럴과 다른 점은 생성자 함수 사용의 경우 대량 생산이 가능하다는 점이다.

만약 사용자에게 충분히 조작 방식에 대해 설명할 수 있거나 외부에서 내부 값 변경을 해야하는 특별한 상황에서 사용할 수 있다.

객체리터럴 패턴으로 생성한 객체에도 프로토타입 패턴 상속을 사용할 수 있다.

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

// create 함수의 인자는 객체이다.
var child = Object.create(parent);
child.name = 'child';

// var child = Object.create(parent, {name: {value: 'child'}});

parent.sayHi(); // Hi! parent
child.sayHi();  // Hi! child

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

Object.create 함수는 매개변수에 프로토타입으로 설정할 객체 또는 인스턴스를 전달하고 이를 상속하는 새로운 객체를 생성한다. Object.create 함수는 표준에 비교적 늦게 추가되어 IE9 이상에서 정상적으로 동작한다. 따라서 크로스 브라우징에 주의하여야 한다. Object.create 함수의 폴리필(Polyfill: 특정 기능이 지원되지 않는 브라우저를 위해 사용할 수 있는 코드 조각이나 플러그인)을 살펴보면 상속의 핵심을 이해할 수 있다.

// Object.create 함수의 폴리필
if (!Object.create) {
  Object.create = function (o) {
    function F() {}  // 1
    F.prototype = o; // 2
    return new F();  // 3
  };
}

위 폴리필은 프로토타입 패턴 상속의 핵심을 담고 있다.

  1. 비어있는 생성자 함수 F를 생성한다.
  2. 생성자 함수 F의 prototype 프로퍼티에 매개변수로 전달받은 객체를 할당한다.
  3. 생성자 함수 F를 생성자로 하여 새로운 객채를 생성하고 반환한다.

캡슐화(Encapsulation)와 모듈 패턴(Module Pattern)

캡슐화는 관련있는 멤버 변수와 메소드를 클래스와 같은 하나의 틀 안에 담고 외부에 공개될 필요가 없는 정보는 숨기는 것을 말하며 다른 말로 정보 은닉(information hiding)이라고 한다.

클래스를 정의하고 그 클래스를 구성하는 멤버에 대하여 public 또는 private 등으로 한정할 수 있다. public으로 선언된 메소드 또는 데이터는 외부에서 사용이 가능하며, private으로 선언된 경우는 외부에서 참조할 수 없고 내부에서만 사용된다.

이것은 클래스 외부에는 제한된 접근 권한을 제공하며 원하지 않는 외부의 접근에 대해 내부를 보호하는 작용을 한다. 이렇게 함으로써 이들 부분이 프로그램의 다른 부분들에 영향을 미치지 않고 변경될 수 있다.

Factory 함수 사용하여 캡슐화를 하면서 대량 생산까지

  • Factory 함수란 생성자 함수가 아니지만 객체를 반환하는 함수 일컫는다.
function factoryHeadphone () {
  let volume = 0;
  
  return {
    volumeUp: function () {
      if (volume < 10) {
        volume++;
      }
    },
    volumeDown: function () {
      if (volume > 0) {
        volume--;
      }
    }
  };
}

const headphoneA = factoryHeadphone();
const headphoneB = factoryHeadphone();

함수가 정의되었다는 것을 제외하면 IIFE를 사용하여 scope를 사용한 것과 거의 동일한 방식으로 볼 수 있다.

headphoneAheadphoneB에 각각 factoryHeadphone 함수가 반환하는 객체를 할당하는데 그 객체의 속성 값은 volumeUpvolumeDown뿐이다. 이 메소드를 사용하여 상위 스코프안에 숨겨져 있는 volume 변수를 내부에서만 조절할 수 있게 된다.

자바스크립트는 function-level scope를 제공하므로 함수 내의 변수는 외부에서 참조할 수 없다. 만약에 var 때신 this를 사용하면 public 멤버가 된다. 단 new 키워드로 객체를 생성하지 않으면 this는 생성된 객체에 바인딩되지 않고 전역객체에 연결된다.

factoryHeadphone 함수는 객체를 반환한다. 이 객체 내의 메소드 volumeUp, volumeDown은 클로저로서 private 변수 volume에 접근할 수 있다. 이러한 방식을 모듈 패턴이라 하며 캡슐화와 정보 은닉를 제공한다. 많은 라이브러리에서 사용되는 유용한 패턴이다.

Object.create()

Object.create는 괄호 속 객체를 prototype으로 하는 객체를 생성한다.

따라서 Object.create(obj.prototype)obj.protytpeprotytpe으로 하는 객체를 생성하는 것이다.

이때 주의 할 것은 객체 생성으로 인해 끊어진 constructor를 재연결해주어야 상위 메소드와 본인이 가지고 있는 메소드를 모두 사용할 수 있다.

Object.create()를 사용한 고전적인 상속방법

Audio 생성자 함수

function Audio() {
  this.volume = 0;
  this.power = false;
}

Audio.prototype.volumeUp = function() {
  if (this.volume < 10) {
    this.volume++;
  }
}

Audio.prototype.volumeDown = function() {
  if (this.volume > 0) {
    this.volume--;
  }
}

Audio.prototype.powerOnAndOff = function() {
  this.power = !this.power;
}

Audio__proto__로 가지는 Headphone생성자 함수

Audio와 Headphone의 속성들을 모두 사용할 수 있다.

function Headphone () {
  Audio.call(this);
  
  this.noiseCancelling = false;
}

Headphone.prototype = Object.create(Audio.prototype); // prototype 연결
Headphone.prototype.constructor = Headphone; // constructor 재연결

Headphone.prototype.setNoiseCancelling = function () {
  this.noiseCancelling = !this.noiseCancelling;
}

const headphoneA = new Headphone();
const headphoneB = new Headphone();

Audio__proto__로 가지는 AirPod생성자 함수

AudioAirPod의 속성들을 모두 사용할 수 있다.

function AirPod () {
  Audio.call(this);
  
  this.pairingConnected = false;
}

AirPod.prototype = Object.create(Audio.prototype); // prototype 연결
AirPod.prototype.constructor = AirPod; // constructor 재연결

AirPod.prototype.pairingConnect = function () {
  this.pairingConnected = !this.pairingConnected;
}

const airpodA = new AirPod();
const airpodB = new AirPod();

재연결 안할 시..

AirPod.prototype.constructor

위의 코드를 콘솔 창에 찍어보면 Object.create 이후에 값이 달라지는 것을 확인할 수 있다. 따라서 상/하위 생성자 함수의 속성들을 모두 사용하려면 반드시 끊어진 체인을 다시 연결해주어야 한다.

장점: 중복 코드를 줄이며 프로토타입 체인으로 메소드를 상속(위임) 가능하다.
단점: 길어지는 코드와 중요해지는 코드의 위치. 하위 생성자의 경우 메소드 정의 후 상위 생성자와 체인 연결하면 메소드들이 사라진다.

ES6 class

ES6부터 상속의 개념으로 class가 등장했는데 이름은 class이지만 작성법이 달라졌을 뿐 prototype과 크게 다르지 않다

클래스 선언 후 메소드를 작성해주면 된다.

class Audio {
  constructor() {
    this.volume = 0;
    this.power = false;
  }
  
  volumeUp = function () {
    if (this.volume < 10) {
      this.volume++;
    }
  }
    
  volumeDown = function () {
    if (this.volume > 0) {
      this.volume--;
    }
  }
}

프로토타입 연결을 각기 따로 해줄 필요 없다.

class Headphone extends Audio {
  constructor(props) {
    super(props);
    
    this.noiseCancelling = false;
  }
  
  setNoiseCancelling = function () {
    this.noiseCancelling = !this.noiseCancelling;
  }
}

class AirPod extends Audio {
  constructor(props) {
    super(props);
    
    this.pairingConnected = false;
  }
  
  pairingConnect = function () {
    this.pairingConnected = !this.pairingConnected;
  }
}

이렇게 상속(위임)이 가능하다.
위의 코드들과 비교하면 언뜻봐도 코드가 매우 간결해진 것을 알 수 있다.

Abstraction (추상화)

불필요한(복잡한) 정보는 숨기고 중요한 정보만을 표현함으로써 프로그램을 간단히 만드는 기법이다. 예를 들어서 TV를 객체로 나타내는 작업을 생각해보면 TV에는 지금까지 우리가 축적한 엄청난 기술들이 들어 있을 것이다. 따라서 TV를 객체로 나타내려면 객체가 엄청나게 커져야 할 것이다. 하지만 이래서는 프로그램하기 힘들어진다. 우리가 필요한 몇 개만을 남기고 불필요한 것들은 삭제하거나 숨겨야할 필요성이 있다.

전원 버튼을 누르면 TV가 켜지고 채널 버튼을 누르면 채널이 변경되는 기능만 있다고 가정할 수 있다. 이것이 바로 추상화이다.

추상화는 복잡성을 관리하는데 사용된다. -> 추상화를 사용하지 않는다면 객체들이 너무 복잡해질 수 있다.

Polymorphism (다형성)

다형성이란 객체가 취하는 동작이 상황에 따라서 달라지는 것을 의미한다.

Polymorphism 은 "많은(poly) + 모양(morph)" 이라는 의미이다.

다형성을 사용하게 되면 실제 동작은 다르더라도 개념적으로는 동일한 작업을 하는 메소드들에 똑같은 이름을 부여할 수 있으므로 코드가 더 간단해질 수 있다.

예를 들어서 speak() 라는 메소드는 모든 객체 타입마다 정의되어 있어서 호출하면 객체가 소리를 발생한다.

speak() 를 호출 받은 객체는 자신의 상황에 따라서 서로 다른 소리를 내게 된다.

이처럼 객체들의 타입(클래스)이 다르면 똑같은 메시지가 전달되더라도 서로 다른 동작을 하는 것을 말한다.

즉 똑같은 명령을 내리지만 객체의 타입이 다르면 서로 다른 결과를 얻을 수 있는 것이 "다형성"이다. 여기서 중요한 점은 메시지를 보내는 측에서는 객체가 어떤 타입인지 알 필요가 없다는 점이다. 실행 시간에 객체의 타입에 따라서 자동적으로 적합한 동작이 결정된다.

한 곳에 모인 동물들이 각자의 소리를 내게 하고 싶으면 어떤 동물인지 신경 쓰지 말고 무조건 speak 메시지를 보내면 된다. 이 메시지를 수신한 동물은 자신이 낼 수 있는 소리를 낼 것이다.

class의 장/단점

자바스크립트 클래스 작성의 장점들은

  1. 프로토타입로 작성시 각각의 메소드를 따로 작성해주어야 하는데 클래스는 해당 클래스 내에 여러 개 작성 가능

  2. 프로토타입은 각 메소드가 개별적으로 작성되기 때문에 특정 생성자의 프로토타입들을 전체적으로 볼 때 가독성이 떨어지는데 클래스는 하나의 클래스내에 묶여 있기 때문에 가독성이 높고 보기 편함

  3. 코드가 간결해서 프로토타입보다 작성하기 쉬움.

위의 방법들을 통해 즉, 객체 지향 프로그래밍의 특징들을 적용해 데이터 구조를 설계하게 되면 OOP의 궁극적인 목적인 객체 내부의 응집력을 높이고 외부 객체들과의 결합력을 낮추는 방향을 지향하게 된다.

다음의 글을 참고하였습니다.

profile
yeeaasss rules!!!!

0개의 댓글