객체지향 프로그래밍 ( Object-Oriented Programming, OOP )이란 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 “객체(Object)"들의 모임으로 파악하고자 하는 프로그래밍 패러다임을 말한다.
--> 흔히, 얽히고설켜 유지보수가 매우 힘든 코드들을 형상을 따서 스파게티 코드라고 하는데, 이를 피하기 위해 사용된다.
위의 장점들을 관통하는 객체 지향 프로그래밍의 중요한 특성은 강한 응집력(Strong Cohesion)과 약한 결합력(Weak Coupling)을 지향한다는 점이다.
소프트웨어 공학에서 말하는,
응집력(cohesion) : 프로그램의 한 요소가 해당 기능을 수행하기 위해 얼마만큼의 연관된 책임과 아이디어가 뭉쳐있는지를 나타내는 정도. 프로그램의 한 요소가 특정 목적을 위해 밀접하게 연관된 기능들이 모여서 구현되어 있고, 지나치게 많은 일을 하지 않으면 그것을 응집력이 높다고 표현한다.
결합력(coupling) : 프로그램 코드의 한 요소가 다른 것과 얼마나 강력하게 연결되어 있는지, 얼마나 의존적인지를 나타내는 정도. 결합력이 낮다는 것은 한 요소가 다른 요소들과 관계를 크게 맺고 있지 않은 상태를 의미한다.
OOP의 경우 클래스에 하나의 문제 해결을 위해 데이터를 모아 놓은 객체를 활용한 프로그래밍을 지향하므로 응집력을 강화하며, 클래스 간에 독립적으로 디자인함으로써 결합력을 약하게 할 수 있다.
객체 즉, 인스턴스를 찍어내는 공장으로 생각할 수 있다.
Class ( 공장 ) 에서 찍어내는 결과물이고 상위 class의 속성도 가지고 있고 각각 개별적인 특성과 메소드를 가지고 있다.
인스턴스의 속성 및 어떠한 기능을 수행하는 역할을 한다.
class Tteokbokki {
constructor(sort, price){
this.sort = sort;
this.price = price;
}
totalPrice () {
console.log(this.price + '원');
}
}
const red = new Tteokbokki ('red','3000');
const rose = new Tteokbokki ('rose','4000');
const cream = new Tteokbokki ('cream','5000');
red.totalPrice(); // 3000원
rose.totalPrice(); // 4000원
cream.totalPrice(); // 5000원
OOP에서는 4가지의 기초적인 원칙이 존재한다.
캡슐화는 정보의 은닉을 위해 내부를 캡슐처럼 감싸서 숨기는것을 말하며,
일반적으로 연관 있는 변수와 함수를 클래스로 묶는 작업을 말한다.
따라서 외부 코드로부터 내부의 속성이나 상태가 제어되지 않도록 막는 목적을 갖는다.
( 내부 데이터를 외부에서 직접 접근하지 못하게 은닉화를 시켜준다. )
--> 정보은닉
보안적인 측면과 코드의 안정성 측면에서도 필요한 기법이라 할 수 있다.
상속이란 상위 개념의 속성을 하위 개념이 물려받는 것이다.
자세히 말하자면, 자식 클래스(상속받는 클래스)에서 부모 클래스(상속해주는 클래스)의 속성이나 메소드들에 접근할 수 있도록 만들어주는 기법을 말한다.
클래스 간의 계층관계구조를 형성함으로써 공통되는 로직의 재사용성을 높이는 기법이라 할 수 있다.
추상화란 공통의 속성이나 기능을 묶어 이름을 붙이는 것이다.
따라서 객체들이 가진 공통의 특성들을 파악하고 불필요한 특성들을 제거하는 과정을 말한다.
가령 JS에서 EventListener를 사용한다고 할 때, 내부적으로 작동방식은 모르지만 어떻게 작동하는지만 알면 그것을 사용할 수 있다. 이처럼 추상화는 필요한 기능만 핵심적으로 제시해주는 기법이며 OOP뿐만 아니라 프로그래밍 전반적으로 중요하게 사용된다.
자식 클래스에서 부모에게서 상속받은 메소드들을 덮어씌워서 좀 더 복잡하고, 기능이 많은 메소드들로 사용하는 것을 말한다.
즉 같은 이름을 가진 메소드라 할지라도 부모클래스의 메소드와 자식 클래스의 메소드는 기능이 다르다.
( 부모 클래스에서 정의된 메소드의 작업이 자식 클래스에서 다른 것으로 override(대체)될 수 있다. )
C++과 Java는 클래스를 기반으로 객체 지향 프로그래밍을 하는데 그로 인해 OOP의 개념에 대해 잘못 이해하게 되었다.
OOP는 클래스를 사용하는 것이다
이러한 관점에서 자바스크립트를 학습하다보니 문제가 발생했다.
자바스크립트는 클래스라는 개념이 없다
클래스가 없는 자바스크립트로 어떻게 객체 지향 프로그래밍을 할 수 있을까?
결론은 자바스크립트는 클래스를 사용하지 않고 prototype을 통해 객체 생성 및 상속을 구현한 것이다.
참고로 ES6표준에서 Class라는 문법이 생겼지만 이것은 함수로 prototype을 더 쉽게 사용하기 위한 문법일뿐 자바스크립트가 클래스 기반으로 바뀐것이 아니다.
( 자바스크립트는 여전히 prototype 기반 언어이다. )
이게 무슨 말이냐
앞서 배웠던 생성자함수에서는 동일한 생성자 함수에 의해 생성된 모든 인스턴스가 동일한 메서드를 중복 소유하여 메모리를 불필요하게 낭비했다.
function Score(listen, read){
this.listen = listen;
this.read = read;
this.total = function () {
return this.listen + this.read;
}
}
const 소현 = new Score(400,500);
console.log(소현.total());
이를 해결하기 위해 prototype 이라는 공간에 하나의 메소드를 저장해 사용하기때문에 메모리 공간을 효율적으로 사용할 수 있다.
자신의 상태는 나타내는 listen , read 프로퍼티는 개별적으로 소유하고 내용이 동일한 메서드는 상속을 통해 공유하여 사용하는것이다.
function Score(listen, read) {
this.listen = listen;
this.read = read;
}
Score.prototype.total = function () {
return this.listen + this.read;
}
const 소현 = new Score(400,500);
console.log(소현.total());
이 때 알아야 할 것이
prototype vs __proto__
이다.
간단하게 말하면 관점의 차이로 명칭이 다른 것인데,
prototype
: 상속을 시켜줄 무언가 ( 부모 )
__proto__
: 상속을 받는 무언가 ( 자식 )
( 더 쉽게 생각해서, prototype
은 붕어빵 틀 자체에 기능을 추가하는 것. __proto__
는 붕어빵에, 그 추가했던 기능 )
모든 객체는 prototype이라는 내부 슬롯을 갖는다.
그리고 모든 프로토타입은 생성자 함수와 연결되어 있다.
즉, ( new 함수로 생성한 ) 객체와 프로토타입과 생성자 함수는 다음 그림과 같이 서로 연결되어있다.
따라서 저 코드에서 Score의 prototype
과 소현의 __proto__
는 동일하게 Score의 prototype
을 가리키므로 true이다.
console.log(Score.prototype === 소현.__proto__); // true
상속은 코드의 재사용이란 관점에서 매우 중요하다.
생성자 함수가 생성할 모든 인스턴스가 공통적으로 사용할 프로퍼티나 메서드를 프로토타입에 미리 구현해두면,
생성자 함수가 생성할 모든 인스턴스는 별도의 구현없이 상위(부모)객체인 프로토타입의 자산을 공유하여 사용할 수 있다.
리터럴 표기 : 자바스크립트에서 객체를 생성하는 가장 쉬운 방법
생성자 함수 : 함수로부터 객체를 생성하는 기법, 일반적으로 우리가 프로토타입 함수를 사용할 때 사용 할 방법
리터럴 표기에 의한 객체생성으로 객체 이니셜라이저( initializer ) 라고도 불린다.
{}
를 사용하여 묶고, 각 프로퍼티(property)에 값(value)를 매칭시키면 끝난다.
var obj = {
property_1 : value_1;
property_2 : value_2;
// ... ,
property_n : value_n;
}
앞서 배웠듯이 new 연산자로 인스턴스를 생성하고 this로 해당 인스턴스를 바인딩한다.
( 앞에 썼던 코드와 동일 )
function Score(listen, read){
this.listen = listen;
this.read = read;
this.total = function () {
return this.listen + this.read;
}
}
const 소현 = new Score(400,500);
console.log(소현.total());
앞서 말했듯이 동일한 생성자 함수에 의해 생성된 모든 인스턴스가 동일한 메서드를 중복 소유하여 메모리를 불필요하게 낭비하므로 prototype을 사용해 불필요한 메모리 사용을 줄인다.
function Score(listen, read) {
this.listen = listen;
this.read = read;
}
Score.prototype.total = function () {
return this.listen + this.read;
}
const 소현 = new Score(400,500);
console.log(소현.total());
console.log(Score.prototype === 소현.__proto__); // true
위에서 살펴 본 생성자 함수를 이용해서 충분히 객체 지향 프로그래밍을 할 수 있다.
하지만 C++ , java와 같은 객체 지향 프로그래밍 언어에서 사용하는 것과 유사하게 class를 사용할 수 있게 문법을 제공하기 위해 ES6 추가된 Syntactic sugar ( 문법적 설탕 ) 이다.
자바스크립트에서 클래스란... 생성자 함수이다!!!
사용 방법만 다를 뿐 생성자 함수와는 큰 차이가 없다.
class
키워드로 선언constructor
( 생성자 )로 class 객체의 초기값을 설정해 줄 수 있다.extends
키워드로 클래스 간 상속 가능super
키워드로 부모클래스의 constructor메서드를 부르거나 부모클래스의 프로퍼티를 참조할 수 있다.
class User {
constructor(name,age,city){
this.name = name;
this.age = age;
this.city = city;
}
}
class Introduce extends User {
constructor(name,age,city,hobby){
super(name,age,city);
this.hobby = hobby;
}
introduce () {
return this.name + '은 ' + this.city + '에 살고 취미는 ' +
this.hobby + '입니다.';
}
}
const 소영 = new Introduce('소영',24,'seoul','축구');
console.log(소영.introduce()); // 소영은 seoul에 살고 취미는 축구입니다.
Object.create() 메서드는 지정된 프로토타입 객체 및 속성(property)을 갖는 새 객체를 만든다.
주로 기존의 객체를 상속해 확장하는데 사용된다.
Object.create(prototype)
: prototype을 넣어 객체를 생성한다.인자로 null을 넣어주면 아무것도 상속받지 않는 객체를 만들 수 있다.
Object.getPrototypeOf(obj)
: 해당 객체의 프로토 타입을 가져올 수 있다.
Object.setPrototypeOf(obj, proto)
: 해당 객체의 프로토 타입을 설정할 수 있다.
/* 마이쮸에서 어느 맛이 가장 잘팔릴까? 1위면 '1', 2위면 '2'를 썼을 때, 해당하는 맛이 나오게끔 출력하자 */
function MyChew(flavor) {
this.flavor = flavor;
this.sugar = 'sugar';
}
const flavor = [
{taste : 'strawberry' , rank : 1},
{taste : 'orange' , rank : 2},
{taste : 'lemon' , rank : 3}
];
MyChew.prototype.ranking = function(num){
for(var i = 0; i < this.flavor.length; i++ ){
if(this.flavor[i].rank === num){
console.log(num + '위 상품은 ' + this.flavor[i].taste + '맛 입니다.');
}
}
}
const choice = new MyChew(flavor);
choice.ranking(1); // 1위 상품은 strawberry맛 입니다.
// MyChew를 벤치마킹하여 순위에 맞게 YouChew를 만드려고 한다.
function YouChew(flavor) {
this.flavor = flavor;
}
YouChew.prototype = Object.create(MyChew.prototype);
// YouChew.prototype = new MyChew();
const choice2 = new YouChew(flavor);
choice2.ranking(2); // 2위 상품은 orange맛 입니다.
위에서 주석처리 한 부분을 보면,
YouChew.prototype = Object.create(MyChew.prototype);
YouChew.prototype = new MyChew();
이 둘중 어떤걸 써도 .ranking은 형성된다.
하지만 이 둘의 큰 차이점은 Object.create() 는 빈객체를 생성하여 해당하는 프로토타입만 넣어 사용하지만, new 로 상속받을 부모의 객체로 생성해버린다면 부모의 프로토타입뿐만아니라 필요없는 내용도 가져오게된다.
따라서 불필요한 부분도 상속 받기 떄문에 메모리공간에 효육적이지 않다.
/* 1 */
YouChew.prototype = Object.create(MyChew.prototype); // ranking : function(){}
// --> Object.create 는 부모의 프로토타입인 ranking만 가져옴
/* 2 */
YouChew.prototype = new MyChew(); // flavor : undefined , sugar : sugar , ranking : function(){}
// --> new 를 사용해 부모의 객체를 생성한다면 모든 내용을 상속받게 됨