프로토타입 패턴
프로토타입 패턴은 기존 객체를 복제하여 새로운 객체를 생성하는 방법을 제공한다. 이 패턴은 객체를 직접 생성하는 대신, 이미 존재하는 객체를 복제(clone)하여 성능을 향상시키고 객체 생성 비용을 줄인다. 특히, 복잡한 객체를 생성할 때 유용하며, 기존 객체의 상태를 유지하면서 새로운 객체를 쉽게 만들 수 있도록 돕는다. 이를 통해 코드의 재사용성을 높이고, 객체 생성 로직을 단순화할 수 있다. 또한, 객체를 반복적으로 생성해야 하는 경우, 프로토타입 패턴을 활용하면 불필요한 연산을 줄이고 보다 효율적인 메모리 사용이 가능하다.
아래의 코드는 제네릭을 활용한 프로토타입 패턴을 구현하고 있다. Prototype<T> 클래스는 특정 타입 T의 객체를 저장하고 복제할 수 있도록 설계되었다. clone() 메서드는 객체를 복제할 때 단순히 참조를 복사하는 것이 아니라, deepClone() 메서드를 사용하여 깊은 복사를 수행한다. deepClone()은 객체의 타입을 확인하여, 배열이면 재귀적으로 복사하고, 특정 클래스의 인스턴스라면 생성자를 유지하면서 새로운 인스턴스를 생성한다. 이를 통해 단순한 JSON 구조뿐만 아니라 사용자 정의 클래스 인스턴스까지도 올바르게 복제할 수 있다. 이처럼 프로토타입 패턴을 활용하면 복잡한 객체를 효율적으로 복사하면서도 원본 객체의 불필요한 참조 공유를 방지할 수 있다.
class Prototype<T> {
constructor(private _object: T) {}
get object() {
return this._object;
}
clone(): Prototype<T> {
// value가 객체일 경우, 깊은 복사로 복제해야 하므로 _deepClone()을 사용
return new Prototype(this.deepClone(this._object));
}
// 중첩된 객체나 배열을 깊은 복사하는 메서드
private deepClone(obj: T): T {
if (obj === null || typeof obj !== 'object') {
return obj; // 원시 값은 그대로 반환
}
// 객체가 특정 클래스의 인스턴스인지 확인
if (obj.constructor && obj.constructor !== Object) {
return new (obj.constructor as { new(...args: any[]): T })(
...Object.values(obj)
);
}
// 배열인 경우 처리
if (Array.isArray(obj)) {
return obj.map(item => this.deepClone(item)) as T;
}
// 일반 객체인 경우 처리
let clone: { [key: string]: any } = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = this.deepClone(obj[key] as T);
}
}
return clone as T;
}
}
프로토타입 패턴의 진가는 매우 복잡한 객체를 효율적으로 복제해야 하는 상황에서 더욱 빛을 발한다. 객체 내부에 중첩된 구조가 많거나, 클래스 인스턴스, 배열, 원시 타입 등이 혼합되어 있을 경우, 단순한 얕은 복사 방식으로는 올바른 복제가 어렵다. 하지만 프로토타입 패턴을 활용하면 객체의 구조를 유지하면서도 독립적인 복사본을 생성할 수 있다. 특히, 초기화에 많은 비용이 드는 객체를 반복적으로 생성해야 하는 경우, 프로토타입 패턴을 사용하면 한 번만 인스턴스를 생성한 후 복제하여 사용할 수 있으므로 메모리를 절약하고 성능을 향상시킬 수 있다.
예를 들어, 아래와 같이 객체 초기화에 많은 자원이 소모되고, 여러 개의 복잡한 메서드를 포함한 클래스가 있다고 가정해보자. 이런 객체를 반복적으로 생성하는 것은 비효율적이며 성능에도 악영향을 미칠 수 있다. 이럴 때, 프로토타입 패턴을 사용하면 초기에 한 번만 인스턴스를 생성한 후, 원본을 복제하여 사용할 수 있으므로 훨씬 효율적인 객체 생성이 가능해진다.
class Test {
constructor(private _value: number) {}
get value() {
return this._value;
}
set value(value: number) {
this._value = value;
}
testFn() {}
}
const original = new Prototype(new Test(1));
const clone = original.clone();
console.log(original.object.testFn === clone.object.testFn); // true
한 가지 흥미로운 점은, 깊은 복사를 수행했음에도 불구하고 testFn 메서드의 메모리 주소가 여전히 동일하다는 사실이다. 이는 자바스크립트가 내부적으로 클래스를 생성자 함수와 프로토타입 기반의 구조로 변환하는 방식과 관련이 있다. 자바스크립트에서 클래스 내부의 메서드는 개별 인스턴스가 아닌 프로토타입 객체(prototype)에 저장되며, 모든 인스턴스가 이를 공유한다. 따라서 객체를 깊은 복사하더라도, 메서드는 새로운 객체에 복사되지 않고 원본과 동일한 prototype을 참조하게 된다.
즉, clone 객체는 속성(_value)은 독립적으로 가지지만, testFn 같은 메서드는 여전히 Test.prototype을 공유하고 있기 때문에 메모리 주소가 변하지 않는 것이다. 이는 프로토타입 기반 언어인 자바스크립트가 메모리 효율성을 최적화하는 방식이기도 하다. 객체의 상태는 복제하면서도, 변하지 않는 로직(메서드)은 모든 인스턴스가 공유하도록 설계된 것이다.
만약 복제된 객체가 원본과 완전히 독립적인 메서드를 가지길 원한다면, 크게 두 가지 방식을 고려해볼 수 있다. 하나는 깊은 복사 과정에서 프로토타입 체인까지 복사하는 방법이다. 이 방식은 객체와 프로토타입 모두를 재귀적으로 복사하여, 복제된 객체가 원본과 독립적인 프로토타입을 가지게 만든다. 하지만, 일반적으로 프로토타입 메서드를 공유하는 것이 더 효율적이며, 자바스크립트의 객체 생성 방식에 부합하는 방식이다.
다른 하나는 화살표 함수를 사용하는 방법이다. 화살표 함수는 자체적으로 this를 바인딩하지 않고, 선언된 위치에서 상위 스코프의 this를 그대로 사용한다. 따라서 화살표 함수로 선언된 메서드는 프로토타입이 아니라 개별 인스턴스에 위치하게 된다. 즉, 객체의 메서드로 화살표 함수를 사용하면, 메서드가 프로토타입을 통해 공유되는 것이 아니라, 각각의 객체에 독립적으로 복사된다. 이 방법을 사용하면 메서드를 공유하지 않고, 모든 인스턴스에 대해 개별적인 메모리 공간을 할당하게 되므로, 복제된 객체가 원본과 독립적인 동작을 하도록 만들 수 있다. 그러나 이 방식은 메서드가 각 인스턴스에 중복되므로 메모리 사용량이 늘어날 수 있다는 점을 유의해야 한다.
class Test {
constructor(private _value: number) {}
get value() {
return this._value;
}
set value(value: number) {
this._value = value;
}
testFn = () => {}
}
const original = new Prototype(new Test(1));
const clone = original.clone();
console.log(original.object.testFn === clone.object.testFn); // true
블로그의 정보
Ayden's journal
Beard Weard Ayden