jblog
Prototype 패턴
ArchitectureGuru 디자인 패턴 #4

Prototype 패턴

기존 객체를 복제해서 새로운 객체를 만드는 Prototype 패턴을 알아봅니다.

2026-01-239 min readdesign-pattern, creational, architecture

#똑같은 걸 처음부터 다시 만들라고?

게임을 만들고 있다고 해보자.

맵에 나무 오브젝트를 1000개 배치해야 한다. 나무마다 색상이나 크기는 조금씩 다르지만, 기본 구조는 같다.

for (let i = 0; i < 1000; i++) {
  const tree = new Tree();
  tree.loadModel("tree.obj"); // 파일에서 모델 로드 (느림)
  tree.loadTexture("tree.png"); // 텍스처 로드 (느림)
  tree.setPosition(randomX(), randomY());
  tree.setScale(randomScale());
}

모델 파일을 1000번 읽는다. 매번 같은 파일인데.

시간 낭비

이미 만들어진 나무 하나를 복사해서 위치와 크기만 바꾸면 훨씬 빠르지 않을까?

이 아이디어가 바로 Prototype 패턴이다.

visible: false

Prototype이란?

기존 객체를 복제(clone) 하여 새로운 객체를 생성하는 패턴

new로 처음부터 만드는 대신, 이미 있는 객체를 복사한 뒤 필요한 부분만 수정한다.

비유하자면 문서 템플릿과 같다.

이력서를 매번 처음부터 작성하는 사람은 없다. 기존 이력서를 복사해서 내용만 수정한다.

visible: false

문제: 외부에서 복제하기 어렵다

"그냥 스프레드 연산자 쓰면 되는 거 아닌가?"

const original = new User("junbeom", 25, new Address("Seoul", "Korea"));
const copy = { ...original };
 
copy.address.city = "Busan";
console.log(original.address.city); // "Busan" 🤯 원본도 바뀌었다!

얕은 복사(shallow copy) 문제다. 중첩된 객체는 참조만 복사되기 때문에 원본까지 영향을 받는다.

그리고 더 큰 문제가 있다. 객체에 private 필드가 있다면?

class User {
  private passwordHash: string;
  // 외부에서는 이 필드에 접근할 수 없어서 복제도 못 한다
}

외부에서는 객체의 내부 구조를 다 알 수 없다. 그래서 객체 스스로가 자신을 복제하도록 해야 한다.

visible: false

코드로 보기

Prototype 인터페이스

interface Prototype<T> {
  clone(): T;
}

구현 예시: 설정 프리셋

class DashboardConfig implements Prototype<DashboardConfig> {
  constructor(
    public layout: string,
    public theme: string,
    public widgets: Widget[],
    private apiKey: string, // private 필드도 복제 가능
  ) {}
 
  clone(): DashboardConfig {
    // 깊은 복사: 중첩된 객체도 새로 만든다
    const clonedWidgets = this.widgets.map((widget) => widget.clone());
 
    return new DashboardConfig(
      this.layout,
      this.theme,
      clonedWidgets,
      this.apiKey, // 자기 자신이라 private에 접근 가능!
    );
  }
}
 
class Widget implements Prototype<Widget> {
  constructor(
    public type: string,
    public position: { x: number; y: number },
    public size: { width: number; height: number },
  ) {}
 
  clone(): Widget {
    return new Widget(
      this.type,
      { ...this.position }, // 중첩 객체도 새로 복사
      { ...this.size },
    );
  }
}

사용

// 기본 대시보드 설정 (프리셋)
const defaultDashboard = new DashboardConfig(
  "grid",
  "dark",
  [
    new Widget("chart", { x: 0, y: 0 }, { width: 400, height: 300 }),
    new Widget("table", { x: 400, y: 0 }, { width: 600, height: 300 }),
    new Widget("metric", { x: 0, y: 300 }, { width: 200, height: 150 }),
  ],
  "secret-api-key-123",
);
 
// 복제 후 커스터마이징
const myDashboard = defaultDashboard.clone();
myDashboard.theme = "light";
myDashboard.widgets[0].position = { x: 100, y: 100 };
 
// 원본은 그대로!
console.log(defaultDashboard.theme); // "dark"
console.log(defaultDashboard.widgets[0].position); // { x: 0, y: 0 }

