코드스테이츠-부트캠프 [JavaScript - 객체 지향 프로그래밍]

김희목·2024년 3월 2일
0

코드스테이츠

목록 보기
12/56

객체 지향

객체 지향 프로그래밍(OOP, Object-oriented programming)은 사람이 세계를 보고 이해하는 방법과 매우 흡사합니다. 코드를 추상화하여 직관적으로 생각할 수 있기 때문에, 이미 오래전부터 프로그래밍 방법론으로 매우 빠르게 적용되었습니다.

객체 지향 프로그래밍을 철저하게 적용한 프로그래밍 언어 Java나 C#과는 다르게, JavaScript에서 OOP를 구현하는 방법은 조금 독특합니다.

객체 지향 프로그래밍은, 절차 지향 프로그래밍과는 다르게 데이터와 기능을 한 곳에 묶어서 처리합니다.

속성과 메서드가 하나의 "객체"라는 개념에 포함되며,

이는 JavaScript 내장 타입인 object(이하, object literal)와는 다르게, 클래스(Class)라는 이름으로 부릅니다.


클로저 모듈 패턴(1-1)

메서드 호출은 객체.메서드()와 같이 객체 내에 메서드를 호출하는 방법을 의미합니다.

메서드 호출 방식을 이용할 때에는 화살표 함수를 쓰지 않습니다.

그 이유는 화살표 함수 표현식은 this가 없기 때문에 메서드가 아닌 함수에만 사용해야 합니다.

let counter1 = {
  value: 0,
  increase: function() {
    this.value++ // 메서드 호출을 할 경우, this는 counter1을 가리킵니다
  },
  decrease: function() {
    this.value--
  },
  getValue: function() {
    return this.value
  }
}

counter1.increase()
counter1.increase()
counter1.increase()
counter1.decrease()
counter1.getValue() // 2

클로저를 이용해 매번 새로운 객체 생성하기

위의 함수 counter1은 단 하나의 객체만 만들 수 있습니다. 만약 똑같은 기능을 하는 카운터가 여러 개가 필요하다면, 이 코드를 여러 번 만들어야 할까요? 같은 코드를 그대로 복사/붙여넣기 해야 하므로, 재사용성이 떨어집니다.

똑같은 기능을 하는 카운터를 여러 개 만드는 방법 중 하나는, 아래 예제 코드와 같이 클로저 모듈 패턴을 이용할 수 있습니다.

function makeCounter() {
  let value = 0;
  return {
    increase: function() {
      value++;
    },
    decrease: function() {
      value--;
    },
    getValue: function() {
      return value;
    }
  }
}

let counter1 = makeCounter()
counter1.increase()
counter1.getValue() // 1

let counter2 = makeCounter()
counter2.decrease()
counter2.decrease()
counter2.getValue() // -2

클래스와 인스턴스(1-2)

객체 지향 프로그래밍은 하나의 모델이 되는 청사진(blueprint)을 만들고, 그 청사진을 바탕으로 한 객체를 만드는 프로그래밍 패턴입니다.

앞서 언급된 청사진은 자동차 생산을 위한 설계도에 비유됩니다. 자동차가 기능하기 위해서는 네 바퀴와 핸들, 좌석 그리고 엔진이 필요할 것입니다. 이러한 기본적인 설계는 차의 종류에 상관없이 대체적으로 동일하게 적용됩니다.
이런 설계도(청사진)를 바탕으로 각각의 객체가 특정한 자동차 모델로 나오게 되는 것입니다.

이미 자바스크립트에는 "객체"라는 개념이 객체 지향 프로그래밍과 무관하게 이미 존재하죠. 따라서 여기서는 용어를 잘 구분하는 것이 중요합니다.

class = 하나의 모델이 되는 청사진

instance = 청사진을 바탕으로 한 객체를 만드는

객체는 일반적인 함수를 정의하듯 만듭니다.
이때 함수를 이용하는 방법이 조금 다릅니다.

그냥 실행하는 것이 아니고 new 키워드를 써서 만듭니다. 이는 새로운 인스턴스를 만드는 방법입니다.

일반적인 다른 함수와 구분하기 위해 클래스는 보통 대문자로 시작하며 일반명사로 만듭니다.

