Blogging - OOP in JavaScript
Bare mininum requirements
다음 키워드에 대해 스스로 검색하고 찾아보고 이해한 내용을 나만의 언어 (복붙 copy & paste 금지) 로 블로그 플랫폼에 TIL(Today I Leared) 형식으로 기록해 주시기 바랍니다.
- OOP(Object Oriented Programming)가 무엇인지?
- JavaScript에서 Prototype은 무엇이고 왜 사용해야 하는지?
이번 시간에는 객체지향 프로그래밍에 관해 정리해보려고 한다.
들어가기에 앞서, JS this (Feat. new 생성자)와 JS 생성자와 new 키워드를 먼저 보고 오는 걸 추천한다.
객체 지향 프로그래밍은 OOP(Object-Oriented Programming)라고 부른다.
프로그래밍을 조금이라도 접해봤다면, Java, C++ 등에서 OOP(Object-Oriented Programming)를 접해봤을 것이다. 자주 나온다는 것은 그만큼 중요하다는 내용이고, 실제로 인터뷰에서도 단골 소재 중에 하나이니, 이번 기회를 통해 제대로 알고 가자!!
일단 OOP에 본격적으로 들어가기 전에, 프로그래밍의 종류에 대해 간략하게 짚고 넘어가보자! 그래야 OOP의 탄생 배경에 대해 알 수 있다.
순서와 절차에 따라 진행하는 프로그램을 가르킨다. 즉, 어디에 무슨 코드가 있는 지가 매우 중요한데, 이렇게 프로그래밍을 하면, 특정한 부분을 수정해줄 때, 모든 부분들을 연쇄적으로 전부 수정해줘야 되는 매우 비효율적인 문제가 생긴다.
바로 이러한 문제를 해결해주기 위해서 등장한 개념이 OOP이다.
OOP는 모든 게 객체로 구성되있다. 그리고 이 객체들을 모아서 이들의 성질과 기능을 모아놓은 새로운 객체를 만들기 위한 공장이 필요하다. 우리는 이 공장을 class라고 부른다.
그리고 이 class라는 공장을 통해서 만들어지는 객체를 인스턴스(instance)라고 부른다.
[ 좀 더 정확하게는 class 안의 생성자를 통해 만들어지는 객체를 의미 ]
모든 class에는 constructor(생성자)라는 객체를 찍어내주는 내부 함수가 존재하는데, 생성자를 만들어주지 않는다고 해도, class에는 default constructor가 자동으로 생성된다.
ex)
class Car라는 공장에서 만들어진 "엔초 페라리" 라는 instance
좀 더 빠른 이해를 위해 ㄱㅊ은 예시를 생각해봤는데, 일단 원피스를 안 봤다면, 넘어가도 좋다.
ex 2)
원피스에서 "악마의 열매 능력자"라는 class를 만든다고 가정해보자! 그 class 안에서는 "악마의 열매"라는 object와 "사람"이라는 object가 필요하다. 이 2개의 object의 성질과 기능을 모아놓고 "악마의 열매 능력자"로부터 새로운 객체 혹은 인스턴스를 뽑아내는 곳이 "악마의 열매 능력자"라는 class 인 것이다.
우리가 "몽키 D 루피" 라는 객체를 만들기 위해서는 "악마의 열매능력자" 라는 class에 일단 집어넣어야한다.
const
몽키 D 루피 =new
악마의 열매능력자("고무고무 열매" , "사람", "해적왕");
몽키 D 루피
=> 새로운 인스턴스 등장!!
이제 OOP가 무엇인지 알아봤으니, OOP의 바탕을 이루는 4가지 개념들에 대해 알아보자!!
프로그래밍에서의 상속은 부모의 특징을 물려받는 것이다. 부모 클래스가 어떠한 속성이나 기능을 가지고 있다면 그 자식의 클래스도 부모의 속성이나 기능을 상속받아 갖게 된다.
현업에서는 개발자 A가 "햄버거"라는 class를 만들었다면, 그것을 상속받아서 개발자 B가 "치즈 버거"라는 class를 또 그것을 개발자 C가 상속받아서 "베이컨 치즈 버거"라는 class를 만드는 것이 가능하다.
(class) 햄버거 => (상속) 치즈 버거 => (상속) 베이컨 치즈 버거
상속의 가장 중요한 점은 "베이컨 치즈 버거"를 만들때, "햄버거"부터 만들지 않아도 된다는 것이다. 즉, class"햄버거"와 class"치즈버거" 들의 특징과 method들을 전부 가져옴으로써, 베이컨 치즈 버거를 더 효율적으로 만들 수 있게 된다.
=> 이 부분은 다음 편에서 좀 더 자세하게 설명하도록 하겠다.
다형성은 상속의 확장 개념이다. 하지만, 부모의 특징을 그래도 가져다 쓰는 것이 아닌, 자식의 상황에 맞게 부모의 특징과 method를 재정의할 수있다.
ex) class "햄버거"를 상속받아 class "샌드위치"를 만들 수있다.
이때, 부모 클래스의 특성인 동그란 빵인 circle이라는 객체와 이것을 위한 method가 있다면, 그것을 네모난 빵인 Square라는 객체와 Square에 맞는 method도 변환이 가능하다.
이때, circle을 위해 존재하는 method를 변경해서 사용하면, function overriding 이라고 부른다.
그리고 이 method를 무시하고, 새로운 method를 만들어서 네모난 빵의 특성에 맞게 사용하는 것을 function overloading이라고 부른다.
=> 딱히, 이런 용어를 알 필요는 없지만, 구글 검색할때 무슨 키워드로 검색해야 되는지 알아야 될 것같아서 언급한다.
다시 말해, 햄버거의 특성을 상속받은 샌드위치는 햄버거의 특성을 변경해서 사용할 수도 있고, 햄버거의 일부를 깡그리 무시하고 샌드위치에 맞는 특성을 새로 만들 수도 있다. 이렇게 다양한 형태로 변환이 가능하다고 해서 "다형성"이라고 부른다.
만약, 다형성이 없었다면, 햄버거를 상속받은 샌드위치는 샌드위치 형태를 만들기 위해, 엄청난 양의 if와 else를 써야 될 것이다. 후덜덜덜덜...
오늘도 열심히 코드를 치고 계실 김코딩님!!ㅠ.ㅠ
그리고 당신과 함께하는 당신의 노트북!!
김코딩은 이 노트북 내부를 열어보기 힘들고, 구조도 잘 모른다. 하지만, 김코딩은 이 노트북을 어떻게 그리고 왜 써야 되는지를 알 뿐이다.
=> 다시 말해, 노트북의 내부를 열기 힘들기 때문에... 노트북의 돌아가는 방식을 개조해버리는 위험성도 없다. 그냥 노트북을 용도에 맞게 쓰기만 하면 된다!!
이게 추상화이다!!
즉, 추상화는 class 사용자에게 불필요한 정보는 철저하게 숨기고, 사용자가 사용에 필요한 정보만을 제공하는 것을 말한다.
참고로, C++ 와 JAVA에서는 class안에 private 과 public이라는 구역으로 나눠서 객체와 함수를 용도에 맞게 분배해준다.
개인적인 의견으로는 OOP 중에서 가장 핵심적인 내용이면서도 OOP 그 자체를 의미하는 개념이 아닌가 싶다.
특정 class의 속성과 기능을 외부에서 call해서 사용하는 것이 아닌 애초에 내장하고 있는 것을 말한다.
class에 내장된 method들은 class를 통해서 생성된 객체인 instance들만이 접근이 가능하다.
Bag 라는 클래스로부터 생성된 schoolBag이 있다면,
schoolBag.pen() 이런 식으로 Bag이라는 class의 멤버용 메소드들이 존재한다.
이 쯤만 알고 넘어가도 Encapsulation 개념 정리는 됐겠지만...
왜 구지 Encapsulation 해야 되는지 이유가 궁금하다면,
" Programming with Mosh "에서 너무 좋은 설명이 있어서 공유한다.
이런식으로 직원의 wage를 구할 때, getWage 함수를 써줘서, Wage를 구하는 방법도 있다. 여기서 baseSalary는 해당 직원의 기본 월급이다.
만약, 구해야 되는 직원이 1명이라는 상관없다.
그러나, 구해야 되는 직원이 100명이라면???
그 직원에 따라 baseSalary와 overtime 그리고 rate를 다 바꿔서 함수에 보내줘야 되는데... 너무 비효율적이다.
( 언제나 말하지만, 개발자는 항상 효율을 극대화하는 방향을 생각해야 한다. )
하지만, 이렇게 객체를 만들어준다면 어떻게 될까??
직원이 바뀔 때마다 employee1, employee2 등으로 만들어줘서 emplayee1.getWage()만 해준다면... 너무 간편하다.
=> 이렇게 "Encapsulation"은 재사용성을 극대화시켜준다는 점과 불필요하게 복잡해질 수있는 코드를 확!! 줄여준다는 것이다.
이렇게 열심히 class
와 OOP
에 대해서 달려왔건만...
나는 C++
과 Java
를 생각하고 있던 나는 한 가지 큰 문제를 간과하고 있었다......
.....Javascript
에는 Class
가 없다.
아니!! 더 정확하게 말하면, ES6
이전에는 없었다!!
[ 아주 잠깐 동안 진짜 class 없는 줄알고 멘붕왔었다. ]
이제, 간단하게 class를 구현해보면, 다음과 같다.
하지만, 이것은 어디까지나 class를 흉내낸 것일뿐이지, JS가 class기반 언어가 됐다는 것은 아니다.
JavaScript는 프로토타입 기반 언어 이다. 여기서 프로토타입(Prototype)은 원형 객체를 의미한다. 상속을 이해하기 위해서는 이 프로토타입에 대한 이해가 필수적이다. ( ES6에서는 class키워드가 등장하여, 보다 쉽게 상속을 구현할 수 있지만, 그 작동은 프로토타입을 기반으로 한다. )
MDN에 따라서 실습해볼 수 있는 훌륭한 가이드가 나와 있다. 다음 두 문서를 반드시 읽어보고 따라해보면 도움이 될 것같아서 첨부한다.
JS는 프로토타입 기반 언어이고, C++이나 JAVA는 클래스기반 언어이다. 대체 이 둘의 차이는 무엇일까??
코드스테이츠 Help Desk에서 좋은 답변을 주셨는데, 공유해보겠다.
" 상속이라는 기능은 둘 다 동일하게 하지만, 정확성 안정성과 같은 관점에선 클래스기반 언어가 강점을 보이고, 자유롭게 객체의 구조와 동작방식을 바꿀 수 있다는 점에선 프로토타입 기반 언어가 강점을 보인다고 할 수 있습니다. "
JAVA와 같은 전통적인 클래스 기반 언어들은 상속을 부모 클래스 ( super class ) 와 자식 클래스 ( sub class )를 통해서 객체를 생성해낸다. 그렇기 때문에, 해당 객체가 어떠한 기능을 갖게될 것인지는 class
에서 결정된다.
Parent class = super class = 부모 클래스
child class = sub class = 자식 클래스
그런 이유로, 이미 만들어진 객체가 다른 객체의 상속을 받는다던지 하는 것은 불가능하다. 그냥 객체는 태어나면, 본인이 어떤 기능을 갖게 될지가 결정되는 것이다.
물론, JS에서도 class
라는 문법이 있고, extends
라는 키워드가 있지만, 그것은 그냥 장식에 불과하다. 결국, JS가 동작하는 내부 매커니즘이 바뀐 것은 없다. => 그냥 class
는 다른 언어를 썼던 개발자들이 편하게 쓰도록 도입한 문법일 뿐이다.
그런데, JS는 이보다 훨씬 더 자유롭다. 그러나, 그만큼 복잡하고 여러가지 혼란스러운 점들이 있다.
그렇다면 JS는 상속을 어떻게 구현하는가??
전통적인 클래스 기반 언어들은 클래스를 상속 받는데, JS에서는 객체가 직접 다른 객체의 상속을 받을 수 있다. 그리고 개발자가 얼마든지 그 상속관계를 바꿀 수 있다.
예를 들어, 현재 super object를 바꿔주고 싶을 때는 그냥 link만 바꿔주면 된다. 그 링크를 prototype link라고 한다. 이 prototype link가 가리키고 있는 객체를 prototype object라고도 부른다.
super object = prototype object = 부모 객체
sub object = 자식 객체
prototype link
와 prototype object
에 관해 더 자세히 알아보고 싶다면, 필자가 정리한 Prototype in JS를 보는 것을 추천한다.
그렇다면, 프로토타입의 강점인 자유롭게 객체의 구조와 동작방식을 바꿀 수 있다는 점에 대해서 더 알아보자!!
function Person(name, first, second) {
this.name = name;
this.first = first;
this.second = second;
this.sum = function () {
return this.first + this.second;
};
}
let kim = new Person("kim", 10, 20);
let lee = new Person("lee", 30, 40);
위의 코드에서는 kim
과 lee
를 Person
이라는 생성자를 통해서 찍어냈다.
그런데, 여기서 this.sum
이 거슬린다.
sum
이라는 함수는 생성되는 객체마다 고유한 특징을 가지는 함수가 아니다. 그저, 모든 객체에게 코드 한 줄 다르지 않는 동일한 역할을 하는 함수이다.
문제는 sum()
은 객체가 생성될 때마다 새로 만들어지고 있다는 것이다. 그만큼, 새 객체가 생성될 때마다, sum()
을 생성하는 시간이 들 것이고, 메모리가 사용될 것이다.
=> 즉, 이런 접근법은 생산성이 많이 떨어진다.
만약, 우리가 모든 객체들이 동일하게 사용하는 함수를 단 한 번만 생성해서 모든 객체들이 사용하게 할 수있다면, 함수가 매번 생성되는 시간과 메모리를 절약할 수 있다.
어떻게 이것을 실행할 수 있을까?? => 이때, prototype이 등장한다.
function Person(name, first, second) {
this.name = name;
this.first = first;
this.second = second;
}
let kim = new Person("kim", 10, 20);
let lee = new Person("lee", 30, 40);
Person.prototype.sum = function () {
return "프로토타입 " + (this.first + this.second);
};
생성자 함수 안에 sum()
을 정의하는 것이 들어가 있지 않기 때문에, 새로운 객체가 생성자를 통해서 만들어질 때마다, sum()
이 실행되지 않는다. 즉, 딱 한 번만 실행되기 때문에, 성능과 메모리를 절약할 수 있다.
여기서 추가적으로, 만약에
kim
이라는 객체의sum()
은 다르게 동작하게 하고 싶다면 어떻게 해야 할까??
kim.sum = function (params) {
return "kim만의 위해 변형된 " + this.first + this.second;
};
JS는 kim
라는 객체 자신이 sum()
을 가지고 있지 않으면, 이 객체 생성자인 Person
의 메소드에 정의되어 있는지를 찾아본다.
다시 말해서, JS 엔진은 kim이라는 객체에서 먼저 sum()
을 찾기 때문에, kim.sum
을 Person.prototype.sum
보다 먼저 찾게 되고, 실행시킨다.
하지만 lee
객체는 내부에 sum()
이 없기 때문에, Person
객체까지 거슬러올라가게 되고 거기서 Person.prototype.sum
을 찾아 실행시킨다.
여기까지 OOP in JS 끝!!!!!!!!
상속에 관한 자세한 내용은 Inheritance pattern in JS에서 다룬다.
이번 블로그는 코드스테이츠의 강의 내용과 Programming with Mosh 의 일부를 바탕으로 작성했으며, 그 어떠한 상업적 용도도 없음을 밝힌다.