봄맞이 세일

프로토타입 패턴

다음 이름으로도 불립니다: 클론, Prototype

의도

프로토타입은 코드를 그들의 클래스들에 의존시키지 않고 기존 객체들을 복사할 수 있도록 하는 생성 디자인 패턴입니다.

프로토타입 디자인 패턴

문제

객체가 있고 그 객체의 정확한 복사본을 만들고 싶다고 가정하면, 어떻게 하시겠습니까? 먼저 같은 클래스의 새 객체를 생성해야 합니다. 그런 다음 원본 객체의 모든 필드들을 살펴본 후 해당 값들을 새 객체에 복사해야 합니다.

너무 쉽군요! 하지만 함정이 있습니다. 객체의 필드들 중 일부가 비공개여서 객체 자체의 외부에서 볼 수 없을 수 있으므로 모든 객체를 그런 식으로 복사하지 못합니다.

'외부에서' 항목을 복사할 때 무엇이 잘못될 수 있을까요?

객체를 '외부에서부터' 복사하는 것은 항상 가능하지 않습니다.

이 직접적인 접근 방식에는 한 가지 문제가 더 있습니다. 객체의 복제본을 생성하려면 객체의 클래스를 알아야 하므로, 당신의 코드가 해당 클래스에 의존하게 된다는 것입니다. 또, 예를 들어 메서드의 매개변수가 일부 인터페이스를 따르는 모든 객체를 수락할 때 당신은 그 객체가 따르는 인터페이스만 알고, 그 객체의 구상 클래스는 알지 못할 수 있습니다.

해결책

프로토타입 패턴은 실제로 복제되는 객체들에 복제 프로세스를 위임합니다. 패턴은 복제를 지원하는 모든 객체에 대한 공통 인터페이스를 선언합니다. 이 인터페이스를 사용하면 코드를 객체의 클래스에 결합하지 않고도 해당 객체를 복제할 수 있습니다. 일반적으로 이러한 인터페이스에는 단일 clone 메서드만 포함됩니다.

clone 메서드의 구현은 모든 클래스에서 매우 유사합니다. 이 메서드는 현재 클래스의 객체를 만든 후 이전 객체의 모든 필드 값을 새 객체로 전달합니다. 대부분의 프로그래밍 언어는 객체들이 같은 클래스에 속한 다른 객체의 비공개 필드들에 접근​(access) 할 수 있도록 하므로 비공개 필드들을 복사하는 것도 가능합니다.

복제를 지원하는 객체를 이라고 합니다. 당신의 객체들에 수십 개의 필드와 수백 개의 가능한 설정들이 있는 경우 이를 복제하는 것이 서브클래싱의 대안이 될 수 있습니다.

미리 만들어진 프로토타입들

미리 만들어진 프로토타입은 서브클래싱의 대안이 될 수 있습니다.

프로토타이핑은 다음과 같이 작동합니다. 일단, 다양한 방식으로 설정된 객체들의 집합을 만듭니다. 그 후 설정한 것과 비슷한 객체가 필요할 경우 처음부터 새 객체를 생성하는 대신 프로토타입을 복제하면 됩니다.

실제상황 적용

실제 산업에서의 프로토타입​(원기)​은 제품의 대량 생산을 시작하기 전에 다양한 테스트를 수행하는 데 사용됩니다. 그러나 프로그래밍의 프로토타입의 경우 프로토타입들은 실제 생산과정에 참여하지 않고 대신 수동적인 역할을 합니다.

세포 분열

세포의 분열

산업 프로토타입들은 실제로 자신을 복제하지 않기 때문에, 프로토타입 패턴에 더 가까운 예시는 세포의 유사분열 과정입니다. 유사분열 후에는 한 쌍의 같은 세포가 형성됩니다. 원본 세포는 프로토타입 역할을 하며 복사본을 만드는 데 능동적인 역할을 합니다.

구조

기초 구현

