JavaScript 객체 생성 패턴: 팩토리 함수부터 프로토타입까지

1. 팩토리 함수 방식

객체를 일관성 있게 생성하기 위해 팩토리 함수를 사용할 수 있다. 이 방식은 내부적으로 객체를 생성하고 속성을 부여한 뒤 반환한다.

function createUser(username, userAge) {
    let obj = new Object();
    obj.username = username;
    obj.userAge = userAge;
    obj.introduce = function() {
        console.log(`안녕, 나는 ${obj.username}이야.`);
    };
    return obj;
}

let user1 = createUser('홍길동', 25);
user1.introduce();

let user2 = createUser('김철수', 30);
user2.introduce();

이 방법의 장점은 유사한 구조의 객체를 반복해서 만들 수 있다는 점이다. 그러나 생성된 인스턴스가 어떤 타입인지 식별할 수 없다는 단점이 있다.

2. 생성자 함수 방식

생성자 함수는 new 키워드와 함께 호출되어 객체를 생성한다. 일반 함수와 동일한 문법을 가지지만, 호출 방식에 따라 차이가 생긴다.

function User(fullName, years) {
    this.fullName = fullName;
    this.years = years;
    this.greet = function() {
        console.log(`반가워, 난 ${this.fullName}야.`);
    };
}

생성자 함수는 명시적으로 객체를 생성하거나 반환하지 않으며, 속성을 this에 직접 할당한다.

let person1 = new User('박민수', 22);
person1.greet(); // 반가워, 난 박민수야.

let person2 = new User('이지영', 27);
person2.greet(); // 반가워, 난 이지영야.

생성 과정은 다음과 같은 단계로 이루어진다:

  • 새로운 객체 생성
  • 생성자의 스코프를 새 객체에 바인딩 (this가 새 객체를 가리킴)
  • 함수 내부 로직 실행
  • 새 객체 반환

생성자 함수는 일반 함수처럼 호출될 수도 있지만, 이 경우 this는 전역 객체(window)를 참조하게 된다.

User('최현우', 20);
window.greet(); // 반가워, 난 최현우야.

생성자로 만든 인스턴스는 해당 생성자뿐 아니라 Object의 인스턴스이기도 하다.

console.log(person1 instanceof Object); // true
console.log(person1 instanceof User);   // true

단점으로는 메서드가 각 인스턴스마다 별도로 생성되어 공유되지 않는다는 점이다.

console.log(person1.greet === person2.greet); // false

3. 프로토타입 기반 생성

모든 함수는 prototype 속성을 가지며, 이를 통해 인스턴스 간 공통 속성과 메서드를 공유할 수 있다.

function Student() {}

Student.prototype = {
    constructor: Student,
    studentName: '기본 이름',
    studentAge: 20,
    getInfo: function() {
        console.log(`${this.studentName}, ${this.studentAge}살`);
    }
};

let s1 = new Student();
let s2 = new Student();

console.log(s1.getInfo === s2.getInfo); // true
s1.getInfo(); // 기본 이름, 20살

인스턴스들은 프로토타입의 속성과 메서드를 공유하므로 메모리 효율적이다.

isPrototypeOf() 메서드는 프로토타입과 인스턴스 간 관계를 확인할 때 사용된다.

console.log(Student.prototype.isPrototypeOf(s1)); // true

인스턴스에 프로토타입과 동일한 이름의 속성을 추가하면, 해당 인스턴스에서만 새로운 속성이 생성되며 프로토타입 값은 가려지게 된다.

s1.studentName = '새이름';
console.log(s1.studentName); // 새이름
console.log(s2.studentName); // 기본 이름

해당 속성을 삭제하면 다시 프로토타입 값을 참조하게 된다.

delete s1.studentName;
console.log(s1.studentName); // 기본 이름

hasOwnProperty()는 해당 속성이 인스턴스 자체에 존재하는지를 검사하며, in 연산자는 인스턴스나 프로토타입 중 어디에 있든 상관없이 존재 여부를 반환한다.

console.log(s1.hasOwnProperty('studentName')); // false
console.log('studentName' in s1);              // true

s1.studentName = '변경됨';
console.log(s1.hasOwnProperty('studentName')); // true
console.log('studentName' in s1);              // true

console.log(s2.hasOwnProperty('studentName')); // false
console.log('studentName' in s2);              // true

두 메서드를 조합하여 속성이 프로토타입에 존재하는지 판단할 수 있다.

function isInPrototype(obj, prop) {
    return !obj.hasOwnProperty(prop) && (prop in obj);
}

let std1 = new Student();
console.log(isInPrototype(std1, 'studentName')); // true

std1.studentName = '로컬값';
console.log(isInPrototype(std1, 'studentName')); // false

프로토타입은 동적으로 수정할 수 있으며, 변경 사항은 즉시 모든 인스턴스에 반영된다.

function Developer() {}
let dev = new Developer();

Developer.prototype.work = function() {
    console.log("개발 중...");
};
dev.work(); // 개발 중...

단점은 모든 인스턴스가 프로토타입의 속성을 공유하기 때문에 개인화된 데이터를 처리하기 어렵다는 것이다.

4. 생성자 + 프로토타입 조합 방식

가장 널리 사용되는 방식으로, 생성자 함수로 인스턴스 고유 속성을 정의하고, 프로토타입으로 공통 메서드를 정의한다.

function Employee(empName, empAge) {
    this.empName = empName;
    this.empAge = empAge;
    this.projects = ['프로젝트A', '프로젝트B'];
}

Employee.prototype = {
    constructor: Employee,
    introduceSelf: function() {
        console.log(`안녕, 나는 ${this.empName}야.`);
    }
};

let e1 = new Employee('김영희', 28);
e1.introduceSelf(); // 안녕, 나는 김영희야.

let e2 = new Employee('이상훈', 32);
e2.introduceSelf(); // 안녕, 나는 이상훈야.

e1.projects.push('프로젝트C');
console.log(e1.projects); // ["프로젝트A", "프로젝트B", "프로젝트C"]
console.log(e2.projects); // ["프로젝트A", "프로젝트B"]

5. 동적 프로토타입 방식

생성자 내부에서 프로토타입을 조건부로 초기화하여 코드의 일관성을 유지할 수 있다.

function Teacher(tName, tSubject) {
    this.tName = tName;
    this.tSubject = tSubject;

    if (typeof this.describe !== 'function') {
        Teacher.prototype.describe = function() {
            console.log(`${this.tSubject} 교사 ${this.tName}입니다.`);
        };
    }
}

let teacher1 = new Teacher('박선생', '수학');
teacher1.describe(); // 수학 교사 박선생입니다.

let teacher2 = new Teacher('최선생', '영어');
teacher2.describe(); // 영어 교사 최선생입니다.

이 방식에서는 프로토타입의 특정 메서드 존재 여부만 확인하면 되며, 객체 리터럴로 프로토타입 전체를 덮어쓰는 것은 피해야 한다.

태그: JavaScript object-oriented-programming Prototype Constructor factory-pattern

9월 17일 00:03에 게시됨