3.SW개발/Node.js

[JavaScript] 4편. 클래스(Class) & 프로토타입(Prototype) 이해하기

쿼드큐브 2025. 12. 6. 09:25
반응형
반응형

 

4편. 클래스(Class) & 프로토타입(Prototype) 이해하기

 

📚 목차
1. 클래스 기본 개념
2. 프로토타입 이해하기
3. 캡슐화: private 필드, getter/setter
4. static 필드/메서드 & 유틸 패턴
5. 상속과 확장(extends)
✔ 마무리

 

클래스&프로토타입 이해하기 삽화 이미지
클래스&프로토타입 이해하기 삽화 이미지

 

📂 [GitHub 실습 코드 보러가기] (https://github.com/cericube/nodejs-tutorials) /JavaScript

 

1. 클래스 기본 개념

자바스크립트의 클래스(Class)는 객체 지향 프로그래밍(OOP)의 핵심 개념으로, 특정 유형의 객체를 만들어 내기 위한 설계도 역할을 합니다.

ES6(ECMAScript 2015)에서 도입되었으며, 겉보기에는 전통적인 클래스 문법과 유사하지만, 실제로는 자바스크립트의 프로토타입(Prototype) 기반 구조 위에 만들어진 문법적 설탕(Syntactic Sugar)입니다.


즉, 보다 자연스럽고 이해하기 쉬운 방식으로 객체 지향 코드를 작성할 수 있도록 도와주는 친절한 문법이라고 이해하시면 됩니다.

 

🔷 class / constructor / instance란?

1) class - 객체 생성의 설계도
class는 동일한 구조를 가진 객체를 간편하게 생성하기 위한 틀입니다.
이 안에 속성(property)과 기능(method)을 정의하고, 이 설계도를 통해 여러 개의 객체를 만들어 사용할 수 있습니다.

2) constructor - 인스턴스를 초기화하는 특별한 메서드
클래스 안에서 constructor()는 인스턴스가 생성될 때 자동으로 호출되며, 전달받은 값을 바탕으로 객체의 초기 상태를 설정해 줍니다.

다른 언어에서의 생성자(constructor) 개념과 거의 동일합니다.

3) instance - 클래스 설계도로부터 만들어진 실제 객체
new 키워드를 사용해 클래스를 호출하면 클래스가 정의한 구조 그대로 값을 가진 실제 객체(인스턴스)가 생성됩니다.

 

✔️ 예제 - 클래스 정의와 인스턴스 생성

// 클래스 정의
class Person {
  // constructor: 인스턴스가 생성될 때 가장 먼저 호출됩니다.
  constructor(name, age) {
    this.name = name; // 인스턴스 고유의 속성
    this.age = age;
  }
}

// 인스턴스 생성
const person1 = new Person('김철수', 30);
const person2 = new Person('이영희', 25);

console.log(person1.name); // '김철수'
console.log(person2.age);  // 25

위 예시에서 person1과 person2는 동일한 Person 클래스를 기반으로 생성되었지만, 각자 자신만의 name, age 값을 갖는 독립적인 객체입니다.

 

🔷 메서드 추가와 prototype 저장 구조

클래스 내부에 정의된 메서드는 인스턴스마다 따로 생성되지 않고, 클래스의 prototype 객체에 한 번만 저장됩니다.

따라서 클래스로부터 여러 인스턴스를 만들더라도, 모든 인스턴스는 동일한 prototype의 메서드를 함께 사용하게 됩니다.

이는 불필요한 메서드 복사를 방지하여 메모리를 효율적으로 사용하는 방식입니다.


쉽게 말해, 인스턴스는 “내 안에 메서드를 직접 들고 있는 것”이 아니라, 필요할 때 prototype이라는 공유 창고에서 같은 메서드를 꺼내 쓰는 구조라고 이해하시면 좋습니다.

 

✔️ 예시 - ES6 클래스 버전

아래 코드를 보면 클래스 내부에 sayHello, getAgeInTenYears 메서드를 정의했지만, 실제로는 두 메서드 모두 Person.prototype에 저장됩니다.

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

  // sayHello 메서드는 Person.prototype에 저장됩니다.
  sayHello() {
    console.log(`안녕하세요, 저는 ${this.name}입니다.`);
  }

  // getAgeInTenYears 역시 prototype에 저장됩니다.
  getAgeInTenYears() {
    return this.age + 10;
  }
}

const person3 = new Person('박민수', 40);
const person4 = new Person('최지민', 35);

person3.sayHello(); // 안녕하세요, 저는 박민수입니다.

// 두 인스턴스는 동일한 메서드를 공유합니다.
console.log(person3.sayHello === person4.sayHello); // true

// prototype 저장 위치 확인
console.log(Person.prototype.sayHello === person3.sayHello); // true

