cv.cm

코딩 · 디자인 패턴

예제로 이해하는 프로토타입 패턴

7분 분량

핵심 답변

프로토타입 패턴은 new로 처음부터 만드는 대신 기존 인스턴스(프로토타입)를 복사해 새 객체를 만드는 생성 패턴입니다. 각 프로토타입이 clone() 메서드를 구현하므로 클라이언트 코드는 구체 클래스를 몰라도 객체를 복제할 수 있습니다. 설정하는 데 비용이 많이 들거나 미리 정해 둔 변형이 많은 객체에 유용합니다. 가장 흔한 함정은 얕은 복사입니다. 중첩된 객체도 함께 복사하지 않으면 복제본과 원본이 그 객체를 공유하게 됩니다.

의도

원형이 되는 인스턴스로 만들 객체의 종류를 지정하고, 이 프로토타입을 복사해 새 객체를 만듭니다. 프로토타입 패턴은 Gang of Four(GoF) 책의 다섯 가지 생성 패턴 중 하나입니다.

해결하는 문제

붉은 경고 상자, 파란 메모, 초록 "완료" 배지 같은 도형 프리셋 팔레트를 가진 다이어그램 편집기를 떠올려 보세요. 각 프리셋에는 크기, 색상, 글꼴, 스타일 규칙 목록이 있습니다. 사용자가 프리셋을 캔버스로 끌어 놓으면 편집기는 정확히 그 설정으로 시작하는 새롭고 독립적인 도형이 필요합니다.

이 패턴이 없다면 편집기는 프리셋마다 별도의 생성 함수가 필요하고 모든 구체 도형 클래스를 알아야 합니다. 더 나쁜 점은 사용자가 런타임에 정의한 프리셋을 아예 만들 수 없다는 것입니다. 어떤 코드도 그 설정을 미리 알지 못하기 때문입니다. 프로토타입 패턴에서는 편집기가 그냥 preset.clone()을 호출하면 됩니다.

구조와 참여자

참여자역할예제에서
Prototype자기 자신을 복제하는 인터페이스를 선언.clone()을 가진 Shape
ConcretePrototype자신의 상태를 복사해 clone()을 구현.Box
Client프로토타입에게 스스로를 복제하라고 요청해 새 객체를 만듦.편집기 코드
프로토타입 레지스트리(선택)클라이언트가 찾아볼 수 있도록 이름 붙은 프로토타입을 저장.palette 맵

TypeScript 구현

clone()은 중첩된 객체와 배열을 포함해 모든 필드를 복사하므로 복제본은 원본과 독립적입니다.

interface Shape {
  clone(): Shape;
  describe(): string;
}

class Box implements Shape {
  constructor(
    public width: number,
    public height: number,
    public style: { fill: string; border: string },
    public tags: string[],
  ) {}

  clone(): Box {
    // copy nested objects too, so the clone shares nothing mutable
    return new Box(this.width, this.height, { ...this.style }, [...this.tags]);
  }

  describe(): string {
    return `${this.width}x${this.height} ${this.style.fill} [${this.tags.join(", ")}]`;
  }
}

// A registry of named prototypes, which could also be filled at run time.
const palette = new Map<string, Shape>([
  ["warning", new Box(120, 40, { fill: "red", border: "black" }, ["alert"])],
  ["note", new Box(160, 80, { fill: "blue", border: "none" }, [])],
]);

const a = palette.get("warning")!.clone() as Box;
a.width = 200;
a.tags.push("urgent");
console.log(a.describe());                        // 200x40 red [alert, urgent]
console.log(palette.get("warning")!.describe());  // 120x40 red [alert]

편집기는 Box라는 이름을 전혀 쓰지 않습니다. 새 프리셋은 새 클래스나 새 생성 함수가 아니라 레지스트리의 새 항목입니다.

얕은 복사와 깊은 복사

프로토타입 패턴에서 가장 흔한 버그는 복제본이 원본과 중첩 객체를 공유하는 것입니다. 스프레드 복사는 얕은 복사로, 최상위만 복사합니다.

const original = { name: "note", style: { fill: "blue" } };

const shallow = { ...original };
shallow.style.fill = "green";
console.log(original.style.fill); // green (shared object was changed)

const deep = structuredClone(original);
deep.style.fill = "yellow";
console.log(original.style.fill); // green (unchanged)

