본문으로 바로가기

생성 패턴 - ⑥ 프로토타입 패턴

객체를 생성하는 일은 다소 번거롭고 많은 주의가 요구됩니다. 만약 생성 절차가 특별한 규칙을 따라야 한다면 객체를 생성하는 일은 더욱 까다로워집니다. 디자인 패턴에서 생성 패턴은 객체 생성의 어려움을 해소하기 위해 등장했습니다. 생성 패턴은 객체를 만드는 복잡한 과정을 추상화한 패턴입니다. 생성 패턴의 종류는 다음과 같습니다.

  • Factory Pattern
  • Factory Method Pattern
  • Abstract Factory Pattern
  • Builder Pattern
  • Singleton Pattern
  • Prototype Pattern

이번 글에서는 여섯 번째 프로토타입 패턴에 대해서 다뤄보도록 하겠습니다.

Prototype Pattern

프로토타입 패턴은 객체를 생성할 때 매번 객체를 전체 초기화하는 대신 미리 부분적으로 만들어진 프로토타입 객체를 복제한 후 약간의 수정만 하여 새로운 객체를 생성하는 디자인 패턴입니다. 프로토타입 패턴에서는 객체의 복제(깊은 복사)가 중요합니다. 이번 글에서는 프로토타입 객체를 어떻게 복제하여 새로운 객체를 만들어낼 수 있는지 몇 가지 예제로 살펴보겠습니다. 

기본적인 프로토타입 패턴 구현

우선 가장 기본적인 프로토타입 패턴을 살펴보겠습니다. 아래와 같이 먼저 worker라는 프로토 타입 객체를 생성합니다. 그리고 john 객체를 만들기 위해서는 worker를 복사한 후 몇 가지 속성만 더 추가로 설정하여 생성할 수 있습니다. 이러한 방식이 가장 간단한 프로토타입 패턴입니다.

struct Address
{
    string street, city;
    int suite;
};
struct Contact
{
    string name;
    Address address;
};
int main()
{
    Contact worker{ "", Address{"123 East Dr", "London", 0} };
    Contact john = worker;
    john.name = "John Doe";
    john.address.suite = 10;

    return 0;
}

하지만 위와 같은 방식에는 다음과 같은 한계가 있습니다.

  • Contact에서 직접 리소스를 관리하는 (포인터 혹은 스마트 포인터) 경우 깊은 복사를 위한 기능(복사 생성자 혹은 복사 인터페이스)이 제공되어야 합니다.
  • 프로토타입으로 객체를 복사한 이후에 사용자가 설정해야 할 속성이 많다면 사용자는 혼란을 느낄 수 있습니다. 이는 팩토리 패턴을 함께 적용하여 조금 더 편리하게 개선할 수 있습니다.

깊은 복사가 필요한 경우

위의 코드에서 Address를 다음과 같이 원시 포인터로 활용하는 경우를 생각해봅시다. 

struct Contact
{
    string name;
    Address* address; // 원시 포인터로 활용
};

그렇다면 address의 정보에 대해서 깊은 복사를 진행해야 합니다. 깊은 복사를 하지 않을 경우 원본 Address 데이터는 유지한 채 Address객체를 가리키는 포인터 값만 복사되어서 서로 다른 Contact객체가 동일한 Address를 참조하는 문제가 발생하기 때문입니다. (Rule of five 원칙에 따라 이동, 소멸 관련 코드도 직접 작성해야 하지만 여기에서는 예제의 편의를 위해서 복사와 관련된 부분만 구현하였습니다. - Rule of five 설명 보기)

 

깊은 복사가 필요한 경우 다음과 같이 복사 생성자를 구현함으로써 프로토타입 패턴을 제작할 수 있습니다.

struct Address
{
    // 기본 생성자
    Address(string inStreet, string inCity, int inSuite) : street(move(inStreet)), city(move(inCity)), suite(inSuite) {}
    // 복사 생성자(Deep copy)
    Address(const Address& other) : street(other.street), city(other.city), suite(other.suite) {}
    string street, city;
    int suite;
};
struct Contact
{
    // 기본 생성자
    Contact(string inName, Address* inAddress) : name(move(inName)), address(inAddress) {}
    // 복사 생성자(Deep copy)
    Contact(const Contact& other) : name( other.name ), address( new Address(*other.address) ) {}
    string name;
    Address* address;
};
int main()
{
    Contact worker{ "", new Address{"123 East Dr", "London", 0} };
    Contact john = worker;
    john.name = "John Doe";
    john.address->suite = 10;

    return 0;
}