위 결과를 보면, person3과 person4가 서로 다른 객체임에도 sayHello는 두 인스턴스 각각에 따로 만들어진 것이 아니라,
Person.prototype.sayHello 하나를 함께 참조하고 있다는 점을 확인할 수 있습니다.

 

✔️ Prototype 방식으로 동일 구조 표현하기

앞서 보신 클래스 버전은 실제로 prototype 기반 구조를 읽기 쉽도록 감싼 문법일 뿐입니다.

그렇다면 같은 기능을, 클래스 문법 없이 순수 prototype 방식으로 작성하면 어떻게 될까요?


아래 예시는 ES6 클래스의 내부 동작을 그대로 드러낸 형태의 구현입니다.

// 생성자 함수(= constructor 역할)
function Person(name, age) {
  this.name = name;
  this.age = age;
  
  // 1. this를 사용하여 메서드를 인스턴스에 직접 추가 (메모리 비효율적)
  this.getAgeInTenYears = function () {
    return this.age + 10;
  }
}

// 2. 메서드는 prototype에 저장하여 모든 인스턴스가 공유
Person.prototype.sayHello = function () {
  // 여기서 this는 인스턴스를 가리킵니다.
  console.log(`안녕하세요, 저는 ${this.name}입니다.`);
};

▸ new Person()으로 객체를 생성할 때마다 this.getAgeInTenYears로 함수를 정의하면, 생성되는 모든 인스턴스(객체)가 이 함수를 각각 독립적으로 복사하여 가지게 됩니다. 이는 메모리 측면에서 비효율적입니다.
▸ JavaScript에서는 함수를 프로토타입(Prototype)에 정의하여 모든 인스턴스가 하나의 함수를 공유하도록 하는 것이 표준적이고 효율적입니다.

 

이처럼 자바스크립트의 클래스 문법은 내부적으로는 잘 정리된 형식일 뿐, 핵심 동작 원리는 prototype 공유 구조에 기반하고 있다는 점이 변하지 않습니다.

 

2. 프로토타입 이해하기

자바스크립트의 프로토타입(Prototype)은 객체 간에 속성과 메서드를 공유하고 상속하기 위해 사용되는 핵심 메커니즘입니다.

자바스크립트에서 만들어지는 모든 객체는 자신보다 상위에 존재하는 또 다른 객체를 가리키는 숨겨진 내부 연결(링크)을 가지고 있으며, 이 연결이 가리키는 객체를 바로 “프로토타입”이라고 부릅니다.

function Item(id) {
  this.id = id;
}

// 1. 메서드 추가 (가장 일반적인 사용)
Item.prototype.getDescription = function() {
  return `ID: ${this.id}, Type: ${this.type}`;
};

// 2. 일반 속성(데이터) 추가
Item.prototype.type = 'General'; // 모든 Item 객체가 공유할 기본값

const item1 = new Item(101);
const item2 = new Item(102);

// item1과 item2 모두 prototype에서 type 속성과 getDescription 메서드를 상속받아 사용
console.log(item1.type);        // 출력: General
console.log(item2.getDescription()); // 출력: ID: 102, Type: General

 

🔷 prototype과 [[Prototype]] 의 정확한 의미

자바스크립트에서는 ‘프로토타입’이라는 말이 문맥에 따라 두 가지 다른 개념을 가리킬 수 있습니다.

이 둘을 명확히 구분하면 프로토타입 구조를 훨씬 쉽게 이해할 수 있습니다.

 

1) prototype (속성, 메소드)

▸ 대상: “함수(특히 생성자 함수 또는 클래스)”가 가지는 속성
▸ 내용: 생성된 모든 인스턴스가 공유할 메서드와 속성이 이곳에 저장됩니다.
▸ Person.prototype, Array.prototype과 같은 형태로 접근 가능
▸ 클래스 문법에서는 클래스 안에 작성한 메서드들이 자동으로 prototype에 배치됩니다.

▸ 즉, prototype은 자식 객체들에게 "이걸 같이 써라" 하고 물려줄 공유 메서드와 속성들의 집합소입니다.

 

2) [[Prototype]] (내부 슬롯, 숨겨진 링크)

▸ 대상: 모든 객체가 기본적으로 가지는 내부 슬롯
▸ 역할: “나의 부모 역할을 하는 프로토타입 객체”를 가리키는 참조

▸ 코드에서는 직접 접근할 수 없지만
- 비표준 속성 __proto__
- 표준 메서드 Object.getPrototypeOf() 를 사용해 확인할 수 있습니다.

▸ 즉, [[Prototype]]은 객체가 자신의 부모(prototype 객체)에게 연결되어 속성을 빌려 쓸 수 있게 해주는 "상속 링크"입니다.

 

