본문 바로가기

algorithm/알고리즘

[디자인패턴][행위패턴] 전략 Strategy - C++

728x90

[모던 C++ 디자인 패턴] 책을 바탕으로 공부하는 내용을 정리한 내용이다.


Strategy Pattern

배열에 문자열 여러 개를 목록 정리해서 출력하려고 한다고 해보자. HTML이나 LaTeX의 경우 목록을 표현하기 위해서는 그 렌더링 언어만의 열림/닫힘 태그가 필요하다. 목록을 출력하는 일은 여러 경우마다 비슷한 부분이 있기도 하고 다른 부분도 있기도 하다. 이때 각 경우들 하나하나를 별개의 "전략"으로 취급할 수 있다.

목록을 출력하는 작업은 다음과 같은 전략으로 공식화할 수 있다. 이때 서로 다른 포맷마다 서로 다른 전략을 공식화하고 각 전략은 일반화되어 불변하는 상위 수준 텍스트 출력 알고리즘에 입력돼 세부 동작에 가변성을 부여할 수 있다.

  • 목록의 열림 태그와 항목을 출력한다.
  • 목록의 각 항목을 출력한다.
  • 목록의 닫힘 태그와 항목을 출력한다.

이 디자인 패턴은 런타임에 변경 가능한 동적 형태와 탬플릿으로 컴파일 타임에 결정되는 정적 형태가 있다.


동적 전략

단순한 텍스트 목록을 두 포맷으로 렌더링하는 것을 해보자.

enum class OutputFormat {
    markdown,
    html
};

"전략"의 골격은 아래의 베이스 클래스로 정의할 수 있다.

struct ListStrategy {
    virtual void start(ostringstream &oss) {};
    virtual void add_list_item(ostringstream &oss, const string &item) {}
    virtual void end(ostringstream &oss) {};
};

텍스트 처리 컴포넌트는 아래와 같다. 이 컴포넌트는 목록 처리를 위한 전용 멤버 함수 append_list()를 갖는다. oss는 결과가 출력될 버퍼이며, append_list()는 목록을 렌더링 하는 과정을 정의할 뿐만 아니라 렌더링에 적용할 전략도 갖는다.

struct TextProcessor {
public:
    void append_list(const vector<string> itmes) {
        list_strategy->start(oss);
        for ( auto &item : items) 
            lits_strategy->add_list_item(oss, item);
        list_strategy->end(oss);
    }
    
private:
    ostringstream oss;
    unique_ptr<ListStrategy> list_strategy;
};

 

위 코드에서는 컴포지션이 사용되고 있따. 컴포지션은 골격 알고리즘을 실체화된 구현으로 만들어주는 방법 중 하나이다.

컴포지션 대신 add_list_item()과 같은 버추얼 멤버 함수를 두어, 하위 클래스에서 오버라이딩하게 할 수도 있다. 그러한 방법은 템플릿 메서드 패턴이다.

이제 목록 렌더링 전략을 어떻게 구현하는지 살펴보자.

struct HTmlListStrategy: public ListStrategy {
    void start(ostringstream &oss) override {
        oss << "<ul>\n";
    }
    
    void end(ostringstream & oss) override {
        oss << "</ul>\n";
    }
    
    void add_list_item(ostringstream &oss, const string &item) override {
        oss << "<li>" << item << "</li>\n";
    }
};

struct MarkdownListStrategy : public ListStrategy {
    void add_list_item(ostringstream &oss, const string &item) override {
        oss << " * " << item << endl;
    }
};

HTML과 달리 Markdown에서는 열림/닫힘 태그가 필요 없기 때문에 add_list_item() 멤버 함수만 오버라이딩하면 된다.

이제 TextProcessor를 이용해 서로 다른 전략에 목록을 입력해 서로 다른 렌더링 결과를 얻을 수 있다.

TextProcessor tp;
tp.set_output_format(OutputFormat::markdown);
tp.append_list({"foo", "bar", "baz"});
cout << tp.str() << endl;

// 출력 결과
// * foo
// * bar 
// * baz

전략을 런타임 목적에 따라 선택할 수도 있다. 이 때문에 이 구현을 "동적 전략" 패턴이라고 부른다. set_output_format() 함수에서 전략의 선택이 이루어진다. 이 함수의 구현은 아래와 같이 단순하다.

void set_output_format(const OutputFormat format) {
    switch ( format ) {
    case OutputFormat::markdown:
        list_strategy = make_unique<MarkdownListStrategy>();
        break;
        
    case OutputFormat::html:
        list_strategy = make_unique<HtmlListStrategy>();
        break;
    }
}

정적 전략

템플릿 덕분에 어떤 전략이든 자동으로 타입에 맞추어 적용할 수 있다. 아래와 같이 TextProcessor 클래스에 약간에 수정만 하면 된다.

template <typename LS>
struct TextProcessor {
public:
    void append_list(const vector<string> items) {
        list_strategy.start(oss);
    
        for ( auto &item : items ) 
            list_strategy.add_list_item(oss, item);
    
        list_strategy.end(oss);
    }
    
    // 다른 함수는 변경 없음
    
private:
    ostringstream oss;
    LS list_strategy;
};

요약

전략 디자인 패턴은 알고리즘의 골격만을 정의하고 세부 구현은 컴포지션으로서 특정 전략을 선택적으로 채워 넣을 수 있게 한다. 실현 방법은 두 가지가 있다.

  • 동적 전략 : 사용될 전략을 단순 퐁니터 또는 참조로 갖는다. 전략을 바꾸고 싶을 때는 참조를 변경한다.
  • 정적 전략 : 컴파일 시점에 전략이 선택되어 고정된다. 나중에 전략을 바꿀 수 없다.

위 두 사항은 개발자의 선택 사항이다.

728x90

'algorithm > 알고리즘' 카테고리의 다른 글

[알고리즘]Knuth's optimization 크누스 최적화  (0) 2019.08.22