structuredClone()은 최신 브라우저와 Node.js에 내장되어 있으며, 중첩 객체, 배열, Map, Set, Date를 포함한 일반 데이터를 깊게 복사합니다. 여기서 중요한 제약이 두 가지 있습니다.

  • 클래스 인스턴스는 일반 객체로 돌아옵니다. 복사본이 프로토타입을 잃으므로 describe() 같은 메서드가 없습니다.
  • 함수와 DOM 노드는 복제할 수 없으며, structuredClone()은 DataCloneError를 던집니다.

언제 쓰고 언제 쓰지 말아야 하나

다음과 같을 때 프로토타입 패턴을 사용합니다.

  • 객체를 준비하는 비용이 크고(파일에서 로드, 계산, 여러 단계의 설정) 비슷한 복사본이 많이 필요할 때.
  • 사용자가 만든 프리셋이나 템플릿처럼 변형의 집합이 런타임에 정해질 때.
  • 구체 클래스에 의존하지 않고, 병렬 팩토리 계층 없이 객체를 만들고 싶을 때.

다음 경우에는 피하세요.

  • 객체가 만들기 싸고 단순할 때. 생성자나 팩토리 함수를 호출하는 편이 더 명확합니다.
  • 열린 파일, 소켓, 고유 ID처럼 복제해서는 안 되는 리소스를 객체가 가지고 있을 때. 이런 것은 복제에 특별한 처리가 필요합니다.
  • 객체에 순환 참조와 복잡한 소유 관계가 있어 올바른 깊은 복사가 어려울 때.

실제 사용 사례

  • JavaScript 자체가 프로토타입 기반입니다. Object.create(proto)는 기존 객체를 상속하는 새 객체를 만듭니다. 새 객체가 프로토타입을 복사하지 않고 위임한다는 점에서 이 패턴과 관련은 있지만 똑같지는 않습니다.
  • DOM: node.cloneNode(true)는 요소와 그 자식을 깊게 복사하며, 반복되는 UI 조각을 찍어 내는 흔한 방법입니다.
  • Java: 클래스가 Cloneable 인터페이스를 구현하고 Object.clone()을 오버라이드할 수 있지만, 많은 Java 개발자는 복사 생성자를 선호합니다.
  • 게임 엔진과 편집기는 흔히 미리 구성된 템플릿(프리팹이라고 부르는 경우가 많음)에서 새 객체를 생성합니다.

관련 패턴

  • 추상 팩토리 패턴: 팩토리가 프로토타입 집합을 저장해 두고 복제로 제품을 만들 수 있어, 제품군마다 팩토리 서브클래스가 필요 없습니다.
  • 팩토리 메서드 패턴: 서브클래스를 통해 객체를 만듭니다. 프로토타입 패턴은 인스턴스를 복사해 그 서브클래스를 없앱니다.
  • 컴포지트 패턴과 데코레이터 패턴: 이 패턴들을 많이 쓰는 설계는 프로토타입 패턴의 덕을 보는 경우가 많습니다. 구성이 끝난 구조를 복사하는 편이 다시 만드는 것보다 쉽기 때문입니다.
  • 메멘토 패턴: 이것도 객체의 상태를 복사하지만, 새 독립 객체를 만드는 것이 아니라 나중에 복원하기 위한 것입니다.

자주 묻는 질문

프로토타입 패턴은 JavaScript의 프로토타입과 같은가요?

아니요. JavaScript 프로토타입은 상속을 위한 언어 메커니즘으로, 객체가 없는 프로퍼티를 프로토타입에 위임합니다. 프로토타입 패턴은 객체의 독립적인 복사본을 만듭니다.

clone()은 깊은 복사와 얕은 복사 중 무엇을 해야 하나요?

가변 중첩 데이터는 보통 깊은 복사를 해서 복제본과 원본이 서로 영향을 주지 않게 합니다. 불변 값과 공유되는 읽기 전용 리소스는 안전하게 공유해도 됩니다.

structuredClone()으로 clone()을 구현해도 되나요?

일반 데이터 객체에만 해당합니다. 클래스 인스턴스에 structuredClone()을 쓰면 메서드 없는 일반 객체가 반환되므로, 복사 로직을 직접 작성하거나 생성자로 인스턴스를 다시 만드세요.

프로토타입 패턴이 팩토리보다 나은 때는 언제인가요?

변형이 많거나, 런타임에 구성되거나, 준비 비용이 클 때입니다. 이때는 완성된 인스턴스를 복제하는 것이 변형마다 생성 함수를 쓰는 것보다 간단합니다.

코딩 튜토리얼 전체