[Design Pattern] 프로토타입 패턴

olwooz·2023년 2월 11일

Design Pattern

목록 보기
5/22

프로토타입 패턴

이미 존재하는 객체의 클래스에 의존하지 않고 해당 객체를 복제할 수 있게 해주는 생성 패턴

문제

하나의 객체가 있고, 그 객체와 똑같은 복제본을 만들고 싶은 경우

같은 클래스의 새로운 객체 생성 → 원본 객체의 모든 필드 값을 새 객체에 복사?

- 객체의 몇몇 필드들이 private여서 객체의 외부에서 볼 수 없을 수 있음
- 객체의 복제본을 생성하려면 객체의 클래스를 알아야 함 → 코드 해당 클래스에 의존하게 됨
- 메서드의 파라미터가 특정 인터페이스를 따르는 모든 객체를 받아들이는 경우, 
  그 객체가 따르는 인터페이스만 알고 concrete 클래스는 알지 못함

해결책

프로토타입 패턴 - 실제로 복제되는 객체들에 복제 프로세스를 위임

복제를 지원하는 모든 객체에 공통 인터페이스 선언 → 그 인터페이스가 코드를 해당 객체의 클래스에 결합하지 않고도 객체를 복제할 수 있게 해줌, 인터페이스는 보통 단일 clone 메서드만 보유

clone 메서드 구현은 모든 클래스에서 매우 비슷함 → 현재 클래스 객체 생성 후 기존 객체의 모든 필드 값들을 전달

대부분의 언어들은 같은 클래스에 속하는 다른 객체들의 private 필드에 접근할 수 있게 하기 때문에 private 필드도 복사 가능

프로토타입 - 복제를 지원하는 객체

  • 객체에 아주 많은 양의 필드와 가능한 configuration들이 있을 때, 복제가 서브클래스의 대안이 될 수 있음
  • 다양한 방법으로 설정된 객체들의 집합을 만들고, 설정해놓은 특정 객체가 필요할 때 새로 만들지 않고 프로토타입을 복제

구조

1. 프로토타입 인터페이스 - 복제 메서드 선언, 대부분의 경우 단일 `clone` 메서드

2. concrete 프로토타입 클래스 - 복제 메서드 구현
    - 기존 객체의 데이터를 클론에 복사하는 것 외에, 
      연결된 객체들의 복제나 재귀 종속성 풀기와 같은 예외 처리 수행할 수도 있음
    
3. 클라이언트 - 프로토타입 인터페이스를 따르는 모든 객체의 복제본을 생성할 수 있음

프로토타입 레지스트리

- 자주 사용하는 프로토타입들에 쉽게 접근하는 방법 제공
- 미리 만들어져 복제될 준비가 된 객체들을 저장함
- 가장 간단한 프로토타입 레지스트리: `name → prototype` 해시맵
- 단순히 이름보다 더 나은 검색 기준이 필요한 경우 더 강력한 버전의 레지스트리 구축 가능

적용

복제하려는 객체의 concrete 클래스에 의존하면 안 되는 경우

- 써드 파티 코드에서 인터페이스를 통해 전달된 객체를 다루는 경우에 자주 발생하는 상황 
  → 객체의 concrete 클래스를 몰라서 의존하고 싶어도 할 수 없음
- 프로토타입 패턴 → 클라이언트 코드에게 복제를 지원하는 모든 객체를 다룰 수 있는 제너럴한 인터페이스를 제공해 
                  클라이언트 코드를 복제하려는 객체의 concrete 클래스에 의존하지 않게 함

각자의 객체들을 초기화하는 방법만 다른 서브클래스들의 수를 줄이고 싶은 경우

- 사용되기 전에 복잡한 설정이 필요한 클래스가 있다고 가정 → 
  클래스를 설정할 여러 가지 공통된 방법들이 존재하고, 그 코드들은 프로그램 전반에 흩어져 있음 
  → 중복을 줄이기 위해 여러 서브클래스들을 만들고 모든 공통 설정 코드를 생성자에 집어넣음 
  → 중복은 해결했지만 쓸데 없이 많은 서브클래스 존재
- 프로토타입 패턴 → 여러 가지 방법으로 설정된 미리 만들어진 객체들을 프로토타입으로 사용할 수 있게 해줌 
  → 서브클래스들을 인스턴스화하는 대신, 클라이언트는 단순히 적절한 프로토타입을 찾아 복제하면 됨

구현방법

1. 프로토타입 인터페이스를 생성해 `clone` 메서드를 선언, 
   만약 이미 존재하는 클래스 계층 구조가  있으면 모든 클래스들에게 메서드 추가

2. 프로토타입 클래스는 반드시 해당 클래스의 객체를 매개변수로 받는 대체 생성자를 정의해야 함, 
  생성자는 전달받은 객체의 클래스에 정의된 모든 필드의 값들을 새로 생성된 객체에 복사해줘야 함
   - 서브클래스를 변경하는 경우, 부모 생성자를 호출해 private 필드 복제를 처리하게 함
   - 언어에서 메서드 오버로딩을 지원하지 않으면 별도의 프로토타입 생성자를 만들 수 없음 
     → `clone` 메서드 내부에서 데이터 복사 수행
   - 그래도 기본 생성자에 데이터 복사 코드가 있는 것이 안전 
     → `new` 연산자를 호출한 직후 결과 객체가 완전히 설정된 상태로 반환되기 때문
     
