• 다음과 같이 불린다.
    • 프로토타입
    • 원형

의도

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

주석

  1. GoF의 디자인 패턴(개정판). 169쪽. 

  2. GoF의 디자인 패턴(개정판). 171쪽.  2

  3. GoF의 디자인 패턴(개정판). 174쪽.  2

  4. GoF의 디자인 패턴(개정판). 170쪽. 

  5. 원주: 그러한 응용프로그램은 복합체 패턴과 장식자 패턴을 가지고 있습니다. 

  6. GoF의 디자인 패턴(개정판). 172쪽. 

  7. 실용주의 디자인 패턴. 450쪽. 

  8. 코틀린 디자인 패턴. 102쪽.