[JavaScript] OOP(Object Oriented Programming)

Hailey Song·2020년 9월 9일
0

DEVLOG_JavaScript

목록 보기
6/6

Intro.


(마우스 우클릭 -> '새 탭에서 이미지 열기'로 보면 보기 편하다.)

OOP(Object Oriented Programming)란?

1) 절차지향형 프로그래밍 vs 객체지향형 프로그래밍


절차지향형 프로그래밍(Procedural Programming)은 말 그대로 절차의 순차적인 처리가 중요시되는 프로그래밍 기법이다. 프로그램의 순서와 흐름을 먼저 세우고 필요한 자료구조와 함수들을 설계하는 방식으로 이루어진다.

반면 객체지향 프로그래밍은 실제 세계를 모델링하여 물체(객체)들의 상호작용을 표현하는 프로그램을 짜는 방식이다. 절차자향형 프로그래밍의 분산적이고 통일성 없는 추상화 과정을 통합하여, 자료구조와 이를 중심으로 한 모듈들을 먼저 설계한 다음에 이들의 실행순서와 흐름을 짜는 방식으로 진행된다.

참고: 객체지향vs절차지향??

2) class, object, instance

  • object
    object는 실제 세상에 존재하는 물체로, 소프트웨어 세계에 구현할 대상을 가리킨다.
    OOP에서는 class로 구현된 모든 대상을 뜻하기도 한다.

  • class
    class는 object를 소프트웨어 세계에 구현하기 위한 템플릿, 설계도, 혹은 틀이다.
    object가 가진 속성과 메소드를 파악하고 이를 코드로 추상화하는 과정을 거친다.

  • instance
    설계도(class)를 바탕으로 소프트웨어 세계에 구현된 구체적인 실체를 뜻한다.
    이렇게 실체화된 instance는 메모리에 할당되며, class에서 구현한 속성과 메소드를 가진다.

참고 : Q. Class, Object, Instance의 차이점을 설명해주세요.

OOP의 4가지 특징

1) 캡슐화(Encapsulation)

관련이 있는 속성과 메소드를 하나의 클래스로 모은 형태.
구현 단계에서 캡슐 내부의 로직이나 변수를 감추고 외부에는 기능만 제공하는 '정보 은닉'이 가능하다.

2) 추상화(Abstraction)

추상화는 유저 인터페이스의 디자인 수준에서 어떤 특정 정보를 볼 수 있는지, 어떤 정보를 숨길지를 식별하는 기술. "서비스를 이용하기 위해 무엇을 해야 하는가"와 관련.

계산기를 예로 들자면,
캡슐화는 내부회로, 배터리 등을 결합하여 계산기로 만드는 것,
추상화는 켜짐, 꺼짐, 숫자 버튼 등 내부의 복잡한 단계를 숨기고 단순한 인터페이스로 작동하게 하는 것.

참고 : 캡슐화와 추상화의 차이점

3) 상속(Inheritance)

상위 집합체의 기능과 특성을 그대로 물려받아 사용하기 때문에 기존의 코드를 재활용할 수 있다는 장점이 있다.

고양잇과 클래스에 enterBox 메소드가 존재한다면
고양잇과 클래스로 선언된 인스턴스들인 고양이, 호랑이, 사자는 모두 enterBox 메소드를 물려받기 때문에, 해당 메소드를 인스턴스마다 따로 만들어서 사용할 필요가 없다.

4) 다형성(Polymorphisim)

하나의 방법으로 다양한 상황에 대처하는 기법.
상위클래스에서 구현된 메소드를 하위클래스에서 동일한 이름으로 자신의 특징에 맞게 다시 구현(overwriting)

고양잇과 클래스의 cry 메소드가 '야옹'을 리턴하는 기능을 한다면,
인스턴스로 생성된 호랑이는 cry라는 메소드를 그대로 가져오되 '어흥'으로 바꿔서 사용이 가능하다.
굳이 catCry, tigerCry로 나눌 필요가 없어진다.

JavaScript에서 Class를 생성하는 여러가지 방법들

1) Functional

함수를 선언하고 그 안에 리턴할 객체를 생성하는 방식.
속성과 메소드는 각각 객체의 key와 value로 들어간다.

// object를 생성하는 함수 선언
let Cat = function(color) { 
  let someInstance = {}; // 리턴할 결과물인 객체 선언
  someInstance.color = color; // property
  someInstance.cry = function() { // method
    return '야옹';
  }
  return someInstance;
}

let yatong = Cat('tricolor');

2) Functional Shared

모든 인스턴스에 모든 메소드를 할당하는 functional한 방식과 달리,
functional shared 방식은 someMethods에 담긴 메소드의 주소를 참조하기 때문에 메모리 효율이 좋아진다.

// someInstance와 someMethod를 합치는 함수
let extend = function (to, from) { 
  for (let key in from) {
    to[key] = from[key]
  }
}

// Method를 담아줄 객체 선언
let someMethods = {}; 
someMethods.cry = function() {
  return '야옹';
}