원본 안전

깊은 복사 덕분에 원본과 복제본이 완전히 독립적이다.

visible: false

Prototype Registry: 프리셋 저장소

자주 쓰는 프로토타입을 레지스트리에 등록해두면 편하다.

class ConfigRegistry {
  private prototypes = new Map<string, DashboardConfig>();
 
  register(name: string, config: DashboardConfig) {
    this.prototypes.set(name, config);
  }
 
  create(name: string): DashboardConfig {
    const prototype = this.prototypes.get(name);
    if (!prototype) {
      throw new Error(`"${name}" 프리셋을 찾을 수 없습니다`);
    }
    return prototype.clone();
  }
}
 
// 프리셋 등록
const registry = new ConfigRegistry();
registry.register("analytics", analyticsDashboard);
registry.register("monitoring", monitoringDashboard);
registry.register("admin", adminDashboard);
 
// 프리셋으로부터 새 설정 생성
const myAnalytics = registry.create("analytics");
myAnalytics.theme = "light"; // 커스터마이징

visible: false

JavaScript에서의 Prototype

사실 JavaScript는 이름부터 프로토타입 기반 언어다.

// JavaScript의 프로토타입 체인
const parent = { greet: () => "Hello!" };
const child = Object.create(parent);
 
child.greet(); // "Hello!" — parent에서 상속받음

하지만 이건 디자인 패턴에서의 Prototype과는 약간 다르다.

  • JS Prototype: 프로토타입 체인을 통한 상속 메커니즘
  • 디자인 패턴 Prototype: 기존 객체를 복제하여 새 객체를 생성하는 패턴

이름은 같지만 목적이 다르다. 디자인 패턴의 Prototype은 복제에 초점이 있다.

visible: false

structuredClone: 모던 JavaScript의 해법

ES2022부터 structuredClone()이라는 내장 함수가 추가되었다.

const original = {
  name: "junbeom",
  address: { city: "Seoul", country: "Korea" },
  hobbies: ["coding", "reading"],
};
 
const copy = structuredClone(original);
copy.address.city = "Busan";
 
console.log(original.address.city); // "Seoul" — 깊은 복사!

단, structuredClone일반 객체에만 작동한다. 클래스 인스턴스의 메서드나 프로토타입 체인은 복사하지 않는다.

그래서 클래스 기반 객체에는 여전히 clone() 메서드를 직접 구현하는 것이 안전하다.

visible: false

언제 사용하면 좋을까?

  1. 객체 생성 비용이 클 때

    • DB에서 데이터를 로드하거나, 파일을 파싱하는 등 무거운 초기화가 필요한 경우
    • 복제하면 이 비용을 건너뛸 수 있다
  2. 비슷한 객체를 많이 만들어야 할 때

    • 게임의 적 캐릭터, 대시보드 프리셋, 문서 템플릿 등
  3. 객체의 내부 구조를 외부에서 알 수 없을 때

    • private 필드가 있거나, 서브클래스가 다양한 경우
  4. 런타임에 동적으로 타입이 결정될 때

    • new ClassName()처럼 정적으로 결정할 수 없는 경우

visible: false

장단점

장점단점
복잡한 객체를 빠르게 생성순환 참조가 있으면 복제가 까다로움
구체 클래스에 의존하지 않고 복제 가능깊은 복사 구현이 실수하기 쉬움
반복적인 초기화 코드 제거clone()을 모든 클래스에 구현해야 함

visible: false

정리하며

Prototype은 "이미 있는 걸 복사해서 새로 만들자" 라는 단순한 아이디어의 패턴이다.

핵심을 한 줄로 요약하면,

"처음부터 만들지 말고, 비슷한 걸 복사해서 고쳐라"

특히 JavaScript/TypeScript에서는 객체 복사에 대한 함정(얕은 복사, 참조 공유)이 많기 때문에,

clone() 메서드를 명시적으로 구현하는 Prototype 패턴이 예상치 못한 버그를 방지하는 데 도움이 된다.

다음 글에서는 가장 유명하면서도 가장 논란이 많은 Singleton 패턴을 알아보겠다.

visible: false

#Reference

Refactoring Guru - Prototype

refactoring.guru

댓글

댓글을 불러오는 중...