14. 클래스

14-1. 객체지향 프로그래밍 소개

14-1-1. 객체지향 프로그래밍의 개념

객체지향 프로그래밍(OOP, Object-Oriented Programming)은 소프트웨어 개발 방법론 중 하나로, 프로그램을 객체들의 집합으로 구성하고, 이 객체 간의 상호작용을 통해 프로그램을 설계하고 구현하는 방식이다. 다시 말해, 현실 세계의 객체와 그 객체들의 상호작용을 모델링하여 소프트웨어를 개발하는 방법이다.
객체(Object)란 사물이나 개념을 나타내는 다른 것들과 구분할 수 있는 것을 의미한다. 각각의 객체는 객체의 특징이나 정보를 나타내는 상태(state)와 특정한 행동을 수행하는 동작(behavior)으로 표현할 수 있다. 예를 들어, 사람은 이름, 나이, 생년월일 등의 상태와 걷다, 먹다 등의 동작이 있다. 자바스크립트에서는 이러한 상태와 동작을 각각 프로퍼티(property)와 메서드(method)라고 부른다.
 

14-2. 클래스 정의

14-2-1. 클래스의 개념

앞서 10장 프로토타입과 11장 생성자에서 살펴본 것처럼 자바스크립트에서는 생성자 함수와 프로토타입을 기반으로 하여 객체를 생성하고 객체의 상속 구조를 만들어 낸다. 이러한 객체지향 메커니즘은 기존의 Java와 C++ 같은 클래스 기반의 프로그래밍 언어에 친숙한 사용자가 자바스크립트에 접근하는데 있어 어려움과 불편함을 주었다.
하지만 ES6에서 클래스 문법이 추가되면서 자바스크립트에서도 클래스 기반의 언어와 비슷하게, 더욱더 직관적으로 객체지향을 구현할 수 있게 되었다. 클래스와 생성자 함수 모두 프로토타입 기반으로 객체를 생성하지만 동작 방식에서 약간의 차이가 있다.
 

14-2-2. 클래스 기본 문법

클래스는 class 키워드를 사용하여 선언하고 생성자와 메서드를 포함할 수 있다.
class ClassName { constructor() { } method1() { } method2() { } }
  • ClassName: 클래스의 이름을 나타낸다. 클래스 이름은 일반적으로 단어의 첫 글자를 대문자로 표현하는 파스칼 케이스(Pascal Case)를 사용한다.
  • constructor: 클래스의 생성자를 나타내며, 생성하려는 객체의 프로퍼티를 초기화하는 역할을 한다. 클래스 내부에서 단 한 개만 존재할 수 있고 생략 가능하다.
  • method1, method2: 클래스의 메서드를 나타내며, 생성된 객체의 동작을 표현하는 역할을 한다. 클래스 내부에서 0개 이상 존재할 수 있다.
 
클래스는 생성자 함수와 마찬가지로 new 키워드를 사용하여 호출할 수 있으며, 인스턴스(instance), 즉 클래스의 객체를 생성한다.
class ClassName { } const instance = new ClassName(); console.log(instance); // ClassName {}
생성자 함수는 new 키워드 없이도 호출이 가능하지만, 클래스는 반드시 new 키워드를 사용하여 호출해야 한다.
class ClassName { } const instance = ClassName(); // TypeError: Class constructor ClassName cannot be invoked without 'new'
식별자에 할당하여 클래스 표현식으로도 클래스를 정의할 수 있지만 클래스 선언문을 사용하는 것이 일반적이다.
const ClassName = class { };

14-2-3. 클래스와 객체에 관한 오해

객체지향 프로그래밍에서의 클래스와 객체와의 관계를 설명할 때 흔히 붕어빵틀과 붕어빵으로 비유하곤 한다. 이를 코드로 다음과 같이 표현할 수 있다.
class 붕어빵틀 { } const 붕어빵 = new 붕어빵틀();
위 예제의 코드는 붕어빵틀 클래스를 통해 붕어빵이라는 객체, 즉 붕어빵틀의 인스턴스를 생성하는 문법적으로 전혀 문제가 없는 코드이다. 하지만 이 예시에는 논리적인 오류가 있다고 할 수 있다. 새로운 붕어빵틀을 만들었는데 붕어빵이 되었다? 앞뒤가 맞지 않는 문장이다. 위 예시는 붕어빵틀이 아닌 붕어빵이라는 클래스를 통하여 팥 붕어빵과 크림 붕어빵을 만들어 내는 것이 논리적으로 맞다.
객체지향 프로그래밍에서의 클래스는 분류의 개념이고, 객체는 그 분류의 실체라고 할 수 있다. 다시 말해, 클래스는 객체들의 공통적인 상태와 행동을 하나로 묶어낸 추상적인 분류의 개념이며, 객체를 찍어내는 팩토리(factory)의 개념이 아니다. 따라서 클래스와 객체와의 관계를 명확하게 이해하고 올바른 관점으로 객체지향 프로그래밍에 접근하는 것이 바람직하다.