일반적인 함수는 적절한 동사를 포함하고 소문자로 시작합니다.

ex) function Car(color) { }  // class

// instances
let avante = new Car('blue');  
let mini = new Car('cyan');
let beetles = new Car('red');

그리고 클래스를 만드는 새로운 문법이 ES6(ECMAScript 6, ECMAScript 2015라고도 합니다)에 도입되었습니다. 바로 class 키워드입니다.
최근에는 ES6 방법을 주로 사용합니다.

여기서 보이는 함수는, 객체지향 프로그래밍에서 생성자(constructor) 함수라고 부릅니다. 인스턴스가 만들어질 때 실행되는 코드입니다.

참고로 생성자 함수는 return 값을 만들지 않습니다.최근에는 ES6 방법을 주로 사용합니다.
By Codestates

// es5
function Car(brnad, name, color) {
	// 인스턴스가 만들어질 때 실행되는 코드
 } 
 
 // es6
 class Car {
 	constructor(brand, name, color) {
    	// 인스턴스가 만들어질 때 실행되는 코드
    }
 }

인스턴스를 만들 때에는 new 키워드를 사용합니다. 즉시 생성자 함수가 실행되며, 변수에 클래스의 설계를 가진 새로운 객체, 즉 인스턴스가 할당됩니다.

각각의 인스턴스는 클래스의 고유한 속성과 메서드를 갖게 됩니다.

let avante = new Car('hyundai', 'avante', 'black');
let mini = new Car('bmw' , 'mini', 'white');
// 각각의 인스턴스는 Car라는 클래스의 고유한 속성과 메소드를 갖습니다.

속성 = 브랜드,이름,색상,최고속도

메소드 = 연료주입, 속력설정, 운전


this

객체지향에서 빠지지 않고 등장하므로 간단하게 알아봅시다.

this는 인스턴스 객체를 의미합니다. parameter로 넘어온 브랜드,이름,색상등 속성들은 인스턴스 생성시 지정하는 값이고, this에 할당한다는 것은 만들어진 인스턴스에 해당 브랜드,이름,색상을 부여하겠단 의미입니다.

// es5
function Car(brand, name, color) {
	this.brand = brand;
    this.name = name;
    this.color = color;
}
// es6
function Car {
	constructor(brand,name,color) {
    	this.brand = brand;
        this.name = name;
        this.color = color;
   }
}

메서드 정의

ES5는 prototype이라는 키워드를 사용해야 메서드를 정의할 수 있습니다. Car 클래스에 메서드를 추가하기 위해서는 Car.prototype.refuel과 같이 prototype을 이용해야 합니다.

ES6에서는 생성자 함수와 함께 class 키워드 안쪽에 묶어서 정의합니다. refuel() {}, drive() {}와 같이 작성되어 있는 부분입니다.

//es5
function Car(brand,name,color) { /생략 }
	Car.prototype.refuel = function() {
    // 연려공급 코드
   }
   	Car.prototype.drive = function() {
    	// 운전 구현
     }


//es6
class Car {
	constructor(brand,name,color) { /생략 }
    	refuel(){
        }
        drive() {
        }
    }
// 인스턴스에서의 사용
let avante = new Car('hyundai', 'avante', 'black');
avante.color; // black
avante.drive(); // 아반떼가 운전을 시작합니다.

let mini = new Car('bmw', 'mini', 'white');
mini.color; // white
mini.refuel() // 미니에 연료를 공급합니다.

- 용어 정리 -

prototype : 모델의 청사진을 만들 때 쓰는 원형 객체입니다.

constructor : 인스턴스가 초기화될 때 실행되는 생성자 함수

this : 함수가 실행될 때, 해당 스코프마다 생성되는 고유한 실행 context / new 키워드로 인스턴스를 생성했을 때에는, 해당 인스턴스가 바로 this 값이 됩니다.


let avante = new Car('hyundai', 'avante', 'black');
avante.color; // black
avante.drive() // 아반떼가 운전을 시작합니다.

let arr = ['code', 'states', 'pre'];
arr.length; // 3
arr.push('course'); // 새 element를 추가합니다.

위의 예제처럼 배열은 전부 Array의 인스턴스입니다.