정리하면,

prototype  → 클래스(또는 생성자 함수)가 가진 ‘메서드 저장소’
[[Prototype]] → 인스턴스가 가진 ‘부모 객체에 대한 링크’

라는 차이가 있습니다.

 

🔷 프로토타입 체인(Prototype Chain)의 동작 방식

자바스크립트에서 객체의 속성이나 메서드를 읽을 때는 다음과 같은 순서로 탐색이 이루어집니다.

 

1. 당신이 new Person('Alice', 30)으로 새로운 객체 alice를 만듭니다.
2. alice 객체는 [[Prototype]] 이라는 숨겨진 링크를 가지게 되는데, 이 링크는 자동으로 Person.prototype 객체를 가리킵니다.
3. 당신이 alice.sayHello()를 호출했는데, alice 객체 자체에 이 메서드가 없습니다.
4. 자바스크립트는 alice의 [[Prototype]] 링크를 따라 Person.prototype 객체로 이동합니다.
5. Person.prototype에서 이 메서드를 발견하고 실행합니다.

이러한 [[Prototype]] 링크를 따라 부모-조상 객체로 계속 연결되어 올라가는 구조를 프로토타입 체인 (Prototype Chain)이라고 부릅니다.

class Person {
  // ... constructor 생략 ...
  sayHello() {
    console.log(`Hello, ${this.name}`);
  }
}

const person1 = new Person('Minho', 30);

// 1. person1 자체에는 sayHello가 없습니다.
console.log(Object.hasOwn(person1, 'sayHello')); 
// false

// 2. 메서드를 찾지 못하면 프로토타입(Person.prototype)에서 찾아 실행합니다.
person1.sayHello(); 
// 'Hello, Minho'

// 3. toString()은 person1에도, Person.prototype에도 없습니다.
//    다음 프로토타입인 Object.prototype에서 찾아 실행합니다.
console.log(person1.toString()); 
// [object Object]

// Object.prototype이 toString을 '직접' 가지고 있는지 확인
console.log(Object.hasOwn(Object.prototype, 'toString')); 
// true

 

이 동작을 체인으로 그리면 다음과 같습니다:

person1
  ↓ [[Prototype]]
Person.prototype (sayHello)
  ↓ [[Prototype]]
Object.prototype (toString)
  ↓ [[Prototype]]
null

 

🔷 __proto__ vs prototype 차이 정리

구분 __proto__ prototype
누가 가지는가 모든 객체 인스턴스 생성자 함수 / 클래스
역할 인스턴스가 ‘부모 프로토타입’을 가리키는 링크 인스턴스가 공유할 메서드(속성)를 담아두는 저장소
표준 여부 비표준(호환성은 있으나 권장되지 않음) 표준
대체 API Object.getPrototypeOf() 없음 (정식 속성)
class Parent {}
class Child extends Parent {}

const instance = new Child();

// 1. prototype: 클래스 자체에 있는 속성 (공유할 메서드 저장소)
console.log(Child.prototype); // { constructor: ... }

// 2. __proto__ 또는 Object.getPrototypeOf(): 인스턴스에서 부모를 참조
console.log(instance.__proto__ === Child.prototype); // true
console.log(Object.getPrototypeOf(instance) === Child.prototype); // true

▸ prototype은 클래스(또는 생성자 함수)가 “이 인스턴스들이 공유할 기능은 여기에 넣어!”라고 지정해 둔 저장소이고,
▸ proto / [[Prototype]]은 “현재 인스턴스가 자신의 부모를 가리키는 링크”라고 이해하면 됩니다.

반응형

 

3. 캡슐화: private 필드, getter/setter

객체지향 프로그래밍(OOP)에서 캡슐화(Encapsulation)란 객체의 속성이나 동작을 외부로부터 보호하여, 객체 내부의 상태가 임의로 변경되는 것을 방지하는 중요한 개념입니다.

즉, 객체는 스스로가 허용한 방식(getter/setter)을 통해서만 안전하게 접근되도록 제어하는 것이라고 이해할 수 있습니다.


이러한 캡슐화는 데이터의 무결성, 예측 가능한 동작, 유지보수성 향상에 매우 큰 도움을 줍니다.

 

🔷 #field, #method - Private 필드와 Private 메서드

ES2020 이후 자바스크립트에서는 # 기호를 붙여 클래스 내부에서만 접근 가능한 Private 필드와 메서드를 공식적으로 지원합니다.
이 방식은 클래스 외부에서의 직접 수정이나 접근을 원천적으로 차단하여, 매우 강력한 캡슐화를 제공합니다.


아래 예시는 Private 필드를 사용하여 계좌 잔액(balance)을 외부에서 직접 바꾸지 못하도록 설계한 사례입니다.

 

