본문 바로가기

개발/Javascript

알아야할 필요가 있는 자바스크립트 객체지향프로그래밍

출처:

javascriptissecy.com 번역


OOP(Object Oriented Programming)는 어플리케이션을 개발하기 위해 독립적인 코드 조각들을 사용하는 것을 말한다. 우리는 이런 독립적인 코드조각을 객체라고 부르는데 대부분의 OOP 프로그래밍 언어에서는 클래스(classes)로, 자바스크립트에서는 함수(functions)로 더 알려져 있다. 객체로 구성된 어플리케이션은 상속, 다형성, 캡슐화와 같은 좋은 기술들을 쓸 수 있도록 해준다.


이번 포스팅에서 상속과 캡슐화만 다루려는 이유는 이 두가지 개념이 자바스크립트의 OOP에 적용가능하기 때문이다. 자바스크립트에서 객체는 기능을 캡슐화 할 수 있고, 다른 객체에서 메서드와 프로퍼티를 상속할 수 있다. 이제부터 코드를 쉽게 재사용하고, 특별한 객체로 기능들을 추상화 할 수 있는 객체 지향적인 방법으로 자바스크립트 객체를 사용하는 방법을 토론할 것이다.


자바스크립트에서 OOP를 구현하기 위해 오직 두가지 기술에만 집중하려고 한다. 사실 자바스크립트에서 OOP를 구현하기 위해 많은 기술이 존재하지만, 그 모든 것을 평가하고 설명하기 보다도 두가지 기술에 초점을 두기로 했다. 특정 기능으로 이루어진 객체를 생성하는 최고의 방법과 코드를 재사용하는 최고의 방법 말이다. 여기서 최고라는 것은 가장 적절하면서도 효율정이고 가장 탄탄하다는 것을 의미한다.  


캡슐화와 상속의 전반적인 설명

객체는 어플리케이션에서 주인공이라고 생각할 수 있다. 당신이 지금 아는 것 처럼, 자바스크립트에서 함수, Strings, Numbers를 포함한 모든 컴포넌트들이 객체이기 때문에 객체는 자바스크립트 소스 안에 어디에나 있다. 우리는 객체 리터럴이나 생성자 함수로 객체를 쉽게 생성할 수 있다.


캡슐화는 객체안에 객체의 기능들을 에워싸서 객체의 내부적인 임무들이 다른 곳으로부터 숨겨지는 것을 의미한다. 그리하여 객체 안에 있는 특정 기능들을 추상화 시키고 지역화 시킬 수가 있다.


상속은 부모 객체로부터 메서드와 프로퍼티를 상속할 수 있게 된 객체(다른 OOP언어에서는 클래스, 자바스크립트에서는 함수라고 함)를 의미한다.


캡슐화, 상속이라는 이 두가지 개념은 코드를 재사용하게 해주고, 기능을 추상화 시키고, 구조를 확장하는데 아주 중요하다. 유지보수에 좋고, 확장가능하고, 효율적이라는 소리다.


인스턴스는 함수의 구현체다. 간단한 용어로 함수나 객체의 복사본이라고 할 수 있다. 예를들어

//Tree를 호출하기 위해 new를 사용했으므로 Tree는 생성자이다.

​function Tree (typeOfTree) {} 

​// bananaTree 는 Tree의 인스턴스이다.

​var bananaTree = new Tree ("banana");


위의 예제에서 bananaTree는 Tree 생성자 함수로 생긴 객체이다. 여기서 bananaTree 객체를 TreeObject의 인스턴스라고 한다. 바스크립트에서 함수는 객체이기 때문에 Tree는 객체이자 함수이다. 아래에서 더 자세히 공부하게 될 내용으로써 bananaTree는 자신의 메서드와 프로퍼티를 갖을 수 있고 Tree 객체에서 메서드와 프로퍼티를 상속받을 수 있다. 


자바스크립트에서의 OOP

자바스크립트 OOP의 두 가지 중요한 개념은 객체 생성 패턴(캡슐화)와 코드 재사용 패턴(상속)이다. 어플리케이션을 만들 때, 많은 객체를 만들게 되는 데 객체를 만드는 방법은 아주 많다. 예를들어 객체 리터럴이 있다.

var myObj = {name: "Richard", profession: "Developer"}; 