프로토타입 디자인 패턴의 구조프로토타입 디자인 패턴의 구조
  1. 프로토타입 인터페이스는 복제 메서드들을 선언하며, 이 메서드들의 대부분은 단일 clone 메서드입니다.

  2. 구상 프로토타입 클래스는 복제 메서드를 구현합니다. 원본 객체의 데이터를 복제본에 복사하는 것 외에도 이 메서드는 복제 프로세스와 관련된 일부 예외적인 경우들도 처리할 수도 있습니다. (예: 연결된 객체 복제, 재귀 종속성 풀기).

  3. 클라이언트는 프로토타입 인터페이스를 따르는 모든 객체의 복사본을 생성할 수 있습니다.

프로토타입 레지스트리 구현

프로토타입 레지스트리프로토타입 레지스트리
  1. 프로토타입 레지스트리는 자주 사용하는 프로토타입들에 쉽게 접근​(액세스)​하는 방법을 제공합니다. 이 레지스트리는 복사될 준비가 된 미리 만들어진 객체들의 집합을 저장합니다. 가장 간단한 프로토타입 레지스트리는 name → prototype 해시 맵입니다. 그러나 단순히 이름을 검색하는 것보다 더 나은 검색 기준이 필요한 경우 훨씬 더 탄탄한 레지스트리를 구축할 수 있습니다.

의사코드

아래 예시에서의 프로토타입 패턴은 코드를 기하학적 객체들의 클래스들에 결합하지 않고도 해당 객체들의 정확한 복사본을 생성할 수 있도록 합니다.

프로토타입 패턴 구조 예시

클래스 계층구조에 속한 객체 집합의 복제.

모든 shape(모양) 클래스는 같은 인터페이스를 따르며, 이 인터페이스는 복제 메서드를 제공합니다. 자식 클래스는 자신의 필드 값들을 생성된 객체에 복사하기 전에 부모의 복제 메서드를 호출할 수 있습니다.

// 기초 프로토타입.
abstract class Shape is
    field X: int
    field Y: int
    field color: string

    // 일반 생성자.
    constructor Shape() is
        // …

    // 프로토타입 생성자. 기존 객체의 값들로 새로운 객체가 초기화됩니다.
    constructor Shape(source: Shape) is
        this()
        this.X = source.X
        this.Y = source.Y
        this.color = source.color

    // 복제 작업은 Shape(모양) 자식 클래스 중 하나를 반환합니다.
    abstract method clone():Shape


// 구상 프로토타입. 복제 메서드는 현재 클래스의 생성자를 호출해 현재 객체를
// 생성자의 인수로 전달함으로써 한 번에 새로운 객체를 생성합니다. 생성자에서
// 실제로 모든 것을 복사하게 되면 결과의 일관성이 유지됩니다. 생성자가 새로운
// 객체가 완전히 완성되기 전까지 결과를 반환하지 않아서 어떤 객체도 일부분만 완성된
// 복제본을 참조할 수 없습니다.
class Rectangle extends Shape is
    field width: int
    field height: int

    constructor Rectangle(source: Rectangle) is
        // 부모 클래스에 정의된 비공개 필드들을 복사하려면 부모 생성자 호출이
        // 필요합니다.
        super(source)
        this.width = source.width
        this.height = source.height

    method clone():Shape is
        return new Rectangle(this)


class Circle extends Shape is
    field radius: int

    constructor Circle(source: Circle) is
        super(source)
        this.radius = source.radius

    method clone():Shape is
        return new Circle(this)