3. `clone` 메서드는 대개 한 줄: 프로토타입 버전의 생성자로 `new` 연산자 실행
   - 모든 클래스는 명시적으로 `clone` 메서드를 오버라이드해 
     `new` 연산자를 자기 자신의 클래스 이름과 함께 사용해야 함, 
      그렇지 않으면 부모 클래스 객체가 생성될 수 있음
      
4. 자주 사용되는 프로토타입들의 카탈로그를 저장할 프로토타입 레지스트리 생성 (선택)
   - 새로운 팩토리 클래스로 구현하거나, 
     기본 프로토타입 클래스에 프로토타입을 가져오는 정적 메서드와 함께 넣을 수 있음
   - 이 메서드는 클라이언트 코드가 메서드에 전달하는 검색 기준에 따라 프로토타입 검색
   - 검색 기준은 간단한 string 태그일 수도 있고 복잡한 검색 파라미터의 집합일 수도 있음
   - 레지스트리가 적절한 프로토타입을 찾으면 복제해서 클라이언트에게 반환
   - 서브클래스 생성자들에 대한 직접 호출을 프로토타입 레지스트리의 팩토리 메서드 호출로 변경

장단점

장점

- 객체의 concrete 클래스와의 결합 없이 객체 복제 가능
- 초기화 코드의 중복 제거
- 복잡한 객체를 편하게 생성 가능
- 복잡한 객체의 설정 프리셋을 다룰 때 상속의 대안

단점

- 순환 참조가 있는 복잡한 객체의 복제가 까다로울 수 있음

다른 패턴과의 관계

- 많은 디자인은 팩토리 메서드(덜 복잡 & 자식 클래스들을 통해 더 많은 커스터마이징 가능)로 시작해 추상 팩토리, 
  프로토타입 또는 빌더(더 유연 & 더 복잡) 패턴으로 발전

- 추상 팩토리 클래스 - 팩토리 메서드들의 집합을 기반으로 하는 경우가 많지만, 
  프로토타입 패턴을 사용해 클래스의 메서드들을 구성할 수도 있음

- 프로토타입 - 커맨드의 복제본을 history에 저장할 때 도움을 줄 수 있음

- 컴포지트, 데코레이터 패턴을 많이 사용하는 설계는 프로토타입을 사용해 이득을 볼 수 있음 
  → 복잡한 구조를 처음부터 재생성하지 않고 복제 가능
  
- 프로토타입 - 상속 기반이 아니라서 상속의 단점 X, 하지만 복제된 객체의 복잡한 초기화 필요, 
  팩토리 메서드 - 상속 기반이지만 초기화 단계 필요 X
  
- 메멘토 패턴의 간단한 대안이 될 수 있음 
  → 상태를 기록에 저장하려는 객체가 간단하고 외부 리소스에 대한 링크가 없거나 링크를 재설정하기 쉬운 경우에 가능
  
- 추상 팩토리, 빌더, 프로토타입 패턴 모두 싱글턴 패턴으로 구현 가능

TypeScript 예제

/**
 * The example class that has cloning ability. We'll see how the values of field
 * with different types will be cloned.
 */
class Prototype {
    public primitive: any;
    public component: object;
    public circularReference: ComponentWithBackReference;

    public clone(): this {
        const clone = Object.create(this);

        clone.component = Object.create(this.component);

        // Cloning an object that has a nested object with backreference
        // requires special treatment. After the cloning is completed, the
        // nested object should point to the cloned object, instead of the
        // original object. Spread operator can be handy for this case.
        clone.circularReference = {
            ...this.circularReference,
            prototype: { ...this },
        };

        return clone;
    }
}

class ComponentWithBackReference {
    public prototype;

    constructor(prototype: Prototype) {
        this.prototype = prototype;
    }
}

/**
 * The client code.
 */
function clientCode() {
    const p1 = new Prototype();
    p1.primitive = 245;
    p1.component = new Date();
    p1.circularReference = new ComponentWithBackReference(p1);

    const p2 = p1.clone();
    if (p1.primitive === p2.primitive) {
        console.log('Primitive field values have been carried over to a clone. Yay!');
    } else {
        console.log('Primitive field values have not been copied. Booo!');
    }
    if (p1.component === p2.component) {
        console.log('Simple component has not been cloned. Booo!');
    } else {
        console.log('Simple component has been cloned. Yay!');
    }

    if (p1.circularReference === p2.circularReference) {
        console.log('Component with back reference has not been cloned. Booo!');
    } else {
        console.log('Component with back reference has been cloned. Yay!');
    }

    if (p1.circularReference.prototype === p2.circularReference.prototype) {
        console.log('Component with back reference is linked to original object. Booo!');
    } else {
        console.log('Component with back reference is linked to the clone. Yay!');
    }
}

clientCode();
// Output.txt

Primitive field values have been carried over to a clone. Yay!
Simple component has been cloned. Yay!
Component with back reference has been cloned. Yay!
Component with back reference is linked to the clone. Yay!

0개의 댓글