✔️ 예시: Private 필드/메서드를 이용한 안전한 계좌 관리

class UserAccount {
  #balance = 0; // Private 필드: 외부에서 직접 접근/수정 불가

  constructor(initialDeposit) {
    this.#deposit(initialDeposit); // 내부 메서드 호출은 가능
  }

  // Private 메서드
  #deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
      console.log(`입금: ${amount}, 잔액: ${this.#balance}`);
    } else {
      console.log('0원 이하는 입금할 수 없습니다.');
    }
  }

  // Public 메서드를 통해 private 필드에 간접적으로 접근
  getBalance() {
    return this.#balance;
  }
}

const account = new UserAccount(1000); // 입금: 1000, 잔액: 1000

// SyntaxError: Private field '#balance' must be declared in an enclosing class
// account.#balance = 100000; 
// SyntaxError: Private field '#deposit' must be declared in an enclosing class
// account.#deposit(500);

console.log(account.getBalance()); // 1000

위 코드에서 확인할 수 있듯이

▸ #balance와 #deposit()은 클래스 외부에서 접근이 절대 불가능합니다.

▸ 따라서 외부에서 실수로 값을 바꾸거나 잘못된 로직으로 호출하는 문제를 원천적으로 예방할 수 있습니다.

▸ 대신 개발자가 허용한 메서드(getBalance)를 통해서만 필요한 정보를 안전하게 가져올 수 있습니다.

 

🔷 Getter / Setter - 안전한 간접 접근 방식

캡슐화를 강화하기 위해 클래스에서는 Getter와 Setter를 활용할 수 있습니다

▸ Getter(get)
속성 값을 읽을 때 자동으로 호출되어 “읽기 전용 로직”이나 “가공된 결과”를 제공할 수 있습니다.

▸ Setter(set)
값을 설정할 때 호출되며, 이 과정에서 “유효성 검사”나 “추가 연산”을 적용할 수 있습니다.

 

Getter/Setter를 사용하면 외부에서 보기에는 “일반 속성처럼 동작”하지만, 내부적으로는 클래스가 의도한 규칙에 따라 안전하게 데이터를 처리할 수 있습니다.

 

✔️ 예시: Getter/Setter로 가격 검증 및 할인 로직 적용

class Product {
  #price; // Private 필드로 실제 가격을 저장

  constructor(price) {
    this.price = price; // Setter를 호출
  }

  // Getter: 가격을 읽을 때 10% 할인을 적용하여 반환
  get price() {
    return this.#price * 0.9;
  }

  // Setter: 가격을 설정할 때 유효성 검사 수행
  set price(newPrice) {
    if (newPrice > 0) {
      this.#price = newPrice;
    } else {
      console.error('가격은 0보다 커야 합니다.');
    }
  }
}

const item = new Product(10000);
console.log(`실제 가격 (할인 전): ${item.#price}`); // 에러! (외부 접근 불가)
console.log(`판매 가격 (할인 후): ${item.price}`); // Getter 호출 -> 9000

item.price = -500;   // Setter 호출 -> 가격은 0보다 커야 합니다.
item.price = 15000;  // Setter 호출 -> 정상 설정

console.log(`새 판매 가격: ${item.price}`); // Getter 호출 -> 13500

이 예시에서 중요한 점은 다음과 같습니다:
▸ #price는 반드시 Setter를 통해서만 설정되므로 잘못된 값은 차단됩니다.
▸ Getter는 가격을 읽을 때 일정한 규칙(예: 10% 할인)을 적용하므로 항상 일관된 방식으로 외부에 값이 전달됩니다.
▸ 내부 구현이 바뀌어도, 외부에서는 여전히 item.price라는 동일한 인터페이스로 접근할 수 있어 유지보수성이 좋아집니다.

 

🔷 은닉 처리의 장점과 불변성 패턴

캡슐화를 통해 값을 숨기고 직접 접근을 제한하는 것은 여러 측면에서 도움이 됩니다.

 

🔸 은닉 처리의 주요 장점
▸ 데이터 보호 : 내부 상태가 무분별하게 변경되는 것을 방지하여 버그 가능성을 줄입니다.
▸ 유효성 검사 : Setter를 통해 값이 올바른지 검사할 수 있습니다.
▸ 유연한 내부 변경 : Private 필드는 외부에서 볼 수 없으므로, 내부 로직을 자유롭게 변경해도 외부 인터페이스는 그대로 유지됩니다. (사용자 코드가 깨지지 않음)

 

🔸 불변성(immutability) 패턴

불변성 패턴은 객체가 생성된 이후 내부 상태가 외부에 의해 변경되지 않도록 하는 기법입니다.
Private 필드를 사용하거나, Setter를 제공하지 않거나, Getter에서 원본이 아닌 복사본을 제공하는 방식을 통해 구현할 수 있습니다.

 