// object를 생성하는 함수 선언
let Cat = function(color) { 
  let someInstance = {}; // 리턴할 결과물인 객체 선언
  someInstance.color = color; // property

  // someInstance와 someMethod를 합침
  extend(someInstance, someMethods); 

  return someInstance;
}

let yatong = Cat('tricolor');

3) Prototypal Instantiation

extend 함수로 someInstance와 someMethods를 합치지 않고
Object.create() 함수로 someMethods를 프로토타입으로 하는 객체를 생성하는 방식.

// Method를 담아줄 객체 선언
let someMethods = {}; 
someMethods.cry = function() {
  return '야옹';
}

// object를 생성하는 함수 선언
let Cat = function(color) { 
  // Object.create() : Object를 특정 객체를 프로토타입으로 하는 객체를 생성해주는 함수
  let someInstance = Object.create(someMethods); 
  someInstance.color = color; // property

  return someInstance;
}

let yatong = Cat('tricolor');

4) Pseudoclassical

let Cat = function(color) { 
  this.color = color; // property
}

// Method 선언
Cat.prototype.cry = function () {
  return '야옹';
}

// new 키워드로 인스턴스 선언
let yaTong = new Cat('tricolor')

5) class (ES6)

class 키워드로 생성하는 방식

class Cat {
  constructor(color) {
    this.color = color; // property
  }
  // Method 선언
  cry() { 
    return '야옹';
  }
}
// new 키워드로 인스턴스 선언
let yaTong = new Cat('tricolor')

상속(Inheritance Patterns)

상속이란 상위 집합체의 기능과 특성을 그대로 물려받아 사용하는 것을 말한다. 속성을 물려주는 클래스를 super class, 물려받는 클래스를 sub class라고 한다.

ES6에서 class 키워드가 추가됨에 따라 상속의 방법도 효율적으로 변화했는데, 다음은 ES6 도입 전의 상속과 도입 후의 상속 변화를 비교한 것이다. 같은 색끼리 같은 역할을 한다.

새로운 키워드인 counstructor, call, extend, super, Object.creat()들이 보이는데, 하나하나 설명해보도록 하겠다.

일단 super class를 Pseudoclassical한 방법과 ES6의 방법으로 선언해보자면 다음과 같다.

super class : Pseudoclassical

// constructor : 특정 객체가 형성될 때 실행되는 코드(함수)
let Cat = function(color) { 
  this.color = 'yellow';
  // 뒤의 상속 예시를 위해 Cat의 color를 'yellow'로 설정해주었다.
}

// 모든 function에는 prototype 속성이 있으며 assignable하다.
// prototype에 property나 method를 정의할 수 있다
Cat.prototype.cry = function () {
  return '야옹';
}

// 'new' 키워드를 통해 인스턴트를 생성하는 과정 : instantation 
let yaTong = new Cat('tricolor')
yatong instanceof Cat === true // true

constructor란 특정 객체가 형성될 때 실행되는 코드(함수)를 가리키는데, Pseudoclassical한 방법에서는 클래스를 함수로 선언해주기 때문에 constructor라는 키워드를 사용하지 않는 것으로 보인다. (내 추측이다.)
콘솔창에 인스턴스인 yatong이의 constructor를 찾아보면 다음과 같이 나온다.

이처럼 constructor을 사용하면 야통이를 만든 Cat이라는 클래스의 함수를 볼 수 있다.

그렇다면 ES6의 방식은 어떨까?

super class : class (ES6)

class Cat {
  constructor(color) {
     // property 
    this.color = 'yellow';
    // 뒤의 상속 예시를 위해 Cat의 color를 'yellow'로 설정해주었다.
  }
  // Method 선언
  cry() { 
    return '야옹';
  }
}
// new 키워드로 인스턴스 선언
let yaTong = new Cat('tricolor')

class 키워드 자체가 함수가 아니기 때문에 constructor라는 키워드를 추가하여 함수라는 것을 명시적으로 표현한 것으로 보인다. (이것 또한 나의 추측이다!) 아무튼 ES6에 와서는 class라는 키워드와 함께 constructor라는 키워드를 사용함으로써 각 코드의 의미가 명시적으로 파악되는 것 같다.

ES6에서 달라진 또다른 점은 메소드를 class 안에서 선언한다는 점인데, 이는 캡슐화 과정의 완성형으로 보인다.

sub class : Pseudoclassical

1) this

그럼 이제 sub class를 선언해보자. 고양잇과의 특성을 물려받은 tiger라는 sub class를 선언해보도록 하겠다. Pseudoclassical한 방식으로!

let Tiger = function (color) {
  Cat.call(this, color);
}

Tiger.prototype = Object.create(Cat);
Tiger.prototype.constructor = Tiger;

let tiger = new Tiger('orange');

