프로토타입 패턴 (Prototype Pattern)
프로토타입(원형)을 복제해서 새로운 객체를 생성한다
- 다음과 같이 불린다.
- 프로토타입
- 원형
의도
GoF 책에서는 다음과 같이 패턴의 의도를 밝힌다.
원형이 되는(prototypical) 인스턴스를 사용하여 생성할 객체의 종류를 명시하고, 이렇게 만든 견본을 복사해서 새로운 객체를 생성합니다.1
구조는 다음과 같다.2
- Prototype: 자신을 복제하는 데 필요한 인터페이스를 정의한다.
- ConcretePrototype: 자신을 복제하는 연산을 구현한다.
- Client: 원형(Prototype)에 자기 자신의 복제를 요청하여 새로운 객체를 생성한다.
언제 쓰는가?
원형 패턴은 제품의 생성, 복합, 표현 방법에 독립적인 제품을 만들고자 할 때 씁니다. 그리고
- 인스턴스화할 클래스를 런타임에 지정할 때(이를테면, 동적 로딩), 또는
- 제품 클래스 계통과 병렬적으로 만드는 팩토리 클래스를 피하고 싶을 때, 또는
- 클래스의 인스턴스들이 서로 다른 상태 조합 중에 어느 하나일 때 원형 패턴을 씁니다. 이들을 미리 원형으로 초기화해두고, 나중에 이를 복제해서 사용하는 것이 매번 필요한 상태 조합의 값들을 수동적으로 초기화하는 것보다 더 편리할 수도 있습니다. 2
기억해 둘 점들
프로토타입의 각 서브타입들이 모두 clone()을 정확하게 구현해야 한다.
- 즉,
clone()구현 작업이 어렵거나 불가능한 경우에는 프로토타입 패턴을 사용하지 않는 것이 좋다. - 객체가 순환참조 값을 포함하고 있는 경우에 특히 주의할 것.
- deep copy가 필요한 경우가 있을 수 있다.
- deep copy 코딩 실수로 복제한 여러 인스턴스의 일부 상태가 공유될 수 있다는 점에 주의.
객체 복사가 쉽거나 기본적으로 지원되는 프로그래밍 언어에서는 이 패턴이 필요없을 수 있다.
- GoF에 의하면 C++ 에서는 유용하지만, Smalltalk나 Objective-C에서는 중요하지 않은 패턴이라고 한다.3
- Kotlin의 경우 모든 데이터 클래스는
copy()를 기본적으로 갖고 있다.copy()를 그대로 사용해 인스턴스 복제본을 생성할 수도 있고, named parameter를 사용해서 일부 프로퍼티만 변경한 복제본을 생성할 수도 있다.
- 구조가 복잡하다면 가능한 한 deep copy를 할 것.
프로토타입 관리자(prototype manager)를 사용하는 경우가 많다.
- 프로토타입 관리자는 구조도 그림에는 등장하지 않는다.
- 그러나 이 패턴으로 구현한 결과물을 활용할 때 핵심이 되는 역할을 한다.
- GoF 책에서도 프로토타입 관리자를 언급한다
-
사용자는 원형 자체를 다루지는 않으며, 단지 레지스트리에서 원형을 검색하고 그것을 레지스트리에 저장할 뿐입니다. 사용자는 원형을 복제하기 전에 레지스트리에 원형이 있는지 먼저 알아봅니다. 이런 레지스트리를 가리켜 원형 관리자(prototype manager)라고 합니다. 3.
-
사례
설명이 복잡해 보이지만 다음 사례들을 생각해보는 것이 도움이 될 수 있다.
그림 그리기 애플리케이션
프로토타입 패턴을 사용하는 가장 대표적인 사례는 그림 그리기 애플리케이션이다.
- 그림 그리기 애플리케이션을 만든다고 생각해 보자.
- 삼각형, 타원, 별모양, 사람 모양 등의 다양한 도형을 미리 제공해서 사용자가 아이콘만 클릭해도 도형을 그릴 수 있게 하는 그림 프로그램.
- 이 모든 도형들은 모두
Prototype인터페이스를 구현하게 된다.- 사용자가 별 모양을 그리기 위해 별 모양 아이콘을 클릭하면, 별 모양의
Prototype을 복제하여 새로운 별 모양 객체를 생성한다.
- 사용자가 별 모양을 그리기 위해 별 모양 아이콘을 클릭하면, 별 모양의
다음은 그림 그리기 도구 draw.io를 사용하는 장면의 스크린샷이다.