✔️ 예시: Getter가 깊은 복사본을 반환하여 불변성 유지

// 불변성을 위한 Getter 예시
class Config {
  #settings = { theme: 'dark', fontSize: 16 };

  // Getter를 통해 settings 객체의 복사본(Deep Copy)을 반환하여
  // 외부에서 원본 #settings를 직접 수정하는 것을 방지합니다.
  get settings() {
    return JSON.parse(JSON.stringify(this.#settings)); // 깊은 복사
  }
}

const config = new Config();
const userSettings = config.settings; // 복사본을 받음

userSettings.theme = 'light'; // 복사본 수정

console.log(config.settings.theme); // 원본은 'dark'로 유지됨 (불변성)

이러한 방식은 외부 코드가 무심코 내부 상태를 바꿔버리는 실수를 원천적으로 차단할 수 있어, 안전한 데이터 관리가 필요한 환경에서 매우 유용합니다.

 

4. static 필드/메서드 & 유틸 패턴

자바스크립트 클래스에서 static 키워드는 인스턴스가 아닌 클래스 자체에 속하는 멤버(필드와 메서드)를 정의할 때 사용됩니다.

일반적으로 인스턴스마다 필요한 데이터는 생성자를 통해 설정하지만, static 멤버는 클래스 전체가 공유해야 하는 정보나 유틸리티 기능을 담을 때 특히 유용합니다.

 

🔷 static이 필요한 이유

자바스크립트에서 클래스를 사용할 때, 모든 속성과 기능이 인스턴스에 귀속될 필요는 없습니다.

예를 들어, “객체가 몇 번 생성되었는지” 같은 정보는 개별 인스턴스가 아니라 클래스 차원에서 관리되는 것이 더 자연스럽습니다.

또한 계산 관련 보조 함수처럼, 특정 인스턴스의 상태에 의존하지 않는 기능을 제공할 때도 굳이 인스턴스를 만들 필요 없이 클래스 이름을 통해 직접 호출할 수 있으면 효율적입니다.

 

따라서 static 멤버는 다음과 같은 목적에 자주 사용됩니다:

🔸Static 필드
▸ 클래스 전체가 공유해야 하는 값
▸ 인스턴스 개수 카운트
▸ 재사용 가능한 상수
▸ 설정값, 공용 리소스 등

🔸Static 메서드
▸ 인스턴스 상태에 의존하지 않는 유틸 함수(Utility Function)
▸ 인스턴스를 만들지 않고도 호출 가능한 기능
▸ 팩토리 패턴, 싱글턴 패턴과 같은 디자인 패턴 구현 시 주로 사용

 

✔️ 예시 - static 필드 & static 메서드

아래 예제에서는 클래스 레벨의 상수와, 인스턴스 생성 횟수를 기록하는 static 필드를 정의하고 있습니다.

또한 덧셈을 수행하는 간단한 유틸리티 메서드도 static으로 제공하고 있습니다.

class Calculator {
  // Static 필드: 클래스 전체가 공유하는 상수
  static PI = 3.141592;

  // Static 필드: 모든 인스턴스가 공유할 값 (예: 생성된 인스턴스 수)
  static #count = 0; 
  
  constructor() {
    // 생성자에서 static 필드에 접근: 클래스명.필드명
    Calculator.#count++; 
  }

  // Static 메서드: 인스턴스 생성 없이 클래스 이름을 통해 호출
  static add(a, b) {
    return a + b;
  }
  
  static getCount() {
    return Calculator.#count;
  }
}

// 인스턴스 생성 없이 Static 메서드 및 필드 사용
console.log(Calculator.PI); // 3.141592
console.log(Calculator.add(5, 3)); // 8

// 인스턴스는 static 멤버에 직접 접근할 수 없습니다.
// console.log(new Calculator().PI); // undefined

const calc1 = new Calculator();
const calc2 = new Calculator();
console.log(`생성된 인스턴스 수: ${Calculator.getCount()}`); // 2

여기서 중요한 점은 다음과 같습니다.
▸ Calculator.PI나 Calculator.add()처럼 클래스 이름을 통해 직접 접근한다.
▸ new Calculator()를 통해 만든 인스턴스는 static 멤버를 사용할 수 없다.
▸ #count와 같이 private static 필드는 외부에서 직접 접근할 수 없고, static 메서드를 통해서만 사용 가능하다.

 

🔷 static을 활용한 디자인 패턴

static 멤버는 단순 유틸리티 기능 외에도, 객체 생성과 인스턴스 관리에 관련된 디자인 패턴 구현에 매우 효과적입니다.

그중 대표적으로 많이 사용되는 패턴이 싱글턴 패턴과 팩토리 패턴입니다.

 

✔️ 싱글턴 패턴 (Singleton Pattern)

싱글턴 패턴은 클래스의 인스턴스를 단 하나만 생성하도록 보장하는 패턴입니다.

어떤 설정이나 리소스가 전체 프로그램에서 단일하게 유지되어야 할 때 자주 사용되며, static 메서드를 통해 단 하나의 인스턴스를 관리합니다.

class ConfigManager {
  static #instance = null;
  
  // 외부에서 new를 통한 직접 생성을 막기 위해 생성자를 private 처리 (권장)
  // 자바스크립트는 기본적으로 private 생성자가 없으므로, 대신 내부에서만 사용하도록 가이드합니다.
  constructor() {
    if (ConfigManager.#instance) {
      // 이미 인스턴스가 있으면 에러 발생 또는 기존 인스턴스를 반환할 수도 있습니다.
      throw new Error("ConfigManager는 싱글톤입니다. getInstance()를 사용하세요.");
    }
  }

  // Static 메서드: 단일 인스턴스를 반환
  static getInstance() {
    if (ConfigManager.#instance === null) {
      ConfigManager.#instance = new ConfigManager();
      ConfigManager.#instance.settings = { logLevel: 'info', port: 3000 };
    }
    return ConfigManager.#instance;
  }
}

// const manager1 = new ConfigManager(); // 에러 발생 (싱글톤 위반 시도)

const manager1 = ConfigManager.getInstance();
const manager2 = ConfigManager.getInstance();

console.log(manager1 === manager2); // true (동일한 단일 인스턴스 참조)
console.log(manager1.settings.logLevel); // 'info'

여기서 getInstance()는 static 메서드이기 때문에, 인스턴스를 만들지 않아도 ConfigManager.getInstance()를 통해 단일 인스턴스를 안전하게 가져올 수 있습니다.

 

✔️ 팩토리 패턴 (Factory Pattern)

팩토리 패턴은 객체 생성 로직을 한곳에 모아두고, 조건에 따라 적절한 객체를 생성해 반환하는 패턴입니다.

패턴 역시 “여러 인스턴스를 일관된 방식으로 생성해야 하는 상황”에서 매우 유용합니다.
static 메서드를 사용하면 인스턴스를 생성하기 위한 공용 로직을 깔끔하게 분리할 수 있습니다.

class Product {
  constructor(type, price) {
    this.type = type;
    this.price = price;
  }
}

class ProductFactory {
  // Static 메서드: 특정 조건에 따라 Product 인스턴스를 생성
  static create(productType) {
    const basePrice = 10000;
    
    switch (productType) {
      case 'Premium':
        return new Product(productType, basePrice * 1.5); // 50% 할증
      case 'Standard':
        return new Product(productType, basePrice);
      case 'Discount':
        return new Product(productType, basePrice * 0.8); // 20% 할인
      default:
        throw new Error('유효하지 않은 상품 타입입니다.');
    }
  }
}

const premiumProduct = ProductFactory.create('Premium');
const standardProduct = ProductFactory.create('Standard');

console.log(premiumProduct); // Product { type: 'Premium', price: 15000 }
console.log(standardProduct); // Product { type: 'Standard', price: 10000 }

이처럼 static 메서드는 “객체를 생성하는 역할을 클래스가 대신 수행하도록” 만들어 주기 때문에 복잡한 생성 로직을 관리해야 하는 실무 상황에서 큰 장점을 제공합니다.

 

5. 상속과 확장 (extends)

객체지향 프로그래밍(OOP)에서 상속(Inheritance)은 매우 중요한 개념으로, 자식 클래스(파생 클래스)가 부모 클래스(기반 클래스)의 속성과 메서드를 물려받아 그대로 활용하거나, 필요에 따라 확장할 수 있도록 해 줍니다.

 

자바스크립트에서는 이 상속 기능을 extends 키워드로 간단히 구현할 수 있으며, 이를 통해 코드 재사용성, 유지보수성, 확장성을 자연스럽게 향상시킬 수 있습니다.

 

✔️ super 키워드의 역할

상속 구조에서 super는 부모 클래스에 접근하기 위한 중요한 키워드입니다.
특히 다음 두 상황에서 자주 사용됩니다.

 

1) super() - 부모 생성자 호출
자식 클래스의 constructor 내부에서 부모 클래스의 생성자를 호출합니다.

이는 자식 클래스가 this를 사용하기 전에 반드시 호출되어야 하는 필수 동작입니다.

2) super.methodName() - 부모 메서드 호출
자식 클래스가 부모 클래스의 메서드를 오버라이딩(재정의) 하더라도, 필요하면 부모의 원래 메서드를 호출해야 하는 경우가 있습니다.