callprototypeconstructor이라는 키워드들이 나오면서 복잡해졌다. 흑흑..
일단 Tiger의 property는 call이라는 키워드를 통해 가져왔다. call은 this를 명시해주는 메소드이다. 만약 call을 쓰지 않고 그대로 super class를 상속한다면 sub class의 this는 sub class가 아니라 super class를 그대로 사용하게 된다. 예시를 위해 call을 사용하지 않은 Lion 클래스를 하나 더 생성해서 Cat을 상속받아보자.

let Lion = function(color) {
    Cat(color);
}

let lion = new Cat('brown');
console.log(lion.color) // "yellow"

인스턴스 lion의 color 속성을 'brown'으로 명시해주었음에도 불구하고 lion.color를 알아보면 Cat의 color 속성값인 'yellow'가 나오게 된다. 왜냐하면 여기서의 this는 Cat을 그대로 가져오기 때문이다!

그래서 sub class가 super class의 속성을 그대로 가져오면서도 this는 자신인 채로 설정하려면 this를 명시해주는 call 혹은 apply 메소드가 필요한 것이다.

2) Object.create();

이제 call로 super class의 속성들을 추가해주었다. 메소드들은 어떻게 추가해주어야 할까?
처음 생각은 이렇다. 그냥 Tiger.prototype에 Cat.prototype을 그대로 할당해주면 되는 거 아닌가?

Tiger.prototype = Cat.prototype

이러면 작동은 잘 된다. 문제는 상속의 다형성이다. sub class인 tiger에만 roar()메소드를 추가하고 싶다면

Tiger.prototype.roar = function() {
  return '으르렁!'
}
let tiger = new Tiger();
console.log(tiger.roar()); // "으르렁!"

이런 식으로 추가하게 되는데, 여기서 의도치 않은 결과가 나타난다. 바로..

let cat = new Cat();
console.log(cat.roar()); // "으르렁!"

super class인 Cat에도 roar() 메소드가 붙는 것이다! Tiger.prototype과 Cat.prototype은 모두 객체인데, Tiger.prototype = Cat.prototype로 객체에 객체를 할당함으로써 같은 주소값을 참조하게 되는 것이다.

이를 방지하기 위해 Cat.prototype이라는 객체를 복사하는 방법으로 우회할 수 있다. 즉 같은 메소드를 가진 객체를 복사해서 다른 주소값을 생성하는 것이다. 그리고 그 역할을 하는 것이 바로 Object.create()이다. (배열에 slice()를 붙이는 것과 비슷한 기능을 한다고 생각하면 될 것 같다.)

// Tiger.prototype = Cat.prototype 이 아니라
Tiger.prototype = Object.create(Cat.prototype);

이렇게 해주면 Tiger에만 roar() 메소드를 추가해줄 수 있다.

3) constructor

그러나 또 다른 문제가 생겼다. 앞서 constructor란 특정 객체가 형성될 때 실행되는 코드(함수)라고 말한다고 했는데, 클래스 Tiger로 만들어진 인스턴스 tiger의 constructor은 Tiger여야 한다. 그러나 위의 Tiger.prototype = Object.create(Cat.prototype)를 통해 Cat.prototype를 복사 할당해주면서 Cat.prototype.constructor까지 복사해온 것이다. 이렇게 인스턴스 tiger과 클래스 Tiger의 연결 고리가 끊어지게 되었다. 다시 Tiger에게 자신의 constructor를 돌려주기 위해 다음과 같은 코드를 추가한다.

Tiger.prototype.constructor = Tiger;

길었지만 정리해보면.. (정리하기 싫어서 이미지로 때우는 건 아니다.)

위와 같은 과정을 거친다. 옆동네 ES6와 비교했을 때 복잡한 느낌이 든다. 하지만 이러한 원리를 토대로 ES6를 만들었다는 것을 아는 것이 중요하다고.. (누군가가 그랬다.)

sub class : class (ES6)

1) extends

Pseudoclassical에서 call이나 apply 메소드를 사용해서 어떤 super class를 상속하는지 명시할 수 있었다. class는 좀더 직관적인 방법을 쓰는데, 바로 class를 선언할 때 extends 키워드를 사용하는 것이다.

class Tiger extends Cat {
  constructor (color) {
    super(color)
  }
}

let tiger = new Tiger('orange');

이렇게 해주면 Tiger는 Cat으로부터 상속받는다는 것을 알 수 있다.

2) super

Pseudoclassical에서 call이나 apply 메소드를 사용해서 this를 명시하는 방법을 썼다면, ES6에서는 super라는 키워드 하나로 모든 것을 처리해준다. super easy!

// prototype과 constructor 연결을 extend로 표현 가능
class Tiger extends Cat {
  constructor (color) {
    // this 전달을 super로 전달
    super(color)
  }
}

let tiger = new Tiger('orange');

만약 여기서 super라는 키워드를 쓰지 않으면 당신은 아래와 같은 에러 메세지를 받게 된다...

Uncaught ReferenceError: Must call super constructor in derived class
before accessing 'this' or returning from derived constructor

이제 마지막으로 복습을 하자면.. (다시 말하지만 정리하기 싫어서 이미지로 때우는 건 아니다!)

References

0개의 댓글