이제 복사 생성자가 생겼기 때문에 정상적으로 프로토타입 패턴이 동작합니다. 하지만 이 방법은 다음과 같은 어려움이 존재합니다.

  • 모든 종류의 복사 생성자를 직접 작성하여야 합니다.
  • 복사 생성자뿐만 아니라 복사 대입 연산자에 대한 정의도 해야 합니다.

만약 적절한 복사 대입 연산자가 정의되어 있지 않다면, 컴파일 에러로 감지되지는 않지만 이상 동작을 하여 찾기 어려운 버그를 만들어 낼 수 있습니다. 따라서 다음과 같이 Clone 인터페이스를 상속받아 별도의 Clone 기능을 제공하는 방법을 고려해볼 수 있습니다.

 

template <typename T> struct Cloneable 
{
    virtual T Clone() const = 0;
};

프로토타입 팩토리

위의 예제를 다시 살펴보면 프로토 타입으로부터 객체를 복사한 뒤 사용자가 설정해야 할 속성이 아직 많습니다. 이 과정에서 사용자는 혼란스러울 수 있습니다. 이런 경우 적용할 수 있는 디자인 패턴으로 우리는 팩토리 패턴을 다뤘습니다. 프로토 타입 패턴과 팩토리 패턴을 함께 적용하여 객체 생성을 더욱 간단하고 명료하게 진행할 수 있습니다.

 

예를 들어 본사(main)에서 근무하는 직원들의 프로토타입 객체와 지점(aux)에서 근무하는 직원들의 프로토타입 객체가 존재한다고 생각해봅시다. 그렇다면 본사 직원 프로토타입과 지점 직원 프로토타입을 static으로 정의한 후 이를 활용 객체를 생성하는 팩토리 멤버 함수 NewMainOfficeEmployee와 NewAuxOfficeEmployee를 만들 수 있습니다.

// Address struct는 위와 동일
// Contact struct는 위와 동일
class EmployeeFactory
{
public:
    static Contact main;
    static Contact aux;

    static unique_ptr<Contact> NewMainOfficeEmployee(string name, int suite)
    {
        return NewEmployee(name, suite, main);
    }
    static unique_ptr<Contact> NewAuxOfficeEmployee(string name, int suite)
    {
        return NewEmployee(name, suite, aux);
    }
private:
    static unique_ptr<Contact> NewEmployee(string name, int suite, Contact& proto)
    {
        auto result = make_unique<Contact>(proto);
        result->name = name;
        result->address->suite = suite;
        return result;
    }
};
Contact EmployeeFactory::main{ "", new Address{"123 East Dr", "London", 0} };
Contact EmployeeFactory::aux{ "", new Address{"78 East Dr", "London", 0} };

int main()
{
    auto john = EmployeeFactory::NewMainOfficeEmployee("John Doe", 101);
    auto jane = EmployeeFactory::NewMainOfficeEmployee("Jane Doe", 204);

    return 0;
}

 

정리

  • 미리 부분적으로 만들어진 프로토타입 객체를 복제한 후 약간의 수정을 하여 새로운 객체를 만드는 방법을 프로토타입 패턴이라고 한다.
  • 프로토타입 패턴은 복제가 핵심이다. 특히 C++에서는 사용하기 위해서는 객체 복제를 안전하게 하기 위한 방법을 고려해야 한다.
  • 프로토타입 패턴을 팩토리 패턴과 결합할 경우 더욱 안전하고 편한 객체 생성 방법을 제공할 수 있다.

<출처>

유튜브 채널 "코드없는 프로그래밍": https://www.youtube.com/channel/UCHcG02L6TSS-StkSbqVy6Fg

길벗 출판사: 모던 C++ 디자인 패턴(Dmitri Nesteruk 저)