이때 super.methodName()을 사용하면 부모의 기능을 바탕으로 자식 클래스만의 기능을 덧붙일 수 있습니다.

 

✔️ 예시 - Animal → Rabbit 상속 구조

아래 코드는 부모 클래스 Animal을 정의하고, 이를 Rabbit 클래스에서 extends를 통해 확장하는 전형적인 상속 패턴을 보여줍니다.

// 1. 부모 클래스 정의
class Animal {
  constructor(name) {
    this.name = name;
    this.speed = 0;
  }

  move(speed) {
    this.speed = speed;
    console.log(`${this.name}이/가 ${this.speed}km/h로 움직입니다.`);
  }

  stop() {
    this.speed = 0;
    console.log(`${this.name}이/가 멈춥니다.`);
  }
}

// 2. 자식 클래스 정의 및 상속
class Rabbit extends Animal {
  constructor(name, earLength) {
    // 1) super() 호출: 부모(Animal)의 constructor를 먼저 호출하여 초기화
    super(name); 
    this.earLength = earLength; // 자식 클래스만의 새로운 속성
  }

  hide() {
    console.log(`${this.name}이/가 숨었습니다.`);
  }

  // move 메서드 오버라이딩 (재정의)
  move(speed) {
    // 2) 부모의 move 메서드 실행
    super.move(speed); 
    console.log(`(움직이는 중... 귀 길이: ${this.earLength}cm)`);
  }
}