객체의 프로토타입에 각 메서드와 프로퍼티를 추가시키는 프로토타입 패턴을 쓸 수도 있다.

function Employee () {}

Employee.prototype.firstName = "Abhijit";

Employee.prototype.lastName = "Patel";

Employee.prototype.startDate = new Date();

Employee.prototype.signedNDA = true;

Employee.prototype.fullName = function () {

console.log (this.firstName + " " + this.lastName); 

};

​var abhijit = new Employee () //​

console.log(abhijit.fullName()); // Abhijit Patel​

console.log(abhijit.signedNDA); // true


생성자 함수(다른 언어에서는 클래스, 자바스크립트에서는 함수라고 함)를 사용하는 생성자 패턴으로 만들 수도 있다.

function Employee (name, profession) {

​this.name = name;

​this.profession = profession;

} // 아래에서 호출하기 위해 new 키워드를 사용하였으므로 Employee () 는 ​생성자 함수이다.

​var richard = new Employee (“Richard”, “Developer”) // richard는 Employee() 생성자 함수로부터 만들어진 새로운 객체이다

console.log(richard.name); //richard​

console.log(richard.profession); // Developer


후자의 예제에서, 객체를 생성하기 위해 생성자 함수를 사용하였다. 객체에 메서드나 프로퍼티를 추가하고 싶을 때나 객체에 특정 기능들을 캡슐화 하기 위해 어떻게 객체를 생성하는지 보여준다. 자바스크립트 개발자는 생성자 함수를 사용하여 객체를 생성하는 많은 방법과 패턴을 개발해왔다. 객체 생성 패턴이라고 할 때, 위의 예제처럼 생성자 함수로 객체를 생성하는 방법을 주로 고려하게 된다.


당신은 객체를 생성하는 패턴뿐만 아니라, 코드를 효율적으로 재사용하기도 원한다. 객체를 생성할 때, 비슷한 기능을 갖은 메서드나 프로퍼티를 부모 객체로부터 상속하기를 원한다. 그러나 그 객체는 자신만의 메서드와 프로퍼티도 가져야 한다. 코드 재사용패턴은 상속을 구현할 수 있는 방법을 가능하게 한다.


객체를 생성하고, 상속하는 이 두가지 일반적인 방법은 이 포스팅에서 주요한 내용이며 자바스크립트의 OOP에서 또한 주요한 개념이다. 첫번째로 객체 생성 패턴에 대해 토론해 보자.


자바스크립트의 캡슐화

(최고의 객체 생성 패턴: Combination Constructor/Prototype Pattern 생성자/프로토타입 패턴 조합)


위에서 토론했듯이, OOP의 주요 원친 중 하나는 객체 안에 내부 임무를 다 집어 넣고자 하는 캡슐화 이다. 자바스크립트에서 캡슐화를 실현하기 위해, 우리는 객체에 코어 메서드와 프로퍼티를 정의해야 한다. 이렇게 하기 위해, 우리는 생성자/프로토타입패턴을 조합한 최고의 캡슐화 방법을 사용할 것이다. 구현에만 신경쓰면 되기 때문에 패턴의 이름에 신경 쓸 필요는 없다. 구현하기 전에 캡슐화에 대해 조금 배워보고 가자


왜 캡슐화인가?

몇 개의 데이터를 저장하기 위한 간단한 객체를 생성할 떄, 객체 리터럴을 사용해서 생성할 수 있다. 이 것이 꽤 일반적이고 자주 쓰이는 심플한 패턴이다.


그러나, 비슷한 기능을 가진 객체를 생성할 때는, 함수안에 주요 기능을 캡슐화 하고 객체를 생성하기 위해 생성자 함수를 사용한다. 이 것이 캡슐화의 핵심이며 왜 생성자/프로토타입 패턴 조합을 사용해야 하는지에 대한 이유이다.


자바스크립트에서 OOP의 실용적인 사용을 위해, 우리는 이 포스팅에서 배울 모든 원칙과 기술을 사용하는 객체 지향적인 퀴즈 어플리케이션을 만들 것이다. 첫 번 째로, 퀴즈 어플리케이션은 퀴즈를 하는 사용자(Users함수)가 있다. 퀴즈를 하는 사람들은 name, score, email, quize score라고 하는 공통 프로퍼티는 User 객체의 프로퍼티가 될 것이다. 각 User 객체는 name, score를 볼 수 있고, score를 저장할 수 있으며, email을 수정할 수 있다. 이 것들은 객체의 메서드가 될 것이다.