살짝만 더 깊게 들어가 보면, 여러분이 배열 메서드 (push, filter, forEach...) 등을 배울 때에, mdn 문서를 유심히 살펴보신 분들은, 메서드들이 대부분 Array.prototype.메서드명 과 같이 안내되어 있음을 발견하셨을 겁니다.

이는 모든 메서드들이 클래스의 원형 객체(prototype)에 정의되어 있기 때문입니다.


객체 지향 프로그래밍(1-3)

절차 지향 프로그래밍

  1. 초기의 프로그래밍 언어는 일반적으로 절차적 언어라고 부름
  2. 절차적 언어는 순차적인 명령의 조합

객체 지향 프로그래밍

  1. "클래스"라고 부르는 데이터 모델의 청사진을 사용해 코드작성
  2. 현대의 언어들은 대부분 객체 지향의 특징을 갖고 있음
  3. JavaScript: 객체지향으로 작성 가능

OOP

OOP는 프로그램 설계 철학 중 하나입니다. OOP는 객체로 그룹화됩니다. 이 객체는 한번 만들고 나면, 메모리상에서 반환되기 전까지 객체 내의 모든 것이 유지됩니다.

객체 내에는 "데이터와 기능이 함께 있다"라는 원칙에 따라 메서드와 속성이 존재합니다. 예를 한번 들어보겠습니다. 모든 자동차는 공통적인 기능과 고유의 속성이 있습니다. 속도를 낸다든지, 주유를 한다든지 등의 기능이 존재하며, 속성으로는 색상, 최고 속력 혹은 탑승인원 등과 같은 고유의 데이터가 존재하죠.

새로운 객체를 만들 때, "이번에 만들 자동차는, 빨간색의 최고 속력은 200km/h를 내도록 만들어보자!"와 같이, 속성에 고유한 값을 부여할 수 있습니다.

  1. oop는 프로그램의 설계 철학입니다.
  2. oop의 모든 것은 "객체"로 그룹화됩니다.
  3. oop의 4가지 주요 개념을 통해 재사용성을 얻을 수 있습니다.

OOP Basic Concepts

1. 캡슐화

  • 데이터와 기능을 하나의 단위로 묶는 것
  • 은닉(hiding): 구현은 숨기고, 동작은 노출시킴
  • 느슨한 결함(Loose Coupling)에 유리 : 언제든 구현을 수정할 수 있음

= 캡슐화는 외부에서 앞서 말했던 데이터(속성)와 기능(메서드)을 따로 정의하는 것이 아닌, 하나의 객체 안에 넣어서 묶는 것입니다. 데이터(속성)와 기능(메서드)들이 느슨하게 결합되는 것이죠.

느슨한 결합은 코드 실행 순서에 따라 절차적으로 코드를 작성하는 것이 아니라, 코드가 상징하는 실제 모습과 닮게 코드를 모아 결합하는 것을 의미합니다.

은닉화는 내부 데이터나 내부 구현이 외부로 노출되지 않도록 만드는 것입니다.

2. 상속

  • 부모 클래스의 특징을 자식 클래스가 물려받는 것
  • 기본 클래스의 특징을 파생 클래스가 상속 받는다.

ex) 사람(Human)이라는 클래스가 있다고 가정해 봅시다. 사람은 기본적으로 이름과 성별, 나이와 같은 속성, 그리고 먹다, 자다 등과 같은 메서드가 있다고 볼 수 있습니다.

추가적으로 학생(Student)이라는 클래스를 작성한다고 생각해 봅시다. 그런데 이때 앞서 구현했던 사람(Human) 클래스의 속성과 메서드를 다시 구현한다면 비효율적일 것입니다. 학생의 본질은 결국 사람이므로, 상속을 이용하여 학생(Student) 클래스는 사람(Human) 클래스를 상속받을 수 있습니다. 학생은 추가적으로 학습 내용, 공부하다 와 같은 속성/메서드를 추가합니다.

3. 추상화

  • 내부 구현이 아주 복잡함
  • 실제로 노출되는 부분은 단순하게 만든다는 개념
  • 인터페이스를 단순화

= 캡슐화와 종종 헷갈려 하는데, 캡슐화가 코드나 데이터의 은닉에 포커스가 맞춰져있다면, 추상화는 클래스를 사용하는 사람이 필요하지 않는 메서드를 노출시키지 않고 단순한 이름으로 정의하는 것에 포커스