const rabbit = new Rabbit('토순이', 15);
rabbit.move(10); 
// 출력:
// 토순이이/가 10km/h로 움직입니다. (부모 메서드)
// (움직이는 중... 귀 길이: 15cm) (자식 클래스 확장 로직)

rabbit.stop(); // 부모 클래스 메서드를 그대로 사용 (오버라이딩 X)

▸ Rabbit은 Animal의 속성과 메서드를 자연스럽게 물려받는다.
▸ 오버라이딩한 메서드 안에서도 super를 통해 부모 메서드를 재활용할 수 있다.
▸ 필요한 경우 자식 클래스만의 고유 속성을 자유롭게 추가할 수 있다.

 

✔️ 프로토타입 기반 상속 구조

겉으로 보기에는 클래스 기반 상속처럼 보이지만, 자바스크립트의 상속은 근본적으로 프로토타입 체인(Prototype Chain)을 기반으로 작동합니다.

extends 키워드는 이러한 프로토타입 연결을 두 가지 차원에서 자동 설정합니다.

 

1) 인스턴스 메서드 상속

Rabbit.prototype.__proto__ === Animal.prototype  // true

이는 Rabbit 인스턴스가 Animal.prototype의 메서드를 그대로 사용할 수 있는 구조를 의미합니다.
즉, rabbit.move()는 먼저 Rabbit.prototype.move를 확인하고, 없다면 그다음 Animal.prototype.move를 찾아 실행합니다.

 

2) Static(정적) 상속

Rabbit.__proto__ === Animal  // true

클래스 함수 자체도 프로토타입 체인을 통해 연결되기 때문에 부모의 정적(static) 메서드나 속성 역시 자식 클래스에서 사용할 수 있습니다.

 

3) 구조 확인 예시

// 위 Rabbit extends Animal 예시에 기반
console.log(Rabbit.prototype.__proto__ === Animal.prototype); // true
console.log(Rabbit.__proto__ === Animal); // true

이 두 연결이 바로 자바스크립트 상속의 핵심이며, 클래스 문법은 이 구조를 보다 명확하고 직관적으로 사용할 수 있게 도와줄 뿐입니다.

 

✔️ 예제 - 커스텀 Error 클래스 상속

실무에서는 상황에 맞는 에러를 세분화해 처리해야 하는 경우가 많습니다.
이때 기본 Error 클래스를 상속하면, 자신만의 의미 있는 정보를 가진 커스텀 에러(Custom Error)를 정의할 수 있습니다.


아래 예제에서는 잘못된 입력값이 들어왔을 때 어떤 필드에서 문제가 발생했는지 명확히 전달할 수 있는 커스텀 에러를 구현합니다.

// 기본 Error 클래스 상속
class InvalidInputError extends Error {
  constructor(message, fieldName) {
    super(message); // 부모(Error) 생성자 호출
    this.name = 'InvalidInputError'; // 에러 이름 정의
    this.fieldName = fieldName; // 커스텀 정보 추가
  }
}

function validateForm(value) {
  if (value === '') {
    // 커스텀 에러 발생
    throw new InvalidInputError('필수 입력값이 누락되었습니다.', 'username');
  }
  return true;
}