우리는 공통 프로퍼티와 메서드를 가진 user 객체를 전부 원하기 때문에, 객체 리터럴로 전부 생성할 수가 없다. 프로퍼티와 메서드를 캡슐화한 생성자 함수를 써야 한다.


모든 사용자는 공통의 프로프티를 가질 것이라는 것을 알기 때문에 이 프로퍼티와 메서드를 캡슐화 하는 함수를 생성할만하다. 여기서 생성자/프로토타입 패턴 조합을 쓸 것이다.


생성자/프로퍼티 패턴 조합 구현

User 함수:

function User (theName, theEmail) {

    this.name = theName;

    this.email = theEmail;

    this.quizScores = [];

    this.currentScore = 0;

}

User.prototype = {

    constructor: User,

    saveScore:function (theScoreToAdd)  {

        this.quizScores.push(theScoreToAdd)

    },

    showNameAndScores:function ()  {

        var scores = this.quizScores.length > 0 ? this.quizScores.join(",") : "No Scores Yet";

        return this.name + " Scores: " + scores;

    },

    changeEmail:function (newEmail)  {

        this.email = newEmail;

        return "New Email Saved: " + this.email;

    }

}


User 함수의 인스턴스 생성

// A User ​

firstUser = new User("Richard", "Richard@examnple.com"); 

firstUser.changeEmail("RichardB@examnple.com");

firstUser.saveScore(15);

firstUser.saveScore(10); 

firstUser.showNameAndScores(); //Richard Scores: 15,10​

​// Another User​

secondUser = new User("Peter", "Peter@examnple.com");

secondUser.saveScore(18);

secondUser.showNameAndScores(); //Peter Scores: 18



생성자/프로토타입 패턴 조합의 설명

한줄한줄 자세하게 설명해서 이 패턴을 정확하게 이해하도록 하자.


아래의 라인은 인스턴스 프로퍼티를 초기화 시킨다. 프로퍼티는 생성된 각 User 인스턴스에서 정의되므로 사용자마다 각각 다른 값을 갖는다. 메서드 안에 있는 this 키워드의 사용은 각 User 객체의 인스턴스가 각각 다른 프로퍼티를 갖게 해준다.

this.name = theName;

​this.email = theEmail;

​this.quizScores = [];

​this.currentScore = 0;


아래의 코드에서 객체 리터럴로 프로토타입 프로퍼티를 덮어썼으며, 이 객체에 모든 메서드를 정의한다(모든 User 인스턴스가 메서드를 상속받게됨).

User.prototype = {

    constructor: User,

    saveScore:function (theScoreToAdd)  {

        this.quizScores.push(theScoreToAdd)

    },

    showNameAndScores:function ()  {

        var scores = this.quizScores.length > 0 ? this.quizScores.join(",") : "No Scores Yet";

        return this.name + " Scores: " + scores;

    },

    changeEmail:function (newEmail)  {

        this.email = newEmail;

        return "New Email Saved: " + this.email;

    }

}


생성자를 덮어쓰는 이 방법은 편리해서 아래처럼 매번 User.prototype을 써줄 필요가 없다.

User.prototype.constructor = User;

User.prototype.saveScore = function (theScoreToAdd)  {

    this.quizScores.push(theScoreToAdd)

};

User.prototype.showNameAndScores = function ()  {

    var scores = this.quizScores.length > 0 ? this.quizScores.join(",") : "No Scores Yet";

    return this.name + " Scores: " + scores;

};

User.prototype.changeEmail =  function (newEmail)  {

    this.email = newEmail;

    return "New Email Saved: " + this.email;

}


객체 리터럴로 prototype을 덮어써서 한번에 메서드를 구조화 시킬 수 있었고, 더 보기가 좋아졌다. 

(객체 리터럴로 덮어 쓸 때는 constructor 프로퍼티가 더이상 prototype을 가르키지 않기 때문에 를 꼭 세팅해줘야 한다.)


prototype method

프로토타입에 메서드를 생성해서 모든 User의 인스턴스들이 메서드에 접근가능하다.