클래스 정의 시, 메서드와 속성만 정의한 것을 인터페이스라 부르고 이것이 추상화의 본질입니다.

4. 다형성

  • 다양한 형태를 가짐
  • 객체에서 똑같은 메서드라도 다른 방식으로 구현 가능

ex) 다형성을 HTML 엘리먼트를 예로 들어 설명해 보겠습니다. Textarea(TextBox), Select, 그리고 Checkbox 등의 다양한 요소가 있을텐데요. HTML에서는 이와 같이 모든 요소를 전부 Element라고 부릅니다.

이 엘리먼트를 여러분들이 직접 구현한다고 생각해 보세요. 모든 엘리먼트들은 전부 객체이므로, 내부적으로 모양을 그리고 화면에 뿌리는 메서드가 존재할 것입니다. 이 메서드가 render라는 이름을 갖고 있다고 가정해 봅시다.

이 경우에는, TextBox, Select, Checkbox의 공통의 부모인 HTML Element라는 클래스에 render라는 메서드를 만들고 상속을 받게 만들 수 있습니다. 그런데 다형성의 핵심은 이 같은 이름의 render라는 메서드가 조금씩 다르게 작동한다는 데 있습니다.

TextBox는 가로로 긴 네모 상자와 커서가 있는 형태일 것이고, Select 박스는 눌렀을 때 선택지가 나오도록 화면에 그려야 할 것입니다. 이처럼 같은 이름을 가진 메서드라도 조금씩 다르게 작동합니다. 이것이 바로 다형성입니다.

- 정리 -

캡슐화는 코드가 복잡하지 않게 만들고, 재사용성을 높입니다.

추상화는 마찬가지로 코드가 복잡하지 않게 만들고, 단순화된 사용으로 변화에 대한 영향을 최소화합니다.

상속 역시 불필요한 코드를 줄여 재사용성을 높입니다.

다형성으로 인해 동일한 메서드에 대해 if/else if와 같은 조건문 대신 객체의 특성에 맞게 달리 작성하는 것이 가능해집니다.

즉 코드 상에서, 혹은 화면에서 보이는 하나의 요소를 객체 단위로 구분시켜서 생각하여 보다 이해하기 쉬운 코드 작성을하고 좋은 설계를 할 수 있다.


class와 instance

  1. 클래스는 일종의 원형으로, 객체를 생성하기 위한 아이디어나 청사진입니다.
  2. 인스턴스는 클래스의 사례입니다.
  3. 클래스는 객체를 만들기 위한 생성자(constructor) 함수를 포함합니다.

객체지향 차이점(1-4)

자바스크립트에서 보다 더 많은 기능/ 더 객체 지향적으로 디자인된 언어 TypeScript에 대해 간단히 설명하고 넘어가겠습니다.

은닉화(private 키워드)의 한계

Java나 TypeScript라는 프로그래밍 언어는 클래스 내부에서만 쓰이는 속성 및 메서드를 구분시키기 위해 private이라는 키워드를 제공합니다. 이러한 은닉화를 도와주는 기능이 JavaScript에서는 널리 쓰이지 않습니다. 정확히는 지원하는 브라우저가 적습니다.

아래는 TypeScript의 예제로, name이라는 속성이 존재합니다. 그러나 private 키워드가 붙어 있어서, 클래스 내부에서만 사용 가능합니다.

 TypeScript 문법입니다.

class Animal {
  private name: string;

  constructor(theName: string) {
    this.name = theName;
  }
}

new Animal("Cat").name; // 사용 불가
// Property 'name' is private and only accessible within class 'Animal'. 

추상화(interface 키워드) 기능의 부재

객체 지향 프로그래밍의 주요 키워드 중 하나인 추상화는, 속성과 메서드의 이름만 노출시켜서 사용을 단순화한다는 의미를 갖고 있습니다. 즉, 인터페이스(interface)의 단순화를 의미합니다. Java나 TypeScript 언어는 언어의 주요 기능으로 interface를 구현해 놓았습니다. 그러나 이러한 부분은 JavaScript에는 존재하지 않는 기능입니다.