try {
  validateForm('');
} catch (error) {
  if (error instanceof InvalidInputError) {
    console.error(`커스텀 에러 발생: [${error.name}] ${error.message}`);
    console.error(`누락 필드: ${error.fieldName}`); // 커스텀 필드 사용
  } else {
    console.error('알 수 없는 오류:', error);
  }
}

// 출력:
// 커스텀 에러 발생: [InvalidInputError] 필수 입력값이 누락되었습니다.
// 누락 필드: username

위 패턴은 실무에서 매우 널리 사용되며, 폼 유효성 검증, API 응답 처리, 비즈니스 로직 예외 처리 등 다양한 곳에서 활용할 수 있습니다.

 

6. Mixins

믹스인은 단순히 재사용 가능한 메서드 묶음을 객체 형태로 정의한 뒤, 이를 다른 클래스의 prototype에 복사하여 기능을 주입하는 방식입니다.


즉, “상속받는다”라기보다는 필요한 기능만 적절한 클래스에 ‘섞어 넣는다(mix in)’라고 이해하는 것이 자연스럽습니다.


이 접근 방식은 클래스 계층을 인위적으로 복잡하게 만들지 않으면서도, 여러 클래스가 공통으로 필요한 기능을 깔끔하게 공유할 수 있다는 장점이 있습니다.

 

✔️ 믹스인 사용 방식

믹스인은 대부분 아래의 순서로 구현됩니다:
1. 재사용 가능한 기능들을 포함한 객체를 정의한다.
(여기에는 인스턴스 메서드로 동작할 함수들이 들어 있습니다.)
2. Object.assign()을 사용해 이 객체의 메서드를 특정 클래스의 prototype에 복사한다.
3. 그러면 해당 클래스의 모든 인스턴스는 믹스인 메서드를 사용할 수 있게 됩니다.

(상속은 아니지만, prototype에 동일한 함수가 주입되므로 결과적으로 공유됩니다.)

 

✔️ 예시 -믹스인 패턴 적용

// 1. 믹스인 역할을 할 객체 정의 (재사용 가능한 메서드 집합)
const logMixin = {
  // 인스턴스 메서드로 동작할 함수
  log(message) {
    console.log(`[${this.name}] ${message}`);
  },
  
  // 인스턴스 메서드로 동작할 함수
  logError(message) {
    console.error(`[${this.name}] ERROR: ${message}`);
  }
};

class User {
  constructor(name) {
    this.name = name;
  }
}

class ProductItem {
  constructor(name) {
    this.name = name;
  }
}

// 2. Object.assign()을 사용하여 믹스인 메서드를 클래스의 prototype에 복사
Object.assign(User.prototype, logMixin);
Object.assign(ProductItem.prototype, logMixin);

// 3. 이제 User와 ProductItem의 인스턴스는 logMixin의 메서드를 가집니다.
const user = new User('Alice');
const item = new ProductItem('Laptop');

user.log('로그인 성공'); // [Alice] 로그인 성공
item.logError('재고 부족'); // [Laptop] ERROR: 재고 부족

// User는 logMixin의 메서드를 상속받지 않고 '복사'해서 사용합니다.
console.log(user.log === item.log); // true (대부분의 경우 복사된 함수가 동일한 참조를 가집니다.)

 

✔️ 믹스인의 장점

▸ 클래스 구조를 복잡하게 만들지 않습니다.
상속 계층을 늘리거나 부모 클래스를 억지로 묶어둘 필요가 없습니다.
▸ 필요한 기능만 선택적으로 추가할 수 있습니다.
클래스가 다른 기능을 공유해야 할 때도 유연하게 조합할 수 있습니다.
▸ 유틸리티성 기능을 주입할 때 특히 유용합니다.
예를 들어 로깅, 이벤트 관련 기능, 포맷팅 기능 등 다양한 상황에서 깔끔하게 재사용할 수 있습니다.

 

 

✔ 마무리

자바스크립트의 클래스와 프로토타입 시스템은 단순한 문법 요소를 넘어, 객체 지향 패턴을 유연하게 구현할 수 있도록 설계된 강력한 기반입니다.

클래스 문법은 우리가 익숙하게 알고 있는 OOP 방식처럼 보이지만, 그 안에는 프로토타입 체인이라는 자바스크립트만의 독특하고 직관적인 구조가 자리하고 있습니다.


캡슐화, 상속, 정적 멤버, 그리고 믹스인과 같은 패턴들은 필요한 기능을 안전하게 보호하면서도 확장성과 재사용성을 극대화할 수 있는 도구들입니다.


이러한 개념들을 정확히 이해하고 활용하면 애플리케이션의 구조는 더욱 견고해지고, 유지보수성과 확장성 역시 크게 향상됩니다.

 


※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.

반응형

 

반응형