- 학습 내용
지금까지 프로그래밍을 공부하고 작성했던 방법은 대부분 절차 지향 프로그래밍을 패러다임으로 한 것들이었다. 절차 지형 프로그래밍이란 말 그대로 코드를 절차대로 나열하듯이 작성하는 방법이다. 처음 프로그래밍을 할 때는 이런 절차 지향 프로그래밍적 학습이 중요하다고 한다. 왜냐하면 코드의 흐름들 파악하는 공부로 썩 괜찮기 때문이다. 그러나 이런 방법의 코드들은 가독성이나 효율성에 대한 부분에서 분명 부족한 게 사실이다. 이로 인해 자연스럽게 추구하게 된 방법이 객체 지향 프로그래밍(OPP, Object-oriented programmin)이다. 이것은 절차 지향 프로그래밍과는 다르게 데이터와 기능을 한 곳에 묶어서 처리하는 방식이다. 이런 데이터와 기능인 속성과 메소드를 하나의 "객체"라는 개념으로 포함하여 처리하는 것이다. 자바스크립트에서는 이것을 클래스(Class)라고 부른다. 여기서 알아두어야 할 점은 JS는 Java나 C#처럼 완전히 객체 지향 프로그래밍을 적용한 프로그래밍 언어가 아니라는 것이다. 자바스크립트의 경우 클래스라는 문법을 통해 객체 지향 프로그래밍을 사용할 수 있는 언어라고 이해하는 것이 맞다. 이번 시간 클래스를 사용하는 방법과 객체 지향 프로그래밍의 특징에 대해서 그리고 상속의 개념과 프로토타입에 대해서 살펴보도록 하겠다.
- 클래스(Class)와 인스턴스(instance)
객체 지향 프로그래밍이란 하나의 모델이 되는 청사진을 만들고 그 청사진을 바탕으로 객체를 만드는 프로그래밍 패턴이다. 여기서 하나의 모델이 되는 청사진을 클래스라 부르고, 그 청사진을 바탕으로 만드는 객체를 인스턴스라고 부른다. 클래스를 만드는 방법에는 크게 두 가지 방법이 현존하는데, 먼저 일반적인 함수를 정의하듯이 만드는 것이다. 그러나 일반 함수와 다르게 대문자를 시작으로 하는 그리고 일반명사로 이름을 만들어 일반 함수와 구별한다. 그리고 이용할 때는 new 키워드를 앞에 써서 만들어야 한다. 또 다른 방법으로 ES6부터 추가된 문법인데 class라는 키워드를 사용하는 것이다. class키워드를 사용할 때는 인스턴스를 만드는 코드로 생성자(constructor)를 사용한다. 여기에는 return을 사용하지 않는다는 특징이 있다.
function Car(brand, name, color) {
// 인스턴스가 만들어질 때 실행되는 코드
}
class Car {
constructor(brand, name, color) {
// 인스턴스가 만들어질 때 실행되는 코드
}
- 객체 지향 프로그래밍
위에서 언급한 대로 객체 지향 프로그래밍이라는 패러다임이 등장하기 전까지는 절차 지향 언어들이 있었다. 대표적인 언어는 C와 포트란을 꼽을 수 있다. 절차 지향 프로그래밍은 순차적으로 명령을 조합한다. 그러나 객체 지향 프로그래밍 패러다임이 등장하면서 데이터의 접근과 데이터의 처리 과정에 대한 모형을 만들어 내는 방식을 고안해냈다. 이로 인해 데이터와 기능을 한 번에 묶어서 처리할 수 있게 되었다. OOP는 프로그램 설계 철학 중 하나이다. 객체로 그룹화되고, 이 객체는 한번 만들고 나면 메모리상에서 반환되기 전까지 객체 내의 모든 것이 유지된다.
클래스는 세부 속성이 들어가지 않은 청사진이다. 세부 사항을 넣으면 객체가 되는 것이다. 이렇게 만들어진 객체를 인스턴스 객체 즉, 인스턴스라고 부른다. 그리고 이 세부 사항을 넣어주는 역할을 하는 것이 바로 생성자이다. 함수에 인자를 넣듯, 속성을 넣을 수 있다.
class Car {
constructor(color,price,speed) {
// 속성
}
// 메서드
Start();
Backward();
Forward();
Stop();
}
- 객체 지향 프로그래밍의 컨셉
1) 캡슐화
캡슐화는 데이터(속성)와 기능(메소드)를 하나의 객체 안에 넣어서 묶는 것이다. 속성과 메소드들을 느슨하게 결합시키는 것이다. 여기서 느슨한 결합이란 코드 실행 순서에 따라 절차적으로 코드를 작성하는 것이 아닌 코드가 상징하는 실제 모습과 닮게 코드를 모아 결합하는 것을 의미한다. 캡슐화라는 개념 안에는 '은닉화'의 특징 또한 포함되어 있는데, 은닉화는 내부 데이터나 내부 구현이 외부로 노출되지 않도록 만드는 것이다. 이로 인해 디테일한 구현이나 데이터는 감추고 필요한 동작만 노출시키는 것이다. 은닉화의 특징을 살려서 코드를 작성하면 객체 내 메소드의 구현만 수정하고, 노출된 메소드를 사용하는 코드 흐름은 바뀌지 않도록 만들 수 있다. 이것을 더욱 활용하기 위해 더 엄격한 클래스로는 속성의 직접적인 접근을 막고, 설정하는 함수(setter), 불러오는 함수(getter)를 철저하게 나누기도 한다.
2) 추상화
추상화란 내부 구현은 아주 복잡한데 비해 실제로 노출되는 부분은 단순하게 만드는 개념이다. 추상화의 개념을 통해 사용하는 인터페이스가 단순해질 수 있으며, 너무 많은 기능들이 노출되지 않기 때문에 예기치 못한 사용상의 변화를 방지할 수 있다. 추상화와 캡슐화의 개념을 혼동할 수도 있는데, 캡슐화는 코드나 데이터의 은닉에 집중되어 있다면 추상화는 클래스를 사용하는 사람들이 필요하지 않은 메소드등을 감추고 단순한 이름으로만 정의하는 것에 집중되어 있다.
3) 상속
상속은 부모 클래스의 특징을 자식 클래스가 물려받는 것이다. 기본 클래스(base class)의 특징을 파생 클래스(derive class)가 상속받는 것이다. 사람이란 클래스가 있다고 가정한다면 기본적으로 사람은 이름, 성별, 나이 같은 속성을 가지고, 먹다, 자다와 같은 메소드를 수행한다. 이것이 기본 클래스라고 한다면 학생이란 클래스를 만들어 사람 클래스의 속성과 메소드를 상속받게 할 수 있다. 모든 학생은 사람이기 때문에 상속받는 것이 전혀 어색하지 않고, 이 것을 파생 클래스라고 한다. 상속받은 것에 수강과목, 학년과 같은 속성을 추가하고 배운다와 같은 메소드를 추가할 수 있다.
4) 다형성
다형성이란 다양한 형태라는 뜻이다. 예를 들어 객체의 메소드가 비록 하나여도 다양한 형태를 가질 수 있다는 것이다. HTML 엘리먼트 중 Textarea, Select, Checbox 등이 있다. 이 모두를 Element라고 부른다. 이 엘리먼트를 객체로 구현한다고 하면, 이 기능들을 모두 하나의 이름으로 구현할 수 있다. 그러나 내부적으로 모양을 그리고 화면에 뿌리는 형태의 메소드는 다르게 구현하게 된다. 이 경우 TextBox, Elect, Checkbox의 공통의 부모인 HTML Element라는 클래스에 특정한 메소드를 만들고 상속받게 만들면 같은 이름의 메소드라도 조금씩 다르게 작동할 수 있게 되는 것이다.
if (type === 'select') {
rederSelect()
} else if (type === 'text') {
renderTextBox()
} else if (type === 'checkbox') {
renderCheckBox()
}
- 클래스와 프로토타입
JavaScript는 프로토타입 기반 언어이다. 프로토타입(Prototype)이란 원형 객체를 의미한다. 모든 자바스크립트 객체는 [[Prototype]]이라는 숨김 프로퍼티를 갖는 것이다. 이 숨김 프로퍼티의 값은 null이거나 다른 객체에 대한 참조가 된다. 여기서 다른 객체를 참조하는 경우에 참조 대상을 프로토타입(prototype)이라고 부르는 것이다. 프로토타입의 동작 방식은 특이한 면이 좀 있다. object에서 프로퍼티를 읽으려고 할 때 해당 프로퍼티가 없으면 자바스크립트는 자동으로 프로토타입에서 프로퍼티를 찾는다. 이런 동작 방식으로 '프로토타입 상속'이라고 한다. 이런 프로토타입은 다양한 방법을 통해 값을 설정할 수 있다. 먼저 프로토타입의 getter이자 setter인 __proto__를 사용하면 프로토타입을 획득하거나 설정할 수 있다. 물론 지금은 __proto__는 잘 사용되지 않는다. __proto__보단 Pbject.create(proto, [descriptors]) (프로토타입이 proto를 참조하는 빈 객체를 만든다. 프로퍼티 설명자를 추가로 넘길 수 있다)로 생성하고, Object.getPrototypeOf(obj)로 프로토타입을 반환한다. 그리고 Object.setPrototypeOf(obj, proto)로 obj의 프로토타입이 proto가 되도록 설정한다.
class Human {
constructor(name, age) {
this.name = name;
this.age = age;
}
eat() {
console.logO(`${this.name}이(가) 먹습니다`);
}
}
let john = new Human('존', 27);
Human.prototype.constructor === Human; // true
Human.prototype === john.__proto__; // true
Human.prototype.eat === john.eat; // true
let human = new Human('사람', 20);
let student = {
class: 3
__proto__: human
};
student.name; // '사람'
프로퍼티를 설정할 때는 어떻게 해야 할까? 프로토타입은 프로퍼티를 읽을 때만 사용한다. 프로퍼티를 추가, 수정하거나 지울 때는 객체에 직접 해야 한다.
또 위의 예시에서 'this'에 어떤 값이 들어가는지 의문이 들 수 있다. 예를 들어 student 프로퍼티에 this.name을 사용하면 그 값이 human에 저장될까? student에 저장될까? 답은 this는 프로토타입에 영향을 받지 않는다는 것이다. 메소드를 객체에서 호출했든 프로토타입에서 호출했든 상관없이 this는 언제나. 앞에 있는 객체가 된다. 사실 이 외에도 클래스와 프로토타입에 대한 문법은 아주 많다. 더 자세한 내용은 다음에 따로 다룰 것이다.
- 프로토타입 체인
클래스 역시 상속을 하면 클래스를 다른 클래스로 확장할 수 있다. 'extends'키워드를 사용해서 할 수 있는데, 예를 들면 이렇다.
class Animal {
constructor(name) {
this.name = name;
this.speed = 0;
}
run(speed) {
this.speed = speed;
console.log(`${this.name} 은/는 속도 ${this.speed}로 달립니다.`);
}
stop() {
this.speed = 0;
console.log(`${this.name} 이/가 멈췄습니다.`)
}
}
let animal = new Animal("동물");
class Rabbit extends Animal {
hide() {
console.log(`${this.name} 이/가 숨었습니다.`)
}
}
let rabbit = new Rabbit("흰 토끼");
rabbit.run(5); // 흰 토끼 은/는 속도 5로 달립니다.;
rabbit.hide(); // 흰 토끼 이/가 숨었습니다.
이렇게 Rabbit을 사용해 만든 객체는 rabbit.hide() 같은 Rabbit에 정의된 메소드에도 접근할 수 있고, rabbit.run() 같은 Animal에 정의된 메소드에도 접근할 수 있다. 이 이유는 extends가 프로토타입을 기반으로 동작하기 때문이다. extends는 Rabbit.prototype.[[prototype]]을 Animal.prototype으로 설정한다. 그래서 Rabbit.prototype에서 메소드를 찾지 못하면 Animal.prototype에서 메소드를 가져오는 것이다.
1) 메소드 오버 라이딩
만약 Rabbit에서 stop() 메소드를 자체적으로 정의하면 어떻게 될까? 이때는 상속받은 메소드가 아닌 자체 메소드를 사용한다. 코드를 작성할 때 부모 메소드를 전체 교체하지 않고, 부모 메소드를 토대로 일부 기능만 변경하거나 부모 메소드의 기능을 확장할 수도 있다. 이때 커스텀 메소드를 만들게 된다. 그런데 이미 커스텀 메소드를 만들었다고하면 이 과정 전, 후에 부모 메소드를 호출할 때는 어떻게 할까? 이 때 super키워드를 사용한다. super.method()를 통해 부모 클래스에 정의된 메소드를 호출할 수 있다. 또한 super()는 부모 생성자를 호출하는데, 자식 생성자 내부에서 사용할 수 있다.
class Animal {
constructor(name) {
this.name = name;
this.speed = 0;
}
run(speed) {
this.speed = speed;
console.log(`${this.name} 은/는 속도 ${this.speed}로 달립니다.`);
}
stop() {
this.speed = 0;
console.log(`${this.name} 이/가 멈췄습니다.`)
}
}
let animal = new Animal("동물");
class Rabbit extends Animal {
hide() {
console.log(`${this.name} 이/가 숨었습니다.`)
}
stop() {
super.stop();
this.hide();
}
}
let rabbit = new Rabbit("흰 토끼");
rabbit.run(5); // 흰 토끼 은/는 속도 5로 달립니다.;
rabbit.stop(); // 흰 토끼 이/가 멈췄습니다. 흰 토끼 이/가 숨었습니다.;
2) 생성자 오버 라이딩
앞의 Rabbit 클래스는 constructor가 없었다. 클래스가 다른 클래스를 상속받고 constructor가 없는 경우 '비어있는' constructor가 만들어진다.
class Rabbit extends Animal {
constructor(...args) {
super(...args);
}
}
이처럼 생성자는 기본적으로 부모 constructor를 호출한다. 이때 부모 constructor에도 인수를 모두 전달하기 때문에 클래스에 자체 생성자가 없는 경우에 이런 일이 모두 자동으로 일어난다. 그렇다면 Rabbit에 커스텀 생성자를 어떻게 추가할 수 있을까? 그냥 진행하면 오류가 난다. 왜냐하면 상속 클래스의 생성자에선 반드시 super()를 호출해야 하기 때문이다. 일반 클래스의 생성자 함수와 상속 클래스의 생성자 함수 간의 차이는 new와 함께 드러나는데, 일반 클래스가 new와 함께 실행되면 빈 객체가 만들어지고 this에 이 객체를 할당한다. 그러나 상속 클래스의 생성자 함수를 실행하게 되면, 일반 클래스에서 일어난 일이 일어나지 않고, 상속 클래스의 생성자 함수가 빈 객체에 만들어지고 this에 이 객체를 할당하는 일을 부모 클래스의 생성자가 처리해주길 기대한다. 이런 사이 때문에 상속 클래스의 생성자에선 super를 호출해 부모 생성자를 반드시 실행시켜 주어야 한다.
class Rabbit extends Animal {
constructor(name, earLength) {
super(name);
this.earLength = earLength;
}
}
- 느낀 점
진행하고 있는 부트캠프 코드 스테이츠의 섹션 1과 섹션 1의 H.A가 모두 끝났다. 그리고 곧장 섹션 2로 넘어왔다. 그리고 그 순간 나는 밀려오는 학습내용과 난이도 때문에 정신이 없었다. 사실 이 블로그 게시글 역시 대략 한 달 만에 올리게 되었다. 학습을 따라가는 것에 바빠서 블로깅을 미루다 보니 나타난 현상이다. 사실 섹션 1을 거의 마무리 해 갈 쯤엔 어느 정도 자신감이 생겼었다. 왜냐하면 주어지는 새로운 학습내용을 학습하는 것에 이전보다 훨씬 수월해졌기 때문이다. 그러나 섹션 2가 진행되자마자 착각이란 것을 알았다. 이번 사태를 마주하며 느낀 것은 앞으로 내가 개발 공부를 지속해가며 꾸준히 느끼게 될 것이 어떤 것이냐는 것이다. 그래도 긍정적인 것은 이미 한 계단은 넘어봤다는 것이다. 비록 더 높은 난이도라 할지라도 꾸준히 포기하지 않고 한다면 이번 계단도 오를 수 있다 믿는다. 그리고 앞으로 마주할 계단 역시 마찬가지일 것이다.
오늘 정리한 학습 내용은 내가 이전에 의문을 가졌던 자바스크립트에 대한 특징을 알 수 있는 시간이었다. 처음 자바스크립트를 배울 때 비슷하게 학습했던 내용이지만 신기하게 다가오는 부분이 달랐다. 훨씬 깊게 이해한 느낌이다. 객체 지향 이라던지 프로토타입이라던지 상속이라던지 물론 아직 내용은 어렵고 완전히 이해하지 못한 부분도 있고, 사실 더 알아야 하는 부분도 있다. 그러나 기본적으로 프로토타입이 자바스크립트의 객체 자체가 가지고 있는 참조 객체이고, 상속이라는 것이 진행되면 상속받은 객체는 상속해준 객체를 참조하는 것이 아닌 그 객체의 프로토타입을 참조한다는 정도는 이해할 수 있었다. 그리고 오늘 내용을 정리하면서 느낀 가장 큰 부분은 아직도 내가 절차 지향으로만 개발을 하고 있다는 것이다. 여전히 객체 지향적인 개발이 어색하고 그렇게 접근하는 것이 쉽지 않다. 이제부터라도 알고리즘 문제를 푸는 것에 객체지향적으로 접근해보고자 한다.
'JavaScript' 카테고리의 다른 글
JS/알고리즘 자료구조 기초 (0) | 2021.10.20 |
---|---|
JS/알고리즘 재귀 알고리즘(recursive algorithms) (0) | 2021.10.08 |
JavaScript 중급1 고차함수(higher order function) (0) | 2021.08.05 |
JavaScript 기초9 클로저(closure)와 Spread/Rest문법 (0) | 2021.07.23 |
JavaScript 기초8 자료형(type)과 스코프(scope) (0) | 2021.07.22 |