// 클라이언트 코드의 어딘가에…
class Application is
    field shapes: array of Shape

    constructor Application() is
        Circle circle = new Circle()
        circle.X = 10
        circle.Y = 10
        circle.radius = 20
        shapes.add(circle)

        Circle anotherCircle = circle.clone()
        shapes.add(anotherCircle)
        // `anotherCircle` 변수에는 `circle` 객체와 똑같은 사본이 포함되어
        // 있습니다.

        Rectangle rectangle = new Rectangle()
        rectangle.width = 10
        rectangle.height = 20
        shapes.add(rectangle)

    method businessLogic() is
        // 프로토타입은 매우 유용합니다! 왜냐하면 프로토타입은 당신이 복사하려는
        // 객체의 유형에 대해 아무것도 몰라도 복사본을 생성할 수 있도록 하기
        // 때문입니다.
        Array shapesCopy = new Array of Shapes.

        // 예를 들어, 우리는 shapes(모양들) 배열의 정확한 요소들을 알지
        // 못하며, 이 요소들이 모양이라는 것만 압니다. 그러나 다형성 덕분에
        // 모양의 `clone`(복제) 메서드를 호출하면 프로그램이 모양의 실제
        // 클래스를 확인하고 해당 클래스에 정의된 적절한 복제 메서드를
        // 실행합니다. 그래서 우리가 단순한 모양 객체들의 집합이 아닌 적절한
        // 복제본들을 얻는 것이죠.
        foreach (s in shapes) do
            shapesCopy.add(s.clone())

        // `shapesCopy`(모양들의 복사본) 배열에는 `shape`(모양) 배열의
        // 자식들과 정확히 일치하는 복사본들이 포함되어 있습니다.

적용

프로토타입 패턴은 복사해야 하는 객체들의 구상 클래스들에 코드가 의존하면 안 될 때 사용하세요.

이와 같은 경우는 당신의 코드가 어떤 인터페이스를 통해 타사 코드에서 전달된 객체들과 함께 작동할 때 많이 발생합니다. 이러한 객체들의 구상 클래스들은 알려지지 않았기 때문에 이러한 클래스들에 의존할 수 없습니다.

프로토타입 패턴은 클라이언트 코드에 복제를 지원하는 모든 객체와 작업할 수 있도록 일반 인터페이스를 제공합니다. 이 인터페이스는 클라이언트 코드가 복제하는 객체들의 구상 클래스들에서 클라이언트 코드를 독립시킵니다.

프로토타입 패턴은 각자의 객체를 초기화하는 방식만 다른 자식 클래스들의 수를 줄이고 싶을 때 사용하세요.

사용하기 전에 많은 설정이 필요한 복잡한 클래스가 있다고 가정해 봅시다. 이 클래스를 설정하는 데는 몇 가지 일반적인 방법들이 있으며 설정되어야 하는 클래스의 새로운 인스턴스들의 생성을 담당하는 코드는 당신의 앱에 흩어져 있습니다. 중복을 줄이기 위해 당신은 여러 자식 클래스들을 만들어 모든 공통 설정 코드를 그 클래스들의 생성자들에 넣었습니다. 이렇게 중복 문제는 해결했지만 이제 쓸모없는 자식 클래스들이 많이 생겼습니다.

프로토타입 패턴은 다양한 방식으로 설정된 미리 만들어진 객체들의 집합을 프로토타입들로 사용할 수 있도록 합니다. 일부 설정과 일치하는 자식 클래스를 인스턴스화하는 대신 클라이언트는 간단하게 적절한 프로토타입을 찾아 복제할 수 있습니다.

구현방법

  1. 프로토타입 인터페이스를 생성한 후 그 안에 clone 메서드를 선언하세요. 또는 기존 계층 구조가 있는 경우, 이 메서드를 그 계층 구조의 모든 클래스들에 추가하세요.

  2. 프로토타입 클래스는 이 클래스의 객체를 인수로 받아들이는 대체 생성자를 반드시 정의해야 합니다. 또 생성자는 이 클래스에 정의된 모든 필드의 값들을 전달된 객체에서 새로 생성된 인스턴스로 복사해야 합니다. 또 자식 클래스를 변경할 때에는 부모 생성자를 호출하여 부모 클래스가 부모 클래스의 비공개 필드들의 복제를 처리하도록 해야 합니다.

    현재 사용 중인 프로그래밍 언어가 메서드 오버로딩을 지원하지 않으면 별도의 '프로토타입' 생성자를 만들 수 없습니다. 따라서 객체의 데이터를 새로 생성된 복제본에 복사하는 작업은 clone​(복제본) 메서드 내에서 수행되어야 합니다. 그래도 이 코드를 일반적인 생성자에 두는 것이 더 안전한 이유는 new 연산자를 호출한 직후에 생성된 객체는 완전히 설정된 상태로 반환되기 때문입니다.

  3. 복제 메서드는 일반적으로 한 줄로 구성됩니다. 이 줄은 생성자의 프로토타입 버전으로 new 연산자를 실행합니다. 모든 클래스는 복제 메서드를 명시적으로 오버라이딩한 후 new 연산자와 함께 자체 클래스 이름을 사용해야 합니다. 그렇게 하지 않으면 복제 메서드가 부모 클래스의 객체를 생성할 수 있습니다.

  4. 또, 추가 옵션으로 자주 사용하는 프로토타입들의 카탈로그를 저장할 중앙 프로토타입 레지스트리를 생성할 수 있습니다.

    레지스트리를 새 팩토리 클래스로 구현하거나 레지스트리를 기초 프로토타입 클래스에 프로토타입을 가져오기 위한 정적 메서드와 함께 넣을 수 있습니다. 이 정적 메서드는 클라이언트 코드가 메서드에 전달하는 검색 기준을 기반으로 프로토타입을 검색해야 합니다. 이때 검색 기준은 단순한 문자열 태그이거나 복잡한 검색 매개변수들의 집합일 수 있습니다. 적절한 프로토타입을 찾고 나면, 레지스트리는 이를 복제한 후 복사본을 클라이언트에 반환해야 합니다.

    마지막으로, 자식 클래스들의 생성자들에 대한 직접 호출들을 프로토타입 레지스트리의 팩토리 메서드에 대한 호출들로 대체하세요.