사람 모양 도형을 클릭하면 까만색 막대, 흰색 배경의 사람 모양이 그려지며 바로 아래에는 Actor라는 이름이 붙어 있다.
draw.io에서 실제로 이 패턴을 사용하는지는 모르겠지만, 내가 그림 그리기 도구를 만들어야 한다면 나는 이 패턴을 사용할 것 같다.
다양한 기본 도형 하나하나를 xml이나 json 파일로 정의해 뒀다고 하자.
- 그렇다면 사용자가 도형을 그릴 때마다 파일을 읽어서 객체를 생성하는 것은 비효율적이다.
- 애플리케이션을 실행할 때 한 번에 모두 읽어서 프로토타입으로 만들어두는 것이 낫다.
- 사용자가 도형을 그릴 때마다 프로토타입을 복제하여 새로운 객체를 생성해 리턴해주면 된다.
- 물론 도형 정의 파일이 너무 많다면 모두 읽는 게 아니라 lazy하게 필요할 때 읽어서 프로토타입으로 만들 수도 있다.
class GraphicFactory {
// 설정 파일에서 읽은 다양한 도형의 프로토타입을 미리 갖고 있다.
private final Map<String, Graphic> prototypeMap;
public GraphicFactory(String configFileName) {
this.prototypeMap = readConfigFilesAndCreateMap(configFileName);
}
// 새로운 모양을 생성해주는 팩토리 같지만 실제로는 프로토타입을 복사해서 나눠주는 일을 하는 메소드.
public Graphic newGraphic(String graphicName) {
Graphic prototypeGraphic = prototypeMap.get(graphicName);
if (prototypeGraphic == null) {
throw new IllegalArgumentException(
"찾을 수 없는 graphicName 이므로 복제할 수 없습니다.");
}
return prototypeGraphic.clone();
}
Map<String, Graphic> readConfigFilesAndCreateMap(String fileName) {
Map<String, Graphic> map = new HashMap<>();
// 파일을 읽어 for loop 을 돌면서 각 도형 인스턴스를 생성해 map 에 등록한다.
// 설정 파일에 지정한 도형의 기본 타입을 읽고 concrete class 인스턴스를 생성한다.
return map;
}
}
GoF: 음악 편집기
GoF 책에서는 음악 편집기(악보 편집기)를 예로 들고 있다.
(전략)
이를 해결할 수 있는 방법은 GraphicTool 클래스가 Graphic 서브클래스의 인스턴스를 복사, 다시 말해 "복제(cloning)"함으로써 새로운 Graphic 인스턴스를 만들도록 하는 것입니다. 이렇게 복제된 인스턴스를 원형이라고 합니다. GraphicTool은 자신이 복제해야 할 원형으로 매개변수화되며 문서에 추가됩니다. 모든 Graphic 서브클래스가 Clone() 연산을 제공한다면 GraphicTool 클래스는 어떤 종류의 Graphic 클래스도 복제할 수 있게 됩니다.그러므로 음악 편집기의 예에서 음악 객체를 생성하는 각 도구는 GraphicTool 클래스의 인스턴스이고, 이들은 서로 다른 원형으로 초기화됩니다. 각 GraphicTool 클래스의 인스턴스는 이 원형을 복제하여 음악 객체를 만들며, 이 복제본을 악보에 추가시킵니다.
원형 패턴을 사용하면 심지어 클래스 수를 더 줄일 수 있습니다. 반음표, 온음표 등을 나타내는 클래스가 있긴 하지만 사실 꼭 필요한 클래스로 보긴 힘듭니다. 이것보다는 이들을 같은 클래스의 인스턴스로 생성하되, 각각을 서로 다른 비트맵 및 음길이 값을 이용해서 초기화하는 것이 더 좋은 방법입니다. 즉, 클래스는 동일하게 하고, 안에 정의될 값이나 구조를 달리하는 것이 더 좋은 방법이라는 이야기입니다. 전체 음표를 만드는 도구는 보표로 GraphicTool이 되고, 이 GraphicTool의 클래스는 모든 음표에 초기화되는 MusicalNote가 될 것입니다. 이로써 시스템에서 발생할 수 있는 클래스의 수를 놀랠 정도로 줄일 수 있습니다. 또한 새로운 종류의 음표도 쉽게 추가할 수 있게 됩니다. 4
GoF: 회로도 설계 편집기
많은 응용 프로그램은 구성요소와 부분 구성요소의 복합을 통해 객체를 구축합니다. 예를 들어, 회로 설계를 위한 편집기는 세부 회로를 모아서 큰 회로를 만듭니다.5 이런 응용프로그램에서는 편의를 위해 복잡한 사용자 정의 구조를 사용자가 인스턴스화하여 그 상황에 맞는 세부 회로를 계속 이용할 수 있도록 배려해 줄 때가 많습니다.
이때도 원형 패턴은 매우 좋은 해결책이 될 수 있습니다. 그냥 이 세부 회로를 원형으로 만들어, 이것을 현재 사용 가능한 회로 요소 관리 팔레트에 등록하기만 하면 됩니다. 그러고 나면 이 복합 회로 객체가 Clone() 연산을 구현함으로써 다른 구조를 갖는 회로의 기본 골격을 만듭니다. 6
인용
From: 실용주의 디자인 패턴
나는 Allen Holub의 다음 설명이 이 패턴을 쉽게 설명한다고 생각한다.
1. 객체가 어떤 타입인지를 알지 못해도 동일 타입의 객체를 여러 개 생성할 수 있다. [[/pattern/abstract-factory]]{AbstractFactory}에서는 Concrete Product를 초기화하는 데 필요한 정보(예를 들어 생성자의 인자) 등이 컴파일 타임에 알려져 있어야 한다. 대부분의 [[/pattern/abstract-factory]]{Abstract Factory} 구체화는 인자가 없는 디폴트 생성자를 사용한다. 하지만 [[/pattern/abstract-factory]]{Abstract Factory}를 사용하여 디폴트 상태에 있지 않은 객체를 생성하려면 우선 객체를 생성한 후, 이를 외부에서 수정해 주어야 하는데 수정을 코드의 이곳 저곳에서 해주어야 할 때도 있다. 이런 경우 원하는 상태를 가진 객체를 생성하고, 이를 단순히 복사해서 사용하는 것이 더 좋다. 이때 [[/pattern/abstract-factory]]{Abstract Factory}를 사용하여 프로토타입 객체를 생성할 수도 있다.
2. 객체는 때론 몇 가지 상태만을 갖지만 각 상태에 있는 여러 개의 객체를 가져야 할 때가 있다(GoF는 음악 작곡 시스템의 Note 클래스를 설명한다. 온음표, 2분음표, 4분 음표 객체들의 수많은 인스턴스가 존재하지만 이들 각각은 동일한 상태에 있다).
3. 어떤 경우엔 클래스가 런타임에 지정되고, 이를 비용이 큰 동적 로딩(예를 들면
Class.forName("class.name"))이나 이와 비슷하게 비용이 큰 프로세스(예를 들면 초기 상태가 XML 파일에 지정되어 있는 경우)를 통해 생성하는 경우가 있다. 반복적으로 비싼 객체 생성 작업을 하는 대신 하나의 프로토타입을 만들고 이를 여러 번 복사해 사용하라. 7
장단점에 대해서는 다음과 같이 언급한다.
장점
- 런타임에 Factory에 새로운 Concrete Product를 추가하기 쉽다. Factory에 Prototype을 넘기기만 하면 되기 때문이다. 삭제 역시 쉽다.
- Prototype은 객체 생성 시간을 줄일 수 있다.
- [[/pattern/abstract-factory]]{Abstract Factory}는 구현 상속을 통해 약간씩 다른 행위를 가진 클래스를 정의하도록 한다. Prototype 패턴은 상태를 이용하여 [[/pattern/abstract-factory]]{Abstract Factory}의 상속 문제를 해결한다. 객체의 행위가 상태에 따라 급격히 변화한다면 객체를 동적으로 설정 가능한 클래스라 생각하고 Prototype을 이용해 객체 생성 메커니즘을 구현할 수 있다.
단점
- 명시적으로
clone()메소드를 구현해야 하는데, 이 작업이 매우 어려울 수도 있다. 또한 깊은 복사(deep copy)와 얕은 복사(shallow copy) 문제를 생각해 보라. 레퍼런스만 복사하면 되는가, 아니면 참조된 객체까지 복사해야 하는가?
From: 코틀린 디자인 패턴
'코틀린 디자인 패턴'에서는 이 패턴에 대해 짧게 설명하면서 Kotlin, Java, JavaScript를 언급한다.
프로토타입의 핵심 아이디어는 객체를 쉽게 복사할 수 있도록 하는 것이다. 적어도 다음의 두 가지 경우에 프로토타입 패턴이 필요하다.
- 객체 생성에 많은 비용이 드는 경우(예를 들어 객체 생성 시 데이터베이스에서 자료를 조회해야 하는 경우)
- 비슷하지만 조금씩 다른 객체를 생성하느라 비슷한 코드를 매번 반복하고 싶지 않은 경우
중요
- td
- 더 깊이 들어가면 프로토타입 패턴이 필요한 다른 이유도 있다. 가령 자바스크립트에서는 클래스 없이 객체와 비슷한 동작을 구현하기 위해 프로토타입을 사용한다.
자바의 엉터리 같은
clone()메서드가 다행히도 코틀린에서는 고쳐졌다. 코틸린에서 모든 데이터 클래스는copy()메서드를 가진다. 이 메서드는 다른 데이터 클래스의 인스턴스를 받아 복제본을 생성하며, 원한다면 그 과정에서 속성을 변경할 수도 있다. 8
참고문헌
- GoF의 디자인 패턴(개정판) / 에릭 감마, 리처드 헬름, 랄프 존슨, 존 블라시디스 공저 / 김정아 역 / 프로텍미디어 / 발행 2015년 03월 26일
- 실전 코드로 배우는 실용주의 디자인 패턴 / Allen Holub 저 / 송치형 편역 / 사이텍미디어 / 발행 2006년 07월 19일 / 원제 : Holub on Patterns : Learning Design Patterns by Looking at Code
- 코틀린 디자인 패턴 2/e / 알렉세이 소신 저/이대근 역 / 에이콘출판사 / 2판 발행: 2023년 08월 31일 / 원제: Kotlin Design Patterns and Best Practices: Build scalable applications using traditional, reactive, and concurrent design patterns in Kotlin, 2nd Edition by Alexey Soshin and Anton Arhipov