다음 코드를 한번 살펴보세요. 인터페이스와 구현이 따로 정의되어 있습니다.

// TypeScript 문법입니다.

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}

class Clock implements ClockInterface {
  currentTime: Date = new Date();
  setTime(d: Date) {
    this.currentTime = d;
  }
  constructor(h: number, m: number) {}
}

인터페이스의 이점은, 인터페이스가 일종의 규약처럼 간주되어, 인터페이스를 클래스로 구현하는 사람들이 이에 맞게 작성할 수 있게 돕습니다. (인터페이스는 다양한 구현이 있을 수 있습니다.) 이는 클래스를 이용하는 입장에서 노출된 인터페이스를 통해 "이 클래스는 메서드 이름이 의도한 바대로 작동할 것이다"라는 것을 명백히 드러나게 해 줍니다. 또한 실질적인 구현 방법을 공개하지 않고, 사용법을 노출시키기에도 유리합니다.


프로토타입

JavaScript는 프로토타입(Prototype) 기반 언어입니다. 여기서 프로토타입은 원형 객체를 의미합니다.

Javascript에서는 객체를 상속하기 위하여 프로토타입이라는 방식을 사용합니다.

모든 객체들이 메소드와 속성들을 상속받기 위한 템플릿으로써 프로토타입 객체를 가진다는 의미입니다.

프로토타입 객체도 상위 프로토타입 객체로부터 메소드와 속성을 상속받을 수도 있고, 그 상위 프로토타입 객체도 마찬가지입니다.

이를 프로토타입 체인이라고 부릅니다.


프로토타입과 클래스(2-1)

class Human {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sleep() {
    console.log(`${this.name}은 잠에 들었습니다`);
  }
}

let kimcoding = new Human('김코딩', 30);

// 실습해보세요
Human.prototype.constructor === Human; 
Human.prototype === kimcoding.__proto__; 
Human.prototype.sleep === kimcoding.sleep;

Human이라는 클래스와 인스턴스, 그리고 프로토타입의 관계

배열 클래스와 인스턴스, 그리고 프로토타입의 관계


프로토타입 체인(2-2)

객체 지향 프로그래밍의 특성 중 상속을 JavaScript에서 구현할 때에는 프로토타입 체인을 사용합니다.

예시를 들기 위해, 학생(Student)과 사람(Human)이라는 클래스가 각각 존재한다고 가정하겠습니다. 클래스 Human의 메서드와 속성을 객체로 구현한다면, 다음과 같습니다.

let kimcoding = new Human('김코딩', 30);

// 속성
kimcoding.age;
kimcoding.gender;
// 메서드
kimcoding.eat();
kimcoding.sleep();

//학생은 학생이기 이전에, 사람입니다. 
따라서 클래스 Student는 Human의 기본적인 메서드를 상속받을 수 있습니다. 
다만, 학생은 일반적인 사람의 특징에 추가적인 특징이 필요합니다. 
예를 들면 다음과 같습니다.

let parkhacker = new Student('박해커', 22);

// 속성
parkhacker.grade;
// 메서드
parkhacker.learn()

위 예시에서 나타나는 박해커(parkhacker)는 Student입니다.

박해커 씨가 학생이라고 해서, age나 gender와 같은 속성이 존재하지 않거나, sleep()이나 eat()이라는 메서드를 사용할 수 없을까요? 그렇지 않습니다.

Student는 Human의 특징을 그대로 물려받습니다. 이렇게 속성과 메서드를 물려주는 클래스를 부모 클래스(여기서는 Human), 속성과 메서드를 물려받는 클래스를 자식 클래스(여기서는 Student), 그리고 이 과정을 상속이라고 합니다.


DOM과 프로토타입

브라우저에서 DOM을 이용하면, document.createElement('div')로 새로운 div 엘리먼트를 만들 수 있습니다. 이렇게 생성된 div 엘리먼트는, HTMLDivElement라는 클래스의 인스턴스입니다.

DOM 엘리먼트는 예를 들어 innerHTML과 같은 속성, 또는 append()와 같은 메서드가 있습니다. 각각의 엘리먼트가 해당 메서드나 속성이 있다는 것을 통해, Element라는 공통의 부모가 있음을 알 수 있습니다.

0개의 댓글