프로토타입 패턴
이미 설정된 객체를 원형으로 복제하되, 공유할 값과 따로 복사할 상태를 명확히 정하는 생성 패턴.
· 마지막 업데이트: 2026-10-25 · 5 min read
개요
보고서 기본 설정을 매번 처음부터 채우는 대신, 설정이 끝난 원본을 복제해서 일부만 바꾸고 싶다고 하자. 이런 복제용 원본을 프로토타입(Prototype), 즉 원형이라고 한다.
프로토타입 패턴에서는 객체가 자신의 복제 방법을 제공한다. 호출부는 내부 생성 절차를 다시 적지 않고 원형을 복제해 새 객체를 얻는다.
UML
호출부는 IPreset.Copy()를 사용한다. Preset이 복제 정책을 가지고, 변경 가능한 Tags 목록을 새로 만든다.
역사
GoF는 1994년 Design Patterns에서 Prototype을 생성 패턴으로 정리했다. 생성할 객체의 구체적인 종류를 원형으로 지정하고 이를 복제하는 방식이다.1
왜 사용하는가?
어떤 설정 묶음으로 시작할지 원형을 고르는 곳에서 결정할 수 있다. 복제 구현이 한곳에 있어 호출부가 내부 필드를 따라 복사하는 코드도 줄어든다.
복사는 새로운 객체 하나를 만든다는 말만으로 충분하지 않다. 변경 가능한 목록을 공유할지 독립시킬지, 파일 핸들처럼 복제할 수 없는 자원은 어떻게 할지 정해야 한다.
예제
원형을 복제한 뒤 복제본의 이름만 바꾼다.
using System;
using System.Collections.Generic;
using System.Linq;
var original = new Preset("base", new[] { "draft" });
var copy = (Preset)original.Copy();
copy.Name = "custom";
Console.WriteLine(original.Name + " / " + copy.Name);
// 호출부가 내부 필드를 직접 복사하지 않도록 복제 동작을 제공한다.
interface IPreset { IPreset Copy(); }
// 가변 태그 목록을 새로 만들어 원형과 복제본의 수정을 분리한다.
sealed class Preset(string name, IEnumerable<string> tags) : IPreset
{
public string Name { get; set; } = name;
public List<string> Tags { get; } = new(tags);
public IPreset Copy() => new Preset(Name, Tags);
}
base / customCopy()의 반환형은 IPreset이다. 여기서는 복제본이 Preset임을 아는 호출부에서 이름을 바꾸려고 형변환한다. 새 객체를 바꾸므로 원형의 이름은 그대로 남는다.
범위 넓힌 예제
복제본의 Tags에도 값을 추가해 본다. 새 목록을 만들었으므로 원형 목록에는 영향을 주지 않는다.
using System;
using System.Collections.Generic;
using System.Linq;
var original = new Preset("base", new[] { "draft" });
var copy = (Preset)original.Copy();
copy.Tags.Add("reviewed");
Console.WriteLine(string.Join(",", original.Tags));
Console.WriteLine(string.Join(",", copy.Tags));
// 호출부가 내부 필드를 직접 복사하지 않도록 복제 동작을 제공한다.
interface IPreset { IPreset Copy(); }
// 가변 태그 목록을 새로 만들어 원형과 복제본의 수정을 분리한다.
sealed class Preset(string name, IEnumerable<string> tags) : IPreset
{
public string Name { get; set; } = name;
public List<string> Tags { get; } = new(tags);
public IPreset Copy() => new Preset(Name, Tags);
}
draft
draft,reviewed이 예제는 문자열 목록을 새로 만들고, 변경할 수 없는 문자열 값은 공유한다. 중첩된 가변 객체까지 독립시켜야 한다면 그 객체의 복제 규칙도 구현해야 한다. 모든 상태를 무조건 깊게 복사하는 규칙은 아니다.
TypeScript 예제
호출부는 필요한 원형을 고르고, Preset은 어떤 상태를 복사할지 결정한다. 태그 목록을 독립시키는 규칙이 바뀌어도 복사하는 모든 호출부를 찾아 고치지 않게 하려는 분리다.
// 호출부가 내부 필드를 직접 복사하지 않도록 복제 동작을 제공한다.
interface IPreset { copy(): IPreset; }
class Preset implements IPreset {
readonly tags: string[];
constructor(public name: string, tags: readonly string[]) { this.tags = [...tags]; }
// 가변 태그 목록을 새로 만들어 원형과 복제본의 수정을 분리한다.
copy(): Preset { return new Preset(this.name, this.tags); }
}
const original = new Preset("base", ["draft"]);
const copy = original.copy();
copy.name = "custom";
copy.tags.push("reviewed");
console.log(original.name + " / " + copy.name);
console.log(original.tags.join(","));
console.log(copy.tags.join(","));base / custom
draft
draft,reviewedTypeScript에서는 copy()가 Preset을 반환하도록 해 이름과 태그를 바꿀 때 형변환이 필요 없다. 목록은 새로 만들고 불변인 문자열 값은 공유한다.
같이 보기
C# 예제는 각각 독립된 .NET 10 콘솔 프로그램이다.
TypeScript 예제는 ES2022 이상으로 컴파일한 JavaScript를 Node.js에서 실행한다.