saveScore:function (theScoreToAdd)  {

        this.quizScores.push(theScoreToAdd)

    },

    showNameAndScores:function ()  {

        var scores = this.quizScores.length > 0 ? this.quizScores.join(",") : "No Scores Yet";

        return this.name + " Scores: " + scores;

    },

    changeEmail:function (newEmail)  {

        this.email = newEmail;

        return "New Email Saved: " + this.email;

    }


User객체의 인스턴스를 생성한다.

// A User ​

firstUser = new User("Richard", "Richard@examnple.com"); 

firstUser.changeEmail("RichardB@examnple.com");

firstUser.saveScore(15);

firstUser.saveScore(10); 

firstUser.showNameAndScores(); //Richard Scores: 15,10​

​// Another User​

secondUser = new User("Peter", "Peter@examnple.com");

secondUser.saveScore(18);

secondUser.showNameAndScores(); //Peter Scores: 18


위에서 보듯이, User 함수안에 User를 위한 모든 기능이 캡슐화 되어서, 각 User의 인스턴스가 프로토타입 메서드를 사용할 수 있었고, 각자 자신의 프로퍼티(name, email같은) 를 정의할 수 있었다.


이 패턴에서, 인스턴스에 instansceOf나 for-in loop나 constructor 프로퍼티 같은 표준 연산자나 메서드를 사용할 수 있다.


자바스크립트의 상속

(최고의 패턴: Parasitic Combination Inheritance 기생(?) 조합 상속)


퀴즈 어플리케이션에서 상속의 구현은 부모 함수로부터 기능을 상속 받음으로써 코드를 재사용하기 쉽고 객체의 기능을 쉽게 확장할 수 있도록 하는 것이다. 객체는 상속받은 기능을 사용할 수 있고, 자신의 특화된 기능도 사용할 수 있다.


자바스크립트에서 상속을 구현하는 최고의 패턴은 parasitic 조합 상속이다. 이런 어썸한! 패턴에 빠지기 전에 왜 상속이 유용한 것인지 보자.


각 user가 필요로 하는 모든 메서드와 프로퍼티를 추가하고 기능을 에워싸서 캡슐을 성공적으로 구현했다. 


왜 상속인가?

이제 우리는 모든 질문(Question)을 위한 기능을 캡슐화 하길 원한다. Question 함수는 오든 종류의 질문들이 가져야하는 일반적인 프로퍼티와 메서드를 가질 것이다. 예를들어, 모든 질문은 질문과, 선택지, 정답을 가진다. 이 것들이 프로퍼티가 될 것이다. 또한 각 질문은 getCorrectAnswer, getUserAnswer, displayQuestion를 갖게 된다.


이 퀴즈 어플리케이션은 다른 종류의 질문을 만들고자 한다. MultipleChoiceQuestion function와 DragDropQuestion를 구현해보자. 이 것을 구현한다고 같은 코드를 반복하면서 프로퍼티와 메서드를 중복해서 넣을 수는 없다.


대신에 질문이 공통적으로 가져야할 프로퍼티와 메서드를 Question 객체에 넣고 MultipleChoiceQuestion와 DragDropQuestion를 상속받게 한다. 바로 이 부분, 코드를 재사용할 수 있다는 점에서 상속이 중요한 것이다. 


MultipleChoiceQuestion HTML 레이아웃은 DragDropQuestion HTML와 다르고 displayQuestion 메서드는 각각 다르게 구현될 것이다. 그래서 DragDropQuestion에서 displayQuestion를 오버라이드 하자. 오버라이딩 함수는 OOP의 또 다른 원칙이다.


Parasitic Combination Inheritance Pattern의 구현

이 패턴을 구현하기 위해서, 자바스크립트에서의 상속을 위해 특별히 발명된 두가지 기법을 사용할 것이다. 아래에 좀 설명이 되어있는데, 자세하게 기억할 필요는 없고 이 기법을 이해하는 정도면 충분하다.


Douglas Crockford가 만든 Prototypal Inheritance(프로토타입 상속)

Douglas Crockford가 아래의 Object.create 메서드를 만들었는데, 이 메서드는 상속을 구현하는 기초적인 방법에서 사용된다.


Object.create method

메서드를 자세히 살펴보자.

 if (typeof Object.create !== 'function') {

    Object.create = function (o) {

        function F() {

        }

        F.prototype = o;

        return new F();

    };

}