책정리/GoF 디자인 패턴

4장 부분 부분 생성을 통한 전체 객체 생성 문제 : BUILDER 패턴(Builder 패턴)

GONII 2019. 1. 14. 21:46

      때때로 클래스의 생성자를 불러서 객체를 생성하는 것이 아니라 객체를 구성하는 부분부분을 따로 생성하고 이를 조합해서 전체 객체를 생성해주는 것이 보다 효과적일 때가 있다. 한꺼번에 객체를 생성하는 것이 불가능하거나, 가능하더라도 부분 부분 생성한 전체 객체로 조합하는 것이 훨씬 비용이 적게 경우가 이에 해당한다.

      • 문제 사례 설명

      자동 번역 소프트웨어를 개발해서 매뉴얼을 제작하기로 결정함.

      개발해야 소프트웨어는 한국어로 매뉴얼을 입력하면 영어, 일본어, 프랑스어로 된 매뉴얼을 만들어내는 것을 목적으로 한다. 여기서 매뉴얼에 사용되는 문장은 평서문, 의문문, 명령문으로만 구성되어 있으며, 자동 번역 소프트웨어는 매뉴얼을 문장 단위로 번역하게 된다.

      • 다양한 접근 방법 및 BUILDER 패턴

      생성해야 영어, 일본어, 프랑스어로 된 매뉴얼을 각각 객체라고 해보자. 경우 우리가 해결해야 하는 문제는 각각의 객체를 생성해주는 것이다. 그런데 여기서 매뉴얼 객체의 생성은 통상적인 객체 생성 방식과는 달리 전체 객체를 한꺼번에 생성하는 것이 아니라 매뉴얼을 구성하는 개별 문장을 단위로 객체의 부분 부분을 생성한 이를 조합해주는 방식이어야 것이다. 왜냐하면 어차피 번역은 문장씩 이루어질 것이기 때문이다.

      • 기본적인 방법: 함수 형태 접근 방식

      // 예제 : 매뉴얼 번역을 위한 함수 형태의 접근 방식

      #include <iostream>

      #include <fstream>

      using namespace std;

       

      const int UNDEF_SENTENCE = 0;

      const int NORMAL_SENTENCE = 1;

      const int INTERROGATIVE_SENTENCE = 2;

      const int IMPERATIVE_SENTENCE = 3;

      const int TO_ENGLISH = 1;

      const int TO_JAPANESE = 2;

      const int TO_FRENCH = 3;

       

      class Sentence

      {

      public:

      Sentence()

      {

      data_ = "";

      type_ = UNDEF_SENTENCE;

      }

       

      int GetType() { return type_; }

      string GetString() { return data_; }

       

      void SetSentenceData(string s)

      {

      SetSentenceType(s);

      data_ = s;

      }

       

      protected:

      void SetSentenceType(string s)

      {

      // 문장 유형을 판단해서 type_에 설정, default는 평서문

      type_ = NORMAL_SENTENCE;

      }

       

      private:

      int type_;

      string data_;

      };

       

      class Manual

      {

      public:

      string GetContents() { return contents_; }

      void AddContents(string s) { contents_ += s; }

       

      private:

      string contents_;

      };

       

      Sentence GetSentence(ifstream&);

      string TransNormalSentence(string, int);

      string TransInterrogativeSentence(string, int);

      string TransImperativeSentence(string, int);

       

      void DoTranslate(char* pInFile, Manual& out, int wantedLang)

      {

      ifstream ifs(pInFile);

      if (!ifs)

      {

      cout << "Can't Open File : " << pInFile << endl;

      return;

      }

       

      string result;

      Sentence next;

       

      while (!(next = GetSentence(ifs)).GetString().empty())

      {

      switch (next.GetType())

      {

      case NORMAL_SENTENCE:        // 평서문

      result = TransNormalSentence(next.GetString(), wantedLang);

      break;

      case INTERROGATIVE_SENTENCE:        // 의문문

      result = TransInterrogativeSentence(next.GetString(), wantedLang);

      break;

      case IMPERATIVE_SENTENCE:

      result = TransImperativeSentence(next.GetString(), wantedLang);

      break;

      default:

      cout << "Untranslatable sentence type" << endl;

      break;

      }

       

      out.AddContents(result);

      }

      }

       

      Sentence GetSentence(ifstream& ifs)

      {

      int c;

      string s;

      Sentence sentence;

      while ((c = ifs.get()) != EOF)

      {

      s += c;

      if (c == '?' || c == '.')

      break;

      }

       

      sentence.SetSentenceData(s);

      return sentence;

      }

       

      string TransNormalSentence(string s, int wantedLang)

      {

      string output;

      switch (wantedLang)

      {

      case TO_ENGLISH:

      // 영어로 문장 번역

      break;

      case TO_JAPANESE:

      // 일어로 문장 번역

      break;

      case TO_FRENCH:

      // 프랑스어로 문장 번역

      break;

      default:

      break;

      }

       

      return output;

      }

       

      string TransInterrogativeSentence(string s, int wantedLang)

      {

      string output;

      switch (wantedLang)

      {

      case TO_ENGLISH:

      // 영어로 문장 번역

      break;

      case TO_JAPANESE:

      // 일어로 문장 번역

      break;

      case TO_FRENCH:

      // 프랑스어로 문장 번역

      break;

      default:

      break;

      }

       

      return output;

      }

      string TransImperativeSentence(string s, int wantedLang)

      {

      string output;

      switch (wantedLang)

      {

      case TO_ENGLISH:

      // 영어로 문장 번역

      break;

      case TO_JAPANESE:

      // 일어로 문장 번역

      break;

      case TO_FRENCH:

      // 프랑스어로 문장 번역

      break;

      default:

      break;

      }

       

      return output;

      }

       

      int main()

      {

      Manual out;

      DoTranslate("input.txt", out, TO_ENGLISH);

      cout << out.GetContents().c_str() << endl;

      return 0;

      }

      DoTranslate() 함수는 인자로 주어진 파일로부터 문장씩 읽어 이를 번역하고, 결과를 out이라는 Manual 객체에 저장하는 역할을 한다. 실제 번역 작업을 수행하는 함수들은 3개의 TransNormalSentence(), TransInterrogativeSentence(), TransImperativeSentence() 함수들이다.

      이런 접근 방식은 요구 사항이 추가되거나 변경될 경우 수정 범위가 불분명하며, 개발된 모듈을 재사용하고자 경우에도 재사용할 모듈의 경계가 불분명해서 일일이 재사용할 부분들을 가려내야 한다는 문제가 존재한다.

      예를 들어 위의 DoTranslate() 함수를 재사용한다고 해볼 , 함수를 재사용하기 위해서 내부에서 불러주는 함수들을 일일이 찾아내어 재사용할 모듈에 포함시켜야 것이다. 그런 다음에도 빠진 부분이 없는지 일일이 테스트해야 것이다.

      • 좀더 나은 방법: 번역 전담 클래스 활용 방식

      일반 함수 형태의 접근, 구조적 개념(Structured Paradigm) 기반한 접근이 가지는 가장 문제 중의 하나는 바로 요구 사항의 추가, 변경 영향을 받는 범위가 불분명하고, 모듈의 재사용 시에도 재사용할 모듈의 경계가 불확실하다는 것이다.

      이런 문제를 개선하기 위해 등장한 것이 바로 객체지향 개념(Object Oriented Paradigm) 기반한 접근 방식이다. 방식은 명확히 구분되는 클래스를 기준으로 소프트웨어의 변경이나 재사용이 가능하게 만들어주는데, 이를 메뉴얼 번역을 위한 소프트웨어에 적용한다면 다음과 같은 형태가 것이다.

      예제) 메뉴얼 번역을 위한 전담 클래스 접근 방식

      #include <fstream>

      #include <string>

      #include <iostream>

      using namespace std;

       

      const int UNDEF_SENTENCE = 0;

      const int NORMAL_SENTENCE = 1;

      const int INTERROGATIVE_SENTENCE = 2;

      const int IMPERATIVE_SENTENCE = 3;

      const int TO_ENGLISH = 1;

      const int TO_JAPANESE = 2;

      const int TO_FRENCH = 3;

       

      class Sentence

      {

      public :

      Sentence()

      {

      data_ = "";

      type_ = UNDEF_SENTENCE;

      }

      int GetType() { return type_; }

      string GetString() { return data_; }

       

      void SetSentenceData(string s)

      {

      SetSentenceType(s);

      data_ = s;

      }

      protected:

      void SetSentenceType(string s)

      {

      // 문장 유형을 판단하여 type_에 설정, defualt 평서문

      type_ = NORMAL_SENTENCE;

      }

      private:

      int type_;

      string data_;

      };

       

      class Manual

      {

      public:

      string GetConstents() { return contents_; }

      void AddContents(string s) { contents_ += s; }

      private:

      string contents_;

      };

       

      class Translator

      {

      public :

      Manual GetResult() { return result_; }

      void TransNormalSentence(string s, int wantedLang)

      {

      string output;

      switch (wantedLang)

      {

      case TO_ENGLISH:

      // 영어로 문장 번역

      break;

      case TO_FRENCH:

      // 프랑스어로 문장 번역

      break;

      case TO_JAPANESE:

      // 일어로 문장 번역

      break;

      default:

      break;

      }

      result_.AddContents(output);

      }

       

      void TransInterrogativeSentence(string s, int wantedLang)

      {

      string output;

      switch (wantedLang)

      {

      case TO_ENGLISH:

      // 영어로 문장 번역

      break;

      case TO_FRENCH:

      // 프랑스어로 문장 번역

      break;

      case TO_JAPANESE:

      // 일어로 문장 번역

      break;

      default:

      break;

      }

      result_.AddContents(output);

      }

      void TransImperativeSentence(string s, int wantedLang)

      {

      string output;

      switch (wantedLang)

      {

      case TO_ENGLISH:

      // 영어로 문장 번역

      break;

      case TO_FRENCH:

      // 프랑스어로 문장 번역

      break;

      case TO_JAPANESE:

      // 일어로 문장 번역

      break;

      default:

      break;

      }

      result_.AddContents(output);

      }

      private:

      Manual result_;

      };

       

      class Director

      {

      public :

      void DoTranslate(char* pInFile, Translator& t, int wantedLang)

      {

      ifstream ifs(pInFile);

      if (!ifs)

      {

      cout << "Can't Open File : " << pInFile << endl;

      return;

      }

       

      Sentence next;

      while (!(next = GetSentence(ifs)).GetString().empty())

      {

      switch (next.GetType())

      {

      case NORMAL_SENTENCE:

      t.TransNormalSentence(next.GetString(), wantedLang);

      break;

      case INTERROGATIVE_SENTENCE:

      t.TransInterrogativeSentence(next.GetString(), wantedLang);

      break;

      case IMPERATIVE_SENTENCE:

      t.TransImperativeSentence(next.GetString(), wantedLang);

      break;

      default:

      cout << "Untranslatable sentence type" << endl;

      break;

      }

      }

      }

      protected:

      Sentence GetSentence(ifstream& ifs)

      {

      int c;

      string s;

      Sentence sentence;

      while ((c = ifs.get()) != EOF)

      {

      s += c;

      if (c == '?' || c == '.')

      break;

      }

      sentence.SetSentenceData(s);

      return sentence;

      }

      };

       

      int _tmain(int argc, _TCHAR* argv[])

      {

      Director d;

      Translator t;

      d.DoTranslate("input.txt", t, TO_ENGLISH);

       

      Manual out = t.GetResult();

      cout << out.GetConstents() << endl;

      }

      전체 번역 과정은 개의 클래스로 구분되는데, Director 클래스는 번역할 문장을 선별하는 역할을 수행하고, Translator 클래스는 문장의 종류별로 실제 번역을 수행하는 역할을 담당한다. 이처럼 개의 클래스로 역할을 분산시킬 경우 얻을 있는 장점은 각각의 역할에 대한 변경 요청이 있을 다른 역할을 수행하는 클래스와는 독립적으로 수정이 가능하다는 점이다.

      이처럼 문제 해결을 위해 클래스를 활용하면 요구 사항의 변화에 따라 수정할 부분이 생길 경우 수정 범위를 클래스 내부로 한정시킬 있고, 이들 모듈을 재사용하고 싶을 경우에도 클래스를 재사용의 단위로 구분지을 있는 등의 장점을 얻게 된다.

      소스와 같은 접근 방법에도 여전히 문제는 존재한다. 새로운 국가 번역이 추가되었을 경우 기존 소스코드를 일일이 분석해서 수정해야 한다는 부담이 존재한다.

      • 패턴 활용 방법: BUILDER 패턴

      객체지향 설계에서 새로운 요구 사항을 기존의 설계와는 독립적으로 추가 반영하기 위한 대표적인 도구는 바로 클래스 상속(Inheritance)이다.

      클래스를 상속하자면 우선 클래스를 세분화시켜나갈 기준을 선정해야 것이다. 그런데 여기서 우리의 목적은 클래스 상속을 통해 새로운 번역 대상 언어의 추가를 쉽게 만들어 주는 것이다. 따라서 메뉴얼 번역 소프트웨어의 경우 클래스 상속의 기준은 번역 대상 언어로 선정하면 것이다. 이런 기준으로 클래스 상속 구조를 설계해보면 아래와 같은 형태가 것이다.


      클래스 설계를 자세히 살펴보면 Translator 하위 클래스들은 평서문이냐, 의문문이냐 명령문이냐에 따라 문장씩 번역을 수행할 있는 인터페이스를 제공하며, 전체 메뉴얼 객체는 이들 문장의 번역이 완료된 결과로 GetResult() 멤버 함수에 의해 얻어지는 형태다. , 문제에서 달성하려고 했던 목적대로 객체를 구성하는 부분 부분 생성을 반복하여 전체 객체를 생성하는 형태다. 그림 4-1 번역 대상 언어를 기준으로 클래스 상속을 정의했기 때문에 새로 추가할 번역 대상 언어가 있을 경우에는 기존의 소스코드에 영향을 주지 않고 새로운 하위 클래스만 추가하면 되는 장점을 가진다.

      이처럼 객체를 생성하되, 객체를 구성하는 부분 부분을 먼저 생성하고, 이를 조합함으로써 전체 객체를 생성하며, 생성할 객체의 종류가 손쉽게 추가, 확장이 가능하게 고안된 [그림 4-1] 같은 설계를 Builder패턴이라고 한다.

      Translator 하위 클래스들이 객체를 생성하는 Builder 역할을 하며, Translator 클래스는 Abstract Base Class 정의되었다. 여기서 Translator 클래스가 Abstract Base Class 정의된 이유는 어떤 하위 클래스의 객체가 사용되더라도 이를 사용하는 Director 객체에게는 동일한 인터페이스를 보장해주기 위한 목적이다. 이처럼 대부분의 Builder 패턴에서는 Builder 클래스들을 대표하기 위한 Abstract Base Class 정의하게 된다.

      • 샘플 코드

      [소스 4-3] 메뉴얼 번역을 위한 Builder 패턴을 적용한 소스코드

      // ConsoleApplication1.cpp : 콘솔 응용 프로그램에 대한 진입점을 정의합니다.

      //

       

      #include "stdafx.h"

      #include <fstream>

      #include <string>

      #include <iostream>

      using namespace std;

       

      const int UNDEF_SENTENCE = 0;

      const int NORMAL_SENTENCE = 1;

      const int INTERROGATIVE_SENTENCE = 2;

      const int IMPERATIVE_SENTENCE = 3;

      const int TO_ENGLISH = 1;

      const int TO_JAPANESE = 2;

      const int TO_FRENCH = 3;

       

      class Sentence

      {

      public :

      Sentence()

      {

      data_ = "";

      type_ = UNDEF_SENTENCE;

      }

      int GetType() { return type_; }

      string GetString() { return data_; }

       

      void SetSentenceData(string s)

      {

      SetSentenceType(s);

      data_ = s;

      }

      protected:

      void SetSentenceType(string s)

      {

      // 문장 유형을 판단하여 type_에 설정, defualt 평서문

      type_ = NORMAL_SENTENCE;

      }

      private:

      int type_;

      string data_;

      };

       

      class Manual

      {

      public:

      string GetConstents() { return contents_; }

      void AddContents(string s) { contents_ += s; }

      private:

      string contents_;

      };

       

      class Translator

      {

      public :

      Manual GetResult() { return result_; }

      virtual void TransNormalSentence(string s) = 0;

      virtual void TransInterrogativeSentence(string s) = 0;

      virtual void TransImperativeSentence(string s) = 0;

      protected:

      Manual result_;

      };

       

      class EnglishTranslator : public Translator

      {

      public:

      void TransNormalSentence(string s)

      {

      string output;

      // s를 영어로 번역

      result_.AddContents(output);

      }

      void TransInterrogativeSentence(string s)

      {

      string output;

      // s를 영어로 번역

      result_.AddContents(output);

      }

      void TransImperativeSentence(string s)

      {

      string output;

      // s를 영어로 번역

      result_.AddContents(output);

      }

      };

       

      class JapanesTranslator : public Translator

      {

      public:

      void TransNormalSentence(string s)

      {

      string output;

      // s를 일어로 번역

      result_.AddContents(output);

      }

      void TransInterrogativeSentence(string s)

      {

      string output;

      // s를 일어로 번역

      result_.AddContents(output);

      }

      void TransImperativeSentence(string s)

      {

      string output;

      // s를 일어로 번역

      result_.AddContents(output);

      }

      };

       

      class FrenchTranslator : public Translator

      {

      public:

      void TransNormalSentence(string s)

      {

      string output;

      // s를 프랑스어로 번역

      result_.AddContents(output);

      }

      void TransInterrogativeSentence(string s)

      {

      string output;

      // s를 프랑스어로 번역

      result_.AddContents(output);

      }

      void TransImperativeSentence(string s)

      {

      string output;

      // s를 프랑스어로 번역

      result_.AddContents(output);

      }

      };

       

      class Director

      {

      public :

      void DoTranslate(char* pInFile, Translator& t)

      {

      ifstream ifs(pInFile);

      if (!ifs)

      {

      cout << "Can't Open File : " << pInFile << endl;

      return;

      }

       

      Sentence next;

      while (!(next = GetSentence(ifs)).GetString().empty())

      {

      switch (next.GetType())

      {

      case NORMAL_SENTENCE:

      t.TransNormalSentence(next.GetString());

      break;

      case INTERROGATIVE_SENTENCE:

      t.TransInterrogativeSentence(next.GetString());

      break;

      case IMPERATIVE_SENTENCE:

      t.TransImperativeSentence(next.GetString());

      break;

      default:

      cout << "Untranslatable sentence type" << endl;

      break;

      }

      }

      }

      protected:

      Sentence GetSentence(ifstream& ifs)

      {

      int c;

      string s;

      Sentence sentence;

      while ((c = ifs.get()) != EOF)

      {

      s += c;

      if (c == '?' || c == '.')

      break;

      }

      sentence.SetSentenceData(s);

      return sentence;

      }

      };

       

      int _tmain(int argc, _TCHAR* argv[])

      {

      Director d;

      EnglishTranslator t;

      d.DoTranslate("input.txt", t);

       

      Manual out = t.GetResult();

      cout << out.GetConstents() << endl;

      }

      Director 클래스의 DoTranslate() 함수는 이상 번역 대상 언어를 함수 인자로 받지 않는다. 대신 번역 대상 언어에 따라 Translator 하위 클래스 전체를 인자로 전달받게 된다. 마찬가지로 Translator 하위 클래스의 멤버 함수들도 이상 번역 대상 언어를 인자로 전달받지 않는다. 왜냐하면 구체적인 Translator 객체의 자료형이 번역 대상 언어를 결정짓기 때문이다.

      • 구현 관련 사항

      Builder패턴은 객체를 생성하되 객체를 구성하는 부분들을 생성하는 단계와 부분별로 생성된 것을 조합해서 전체 객체를 구성하는 단계로 나누어진다. 따라서 Builder 패턴에서 객체를 생성하는 역할을 담당하는 Builder 클래스는 크게 가지 유형의 인터페이스를 가져야 한다. 하나는 객체의 부분을 생성하기 위한 인터페이스고, 다른 하나는 부분별로 생성된 것들을 조합해서 전체 객체를 구성하여 되돌리기 위한 인터페이스이다.

       

      이처럼 Builder 패턴은 객체를 구성하는 부분들을 생성한 이를 조합하는 형태이므로, Builder 클래스를 설계할 때에는 객체를 어떤 단위로 쪼개어 생성하고, 어떻게 조합할지에 대한 과정들을 분석해서 Builder 모든 하위 클래스들이 동일한 인터페이스를 가질 있도록 만들어 주어야 한다. 특히 객체를 생성하기 위해 부분별로 생성한 것을 조합하는 과정이 부분을 생성한 것을 단순히 덧붙이는 형태가 아니라, 추가적인 작업을 필요로 하는 경우에는 충분한 주의를 기울일 필요가 있다.

       

      다음으로  Builder 패턴의 구현과 관련해서 고려해볼만한 사항으로는, Builder 패턴에 의해 생성되는 객체들이 서로 어떤 관계를 가질 필요가 있는가에 대한 것이다. Builder 하위 클래스들이 생성하는 객체의 자료형 클래스 상속 등의 형태로 서로 관련될 필요가 있는가 하는 것이다.

       

      또하나 고려해볼 사항은 Builder 패턴의 최상위 클래스가 반드시 Abstract Base Class 정의되어야 하는가에 대한 것이다.

      앞서 언급한 번역 소프트웨어에서는 Translator 클래스에 Abstract Base Class 정의되었었다. , 내부에 Pure Virtual 멤버 함수를 포함하고 있는데 반드시 이런 방식을 따를 필요는 없다. 오히려 Translator 클래스에서 멤버 함수에 대해 기본적인 구현을 해준다면, 하위 클래스들이 불필요하게 멤버함수를 구현할 필요가 줄어드는 장점이 있을 있다.

      • Builder 패턴 정리

      Builder패턴의 일반적인 구조를 살펴보면 [그림 4-2] 같다.


       

      Builder패턴이 유용한 경우

      • 복잡한 객체를 생성하는 있어 객체를 구성하는 부분 부분들을 생성한 그것을 조합해도 객체의 생성이 가능할 , 객체의 부분 부분을 생성하는 것과 그들을 조합해서 전체 객체를 생성하는 것이 서로 독립적으로 이루어질 있는 경우
      • 서로 다른 표현 방식을 가지는 객체를 동일한 방식으로 생성하고 싶을

      Builder 패턴의 장단점

      • Builder 클래스는 Director 클래스에게 객체를 생성할 있는 인터페이스를 제공한다. 대신 생성되는 객체의 내부 구조나 표현 방식, 조합 방법 등은 Builder 클래스 내부로 숨긴다. 이런 형태이기 때문에 Builder 패턴에서는 생성되는 객체의 내부 구조를 변경시킬 필요가 있을 경우 기존 소스코드에는 영향을 주지 않고, 새로운 하위 클래스를 추가 정의하면 원하는 작업을 수행할 있는 장점이 있다.
      • Builder 패턴은 객체를 생성하는 부분과 그것을 실제 표현하고 구성하는 부분을 분리시켜줌으로써 서로 간의 독립성을 기할 있게 해준다. 이를 통해 Builder Director 클래스는 서로 독립적으로 수정되거나 재사용될 있다.
      • 객체를 한꺼번에 생성하는 것이 아니라, 부분 부분 생성한 최종 결과를 얻어가는 방식이므로, 객체 생성 과정을 세밀히 제어할 있다. 예를 들어 앞에서도 언급하였듯이 번역 소프트웨어 개발의 경우 아시아 지역에서만 필요한 메뉴얼 내용은 영어나 프랑스어로 번역 시에는 포함시키지 않는 등이 가능하다.
      • 새로운 종류의 객체 생성을 추가하기는 쉬우나, 객체를 구성하는 부분들을 새롭게 추가하기는 어렵다. 예를 들어 번역 소프트웨어 개발에서 평서문, 의문문, 명령문만 번역하던 것을 감탄문까지 번역할 있게 추가한다고 가정해보자. 경우에는 Director 클래스 뿐만 아니라 Translator 하위 클래스들 모두에 새로운 인터페이스를 추가하거나 수정해주어야 한다. , Builder 패턴을 구성하는 모든 클래스들을 수정해야 하는 문제가 발생하는 것이다. 따라서 Builder 패턴을 적용하고자 경우에는 생성되는 객체를 구성하는 부분들을 명확히 해서 추가하거나 수정해야 부분이 없게 하는 것이 중요하다.

반응형