[모던C++디자인패턴] 4. 프로토타입

짜장범벅·2022년 6월 4일
0

모던CPP디자인패턴

목록 보기
3/4

4.1 객체 생성

이미 잘 설정된 객체가 있다면 같은 부분을 복제하는 것이 가장 쉽다. 예를 들면,

Contact john{ "John Doe", Address{"123 East Dr", "London", 10} };
Contact jane{ "Jane Doe", Address{"123 East Dr", "London", 11} };

에서 john과 jane은 사무실의 방만 다르고 같은 빌딩에서 일한다(cc인 듯). 아마 같은 회사에서 일하는 사람들도 주소가 같을 것이다. 즉 수많은 객체가 같은 값으로 중복되게 초기화되는 작업이 발생한다. 어떻게 반복된 중복 설정을 피할 수 있을까?

4.2 평범한 중복 처리

예를 들어 연락처와 주소를 아래와 같이 정의하자

struct Address{
    string street;
    string city;
    int suite;
}

struct Contact{
    string name;
    Address address;
}

...

Contact worker{"", Address{"123 East Dr", "London", 0}}; //prototype instance

Contact john = worker; //copy and change some data

john.name = "John Doe";
john.address.suite = 10;

위 코드에서 worker 객체를 선언한 후에 일부 데이터를 수정했다. 만약 Address 객체가 아래와 같이 포인터로 되어있다면?

struct Contact{
    string name;
    Address *address; //or shared_ptr
}

...

Contact john = prototype; //copy address pointer from prototype to john

위 코드의 경우 prototype의 Address 포인터가 john으로 복제되었기 때문에 의도치 않은 복사 문제를 발생시킬 수 있다.

4.3 복제 생성자를 통한 중복 처리

만약 4.2의 마지막처럼 Address를 포인터로 저장한다면 객체의 복제 생성자를 잘 정의하면 된다.

첫 번째 방법은 아래와 같다.

Contact(const Contact& other): name{other.name} //initialize name from other
{   //make new Address instance
    address = new Address(
        other.address->street,
        other.address->city,
        other.address->suite
    );
}

단 위의 경우 other의 멤버 하나하나마다 일일히 값을 집어넣기 때문에 Address의 항목이 바뀌면 일일히 추가해야 한다. 이를 해결하기 위해 Address의 복제 생성자를 정의하자.

Address(const string& street, const string& city, const int suite)
    : street(street),
    city{city},
    suite{suite} {}

...

Contact(const Contact& other)
    : name{other.name}
    , address{ new Address{*other.address}} {}

...

Contact& operator=(const Contact& other){
    if (this == &other)
        return *this;
    
    name = other.name;
    address = other.address;

    return *this;
}

이전 코드와 비교했을 때 일일히 other.address->xx를 이용해 객체를 초기화할 필요가 없어진다.

단, 위 코드는 아래와 같은 문제점이 있다.
1. 복제 생성자를 하나하나 구현해야 한다.
2. 복제 생성자나 대입 연산자 구현이 누락되더라도 컴파일이 되기 때문에 나중에 문제가 될 수 있다.
3. 만약 shared_ptr이 아니라 이중 포인터 혹은 unique_ptr을 사용한다면 구현이 조금 더 복잡해진다.

4.4 직렬화

다른 언어에서는 클래스를 직렬화 할 수 있어서 쉽게 복제가 가능하다. 하지만 C++에서는 직렬화를 지원하지 않는다.

단, Boost 라이브러리의 Boost.Serialization을 이용하면 직렬화 할 수 있다.

개인적인 의견: Boost는 C++ 프로젝트에서 잘 사용되지 않는 것 같다. 우선 표준 C++에 통합된 경우 추후에 코드 수정이 필요하며, 현업에서는 신뢰성의 문제로 잘 사용하지 않는 것 같다.

이하 생략...

4.5 프로토타입 팩터리

자주 사용할 객체들이 미리 정해져 있다면 아래와 같이 전역 변수에 저장해 복사하는 방식으로 사용할 수 있다.

/* Contact.hpp */
Contact main{ "", new Address{ "123 East Dr", "London", 0 } };
Contact aux{ "", new Address{ "123B East Dr", "London", 0 } };

하지만 목적에 맞는 복제본을 요구받는 순간에 객체를 만들어 제공하는 것이 훨씬 깔끔하다. 예를 들면, 아래 코드에서는 객체의 unique_ptr을 리턴하는 편의 함수가 제공된다.

struct EmployeeFactory{
    static Contact main;
    static Contact aux;

    static unique_ptr<Contact> NewMainOfficeEmployee(string name, int suit){
        return NewEmployee(name, suit, main);
    }

    static unique_ptr<Contact> NewAuxOfficeEmployee(string name, int suit){
        return NewEmployee(name, suit, 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;
    }
}

...

auto john = EmployeeFactory::NewAuxOfficeEmployee("John Doe", 123);
auto jane = EmployeeFactory::NewMainOfficeEmployee("Jane Doe", 125);

위처럼 팩터리를 이용하면, 새로 설정해야 하는 부분의 누락을 방지할 수 있다.

profile
큰일날 사람

0개의 댓글