장단점

  • 당신은 객체들을 그 구상 클래스들에 결합하지 않고 복제할 수 있습니다.
  • 반복되는 초기화 코드를 제거한 후 그 대신 미리 만들어진 프로토타입들을 복제하는 방법을 사용할 수 있습니다.
  • 복잡한 객체들을 더 쉽게 생성할 수 있습니다.
  • 복잡한 객체들에 대한 사전 설정들을 처리할 때 상속 대신 사용할 수 있는 방법입니다.
  • 순환 참조가 있는 복잡한 객체들을 복제하는 것은 매우 까다로울 수 있습니다.

다른 패턴과의 관계

  • 많은 디자인은 복잡성이 낮고 자식 클래스들을 통해 더 많은 커스터마이징이 가능한 팩토리 메서드로 시작해 더 유연하면서도 더 복잡한 추상 팩토리, 프로토타입 또는 빌더 패턴으로 발전해 나갑니다.

  • 추상 팩토리 클래스들은 팩토리 메서드들의 집합을 기반으로 하는 경우가 많습니다. 그러나 당신은 또한 프로토타입을 사용하여 추상 팩토리의 구상 클래스들의 생성 메서드들을 구현할 수도 있습니다.

  • 프로토타입커맨드 패턴의 복사본들을 기록에 저장해야 할 때 도움이 될 수 있습니다.

  • 데코레이터복합체 패턴을 많이 사용하는 디자인들은 프로토타입을 사용하면 종종 이득을 볼 수 있습니다. 프로토타입 패턴을 적용하면 복잡한 구조들을 처음부터 다시 건축하는 대신 복제할 수 있기 때문입니다.

  • 프로토타입은 상속을 기반으로 하지 않으므로 상속과 관련된 단점들이 없습니다. 반면에 은 복제된 객체의 복잡한 초기화가 필요합니다. 팩토리 메서드는 상속을 기반으로 하지만 초기화 단계가 필요하지 않습니다.

  • 때로는 프로토타입메멘토 패턴의 더 간단한 대안이 될 수 있으며, 이 패턴은 상태를 기록에 저장하려는 객체가 간단하고 외부 리소스에 대한 링크가 없거나 링크들이 있어도 이들을 재설정하기 쉬운 경우에 작동합니다.

  • 추상 팩토리들, 빌더들프로토타입들은 모두 싱글턴으로 구현할 수 있습니다.

코드 예시

C#으로 작성된 프로토타입 C++로 작성된 프로토타입 Go로 작성된 프로토타입 자바로 작성된 프로토타입 PHP로 작성된 프로토타입 파이썬으로 작성된 프로토타입 루비로 작성된 프로토타입 러스트로 작성된 프로토타입 스위프트로 작성된 프로토타입 타입스크립트로 작성된 프로토타입