14-2-4. 클래스 호이스팅

클래스는 var , let , const 키워드를 사용한 변수 선언과 function 키워드를 사용한 함수 선언과 마찬가지로 호이스팅이 일어난다. (9장 호이스팅 참고) 하지만 다음과 같이 클래스 선언문 이전에 해당 클래스를 참조하려 하면 에러가 발생한다.
console.log(Animal); // ReferenceError: Animal is not defined class Animal { }
이런 현상이 발생하는 이유는 클래스 선언문은 let이나 const 키워드로 선언한 변수처럼 호이스팅이 동작하기 때문이다. 즉, 클래스 선언문 이전에 일시적 사각지대(TDZ, Temporal Dead Zone)에 빠지기 때문에 호이스팅이 일어나지 않는 것처럼 동작한다.
const Animal = 'animal'; { console.log(Animal); // ReferenceError: Cannot access 'Animal' before initialization class Animal { } }
클래스 표현식 역시 호이스팅이 일어난다. 하지만 클래스 표현식의 호이스팅은 클래스 선언문과 다르게 동작한다.
console.log(MyClass); // undefined const instance = new MyClass(); // TypeError: MyClass is not a constructor var MyClass = class { }
클래스 표현식은 함수 표현식과 마찬가지로 결국 변수 식별자에 할당되기 때문에 클래스 선언문과 같은 호이스팅이 아닌 변수 호이스팅이 일어난다.
엄밀히 말하면 var 키워드로 선언된 MyClass 변수 자체는 변수 호이스팅이 일어나 undefined를 참조하게 되지만, MyClass에 할당된 클래스 자체는 호이스팅의 대상이 아니기 때문에 클래스 정의 이전에 참조하게 되면 에러가 발생하는 것이다.

14-3. 메서드

14-3-1. 메서드의 종류

클래스 내부에서 메서드를 정의할 때는 function 키워드가 생략된 메서드 축약 표현을 사용해야 하며, 다음과 같은 3가지의 메서드를 정의할 수 있다.
  • constructor
  • 프로토타입 메서드
  • 정적 메서드
constructor
constructor는 클래스의 인스턴스를 생성하고 프로퍼티를 초기화하는 역할을 한다.
class Car { constructor(name) { this.name = name; } }
constructor는 클래스 생성 시 암묵적으로 정의된 메서드이기 때문에 이름을 변경할 수 없고, 하나의 클래스에 2개 이상의 constructor를 정의할 수 없다.
class Car { constructor() { } consturctor() { } // SyntaxError: A class may only have one constructor }
constructor는 생략할 수 있지만 클래스 외부에서 프로퍼티를 전달받으려면 명시적으로 정의해야 한다. 이때, this 키워드를 참조하여 프로퍼티를 초기화해야 한다. 이 경우, this는 클래스가 생성할 객체, 즉 인스턴스를 가리킨다.
new 키워드를 사용하여 클래스를 호출하면 constructor가 실행되어 내부에서 암묵적으로 인스턴스를 생성하고, 인스턴스를 참조하는 this를 반환한다.
class Car { constructor(name, color) { this.name = name; this.color = color; } } const myCar = new Car('Grandeur', 'black'); console.log(myCar); // Car {name: 'Grandeur', color: 'black'}
프로토타입 메서드
클래스 내부에 정의한 메서드는 기본적으로 프로토타입 메서드이다. 생성자 함수의 프로토타입 메서드와 마찬가지로 생성된 객체에서 접근하여 사용할 수 있다.
class Car { constructor(name, color) { this.name = name; this.color = color; } // 프로토타입 메서드 getInfo() { console.log(`Name: ${this.name}, Color: ${this.color}`); } } const myCar = new Car('Grandeur', 'black'); myCar.getInfo(); // Name: Grandeur, Color: black
정적 메서드
static 키워드와 함께 메서드를 정의하면 인스턴스를 생성하지 않아도 호출할 수 있는 정적 메서드가 된다. 인스턴스를 통해 호출을 시도하면 에러가 발생한다. 이는 정적 메서드는 생성된 인스턴스의 프로토타입 체인에 존재하지 않기 때문이다.
class Car { constructor(name, color) { this.name = name; this.color = color; } // 프로토타입 메서드 getInfo() { console.log(`Name: ${this.name}, Color: ${this.color}`); } // 정적 메서드 static drive() { console.log('Driving...'); } } const myCar = new Car('Grandeur', 'black'); Car.drive(); // Driving... myCar.drive(); // TypeError: myCar.drive is not a function
프로토타입 메서드와 정적 메서드의 차이점은 다음과 같다.
  • 프로토타입 메서드는 인스턴스로 호출하지만, 정적 메서드는 클래스로 호출한다.
  • 프로토타입 체인이 다르다. 따라서, 인스턴스로는 정적 메서드를 호출할 수 없다.

14-3-2. private 프로퍼티와 메서드

자바스크립트 클래스의 프로퍼티와 메서드는 기본적으로 클래스 외부에서 참조할 수 있다.
class Car { constructor(name, color) { this.name = name; this.color = color; } getInfo() { console.log(`Name: ${this.name}, Color: ${this.color}`); } } const myCar = new Car('Grandeur', 'black'); // 외부에서 인스턴스 프로퍼티 참조 console.log(myCar.name); // Grandeur console.log(myCar.color); // black // 외부에서 프로토타입 메서드 참조 myCar.getInfo(); // Name: Grandeur, Color: black // 외부에서 프로퍼티 값 수정 myCar.name = 'K9'; console.log(myCar.name); // K9 myCar.getInfo(); // Name: K9, Color: black
하지만 어떤 경우에는 클래스 내부 데이터를 외부에서 직접 참조하지 못하도록 막을 필요가 있다. 자바스크립트에서는 프로퍼티와 메서드 이름 앞에 #을 붙이면 private 프로퍼티와 메서드로 정의할 수 있다. 즉, 다른 클래스 기반 언어에서 제공하는 private 접근 제한자처럼 외부에서 접근하여 변경되지 못하도록 지정할 수 있다.
class Car { #name; color; constructor(name, color) { this.#name = name; this.color = color; } getName() { return this.#name; } setName(name) { this.#name = name; } } const myCar = new Car('Grandeur', 'black'); console.log(myCar.getName()); // Grandeur myCar.setName('K9'); // 외부에서 private 프로퍼티에 접근하여 수정할 수 없다. // SyntaxError: reference to undeclared private field or method #name // console.log(myCar.#name); // myCar.#name = 'K9'; console.log(myCar.getName()); // K9
이제 외부에서는 name 데이터에 접근할 수 없고, getName 메서드를 통해서만 간접적으로 접근할 수 있다.

14-4. 클래스의 상속과 확장

클래스 상속을 사용하면 기존 클래스를 확장하여 새로운 클래스를 정의할 수 있다.

14-4-1. extends 키워드

extends 키워드를 사용하면 클래스 상속을 구현할 수 있다.
// 부모(슈퍼) 클래스 class Animal { } // 자식(서브) 클래스 class Lion extends Animal { }
다음과 같이 클래스 선언문에서 새로 정의할 클래스 이름 뒤에 extends 키워드와 상속하는 클래스 이름을 명시하면 클래스를 상속받아 확장할 수 있다.
이때 상속하는 클래스를 부모 클래스(parent class), 또는 슈퍼 클래스(superclass)라고 하고, 상속받는 클래스를 자식 클래스(child class), 또는 서브 클래스(subclass)라고 한다.

14-4-2. super

super 함수를 호출하면 부모 클래스의 constructor를 호출하여, 부모 클래스의 프로퍼티를 상속받아 자식 클래스의 constructor에서도 사용할 수 있다. 자식 클래스에서 consturctor를 정의하면 반드시 super 함수를 호출해야 한다.
자식 클래스에서 constructor를 명시적으로 정의하지 않으면 암묵적으로 생성된 constructor 내부에 super 함수가 호출되어 부모 클래스의 프로퍼티를 상속받게 한다.
class Animal { constructor(name) { this.name = name; } run() { console.log(`${this.name} is running.`); } } class Lion extends Animal { constructor(name, age) { super(name); this.age = age; } } const babyLion = new Lion('Simba', 2); babyLion.run(); // Simba is running.
자식 클래스의 constructor에서 super 함수를 호출하기 전에 this를 참조할 수 없다.
class Animal { constructor(name) { this.name = name; } run() { console.log(`${this.name} is running.`); } } class Lion extends Animal { constructor(name, age) { // ReferenceError this.age = age; super(name); } }
super 함수는 자식 클래스에서만 호출할 수 있다.
class Tiger { constructor() { // SyntaxError: 'super' keyword unexpected here super(); } }