[Design Pattern] 1-2. 빌더 패턴(Builder pattern)


Study/Design Pattern  2021. 6. 11. 17:35

안녕하세요. 명월입니다.


이 글은 디자인 패턴의 빌더 패턴(Builder pattern)에 대한 글입니다.


빌더 패턴은 우리가 보통 클래스의 인스턴스를 생성할 때 new 키워드를 사용해서 생성합니다만, 그런 방식이 아닌 다른 클래스를 이용해서 인스턴스를 생성하는 패턴이라고 할 수 있습니다.

그리고 인스턴스 안에 데이터를 입력하고 그에 대한 처리 함수등을 호출하여 클래스의 데이터를 처리하는 방법입니다. 사양에 따라 다르지만 클래스의 목적이 있고 초기값 및 데이터는 어느정도 정해져 있고 특정 패턴에 따라 클래스의 데이터를 입력하는 경우가 있습니다.

그럴 때마다 실행 함수 및 로직 함수에 클래스의 초기 값을 입력하기에는 불편함도 많고 가독성도 좋지 않습니다.

using System;

namespace Example
{
  // Node1 예제 클래스
  class Node1
  {
    // 맴버 변수와 getter, setter 프로퍼티
    public string Name { get; set; }
    public int Data { get; set; }
    // 출력 함수
    public void Print()
    {
      Console.WriteLine("Node1 - Name = " + Name + " Data = " + Data);
    }
  }
  // Node2 예제 클래스
  class Node2
  {
    // 맴버 변수와 getter, setter 프로퍼티
    public string Name { get; set; }
    public int Data { get; set; }
    // 출력 함수
    public void Print()
    {
      Console.WriteLine("Node2 - Name = " + Name + " Data = " + Data);
    }
  }
  // 실행 함수가 있는 클래스
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 인스턴스 생성
      var node1 = new Node1();
      // 변수 설정
      node1.Name = "Test1";
      node1.Data = 1;
      // 함수 출력
      node1.Print();

      // 인스턴스 생성
      var node2 = new Node2();
      // 변수 설정
      node2.Name = "Test2";
      node2.Data = 2;
      // 함수 출력
      node2.Print();

      Console.WriteLine("Press Any Key...");
      Console.ReadKey();
    }
  }
}

위 소스가 잘못되거나 에러가 발생하거나, 성능상 매우 안 좋은 소스는 아닙니다. 그러나 Node 클래스의 인스턴스를 생성하고 멤버 변수에 데이터를 추가하는 게 조금 코드가 지저분해 보입니다.

만약 Node 클래스에서 사용할 변수가 많거나 클래스를 만들기 위해 처리가 많은 경우(파라미터 데이터를 만들기 위한 처리)에는 그만큼 소스의 가독성이 떨어지게 됩니다.

그렇다면 생성자로 Node에 필요한 파라미터를 넘기면 되지 않을까 생각이 되는데, 인스턴스를 생성해서 변수 값을 입력하는 것과 따로 변수를 선언하고 입력해서 생성자 파라미터로 넘겨서 만드는 것과는 결국 같은 구조입니다.


그렇다면 조금 줄일 수 있는 방법으로 Node1과 Node2의 공통 인터페이스를 만들고 함수등을 통해서 인스턴스를 생성해서 받으면 소스를 줄일 수도 있고 가독성을 높일 수 있습니다.

using System;

namespace Example
{
  // Node1 클래스와 Node2 클래스를 묶기 위한 인터페이스
  interface INode
  {
    // 함수
    void Print();
  }
  // Node1 예제 클래스 : 인터페이스 INode 상속
  class Node1 : INode
  {
    // 맴버 변수와 getter, setter 프로퍼티
    public string Name { get; set; }
    public int Data { get; set; }
    // 출력 함수
    public void Print()
    {
      Console.WriteLine("Node1 - Name = " + Name + " Data = " + Data);
    }
  }
  // Node2 예제 클래스 : 인터페이스 INode 상속
  class Node2 : INode
  {
    // 맴버 변수와 getter, setter 프로퍼티
    public string Name { get; set; }
    public int Data { get; set; }
    // 출력 함수
    public void Print()
    {
      Console.WriteLine("Node2 - Name = " + Name + " Data = " + Data);
    }
  }
  // 실행 함수가 있는 클래스
  class Program
  {
    // 생성 패턴
    static INode Build(string type, string name, int data)
    {
      // 리턴을 위한 변수
      INode ret = null;
      // type이 Node1의 경우(대소문자 무시)
      if ("NODE1".Equals(type, StringComparison.OrdinalIgnoreCase))
      {
        // 생성자 생성
        ret = new Node1();
        // 변수 설정
        ((Node1)ret).Name = name;
        ((Node1)ret).Data = data;
      }
      // type이 Node1의 경우(대소문자 무시)
      if ("NODE2".Equals(type, StringComparison.OrdinalIgnoreCase))
      {
        // 생성자 생성
        ret = new Node2();
        // 변수 설정
        ((Node2)ret).Name = name;
        ((Node2)ret).Data = data;
      }
      return ret;
    }
    // 실행 함수
    static void Main(string[] args)
    {
      // 인스턴스 생성
      var node1 = Build("node1", "Test1", 1);
      // 함수 출력
      node1.Print();

      // 인스턴스 생성
      var node2 =  Build("node2", "Test2", 2);
      // 함수 출력
      node2.Print();

      Console.WriteLine("Press Any Key...");
      Console.ReadKey();
    }
  }
}

위 소스는 Build 함수를 사용해서 클래스 인스턴스를 생성했습니다. 그리고 첫번째 파라미터를 통해서 클래스를 구분해서 인스턴스를 생성했습니다.

로직 흐름만 보면 위 예제도 Build 패턴이기는 합니다. 그러나 Build 함수를 보면 안에 변수 설정을 위해서 강제 캐스팅을 해서 데이터를 입력했습니다. 결국 첫번째 예제와 크게 다른게 아닙니다.


정확히 Build 패턴을 구현하기 위해서는 파리미터 역할의 Builder 클래스와 Build를 실행할 Director 클래스를 통해 데이터를 설정하고 최종 결과 Class를 받는 것이 빌드 패턴입니다.

출처 - https://en.wikipedia.org/wiki/Builder_pattern
#pragma once
#include <stdio.h>
#include <iostream>
using namespace std;
// 빌더 클래스, 데이터를 수집해서 클래스를 만들기 위한 클래스
class Builder {
private:
  // 맴버 변수
  int type = -1;
  string name;
  int data;
public:
  // 초기화 데이터
  void setInitialize(string name, int data) {
    this->name = name;
    this->data = data;
  }
  // 맴버 변수를 받기 위한 getter
  string getName() {
    return this->name;
  }
  // 맴버 변수를 받기 위한 getter
  int getData() {
    return this->data;
  }
};
// 추상 클래스(인터페이스)
class INode {
public:
  virtual void initialize(Builder*) = 0;
  virtual void print() = 0;
};
// Node 인스턴스를 만들어 내는 클래스
class Director {
private:
  // 인스턴스를 갖고 있으려는 맴버 변수
  INode* node;
public:
  // 인스턴스 생성
  void setNode(int type) {
    if (type == 0) {
      // 변수가 0이면 Node1 인스턴스를 생성
      this->node = new Node1();
    } else if (type == 1) {
      // 변수가 1이면 Node2 인스턴스를 생성
      this->node = new Node2();
    }
  }
  // 생성된 인스턴스에 변수 실행 - 생성된 인스턴스를 반환한다.
  INode* build(Builder* builder) {
    this->node->initialize(builder);
    return this->node;
  }
  // 인스턴스를 실행
  void execute() {
    this->node->print();
  }
private:
  // INode 클래스를 상속받은 Node1 클래스
  class Node1 : public INode {
  private:
    // 맴버 변수
    string name;
    int data;
  public:
    // 맴버 변수 값을 설정하는 함수
    virtual void initialize(Builder* builder) {
      this->name = builder->getName();
      this->data = builder->getData();
    }
    //콘솔 출력
    virtual void print() {
      cout << "Node1 - Name = " << this->name << " Data = " << this->data << endl;
    }
  };
  // INode 클래스를 상속받은 Node2 클래스
  class Node2 : public INode {
  private:
    // 맴버 변수
    string name;
    int data;
  public:
    // 맴버 변수 값을 설정하는 함수
    virtual void initialize(Builder* builder) {
      this->name = builder->getName();
      this->data = builder->getData();
    }
    //콘솔 출력
    virtual void print() {
      cout << "Node2 - Name = " << this->name << " Data = " << this->data << endl;
    }
  };
};
// 실행 함수
int main() {
  // builder 인스턴스 생성
  Builder builder;
  // director 인스턴스 생성
  Director director;
  // 빌더 데이터 설정
  builder.setInitialize("Test1", 1);
  // Node 인스턴스 생성
  director.setNode(0);
  // 인스턴스 데이터 설정하고 반환 받기
  INode* node1 = director.build(&builder);
  // 인스턴스 실행
  director.execute();
  
  // 빌더 데이터 설정
  builder.setInitialize("Test2", 2);
  // Node 인스턴스 생성
  director.setNode(1);
  // 인스턴스 데이터 설정하고 반환 받기
  INode* node2 = director.build(&builder);
  // 인스턴스 실행
  director.execute();

  // 메모리 삭제
  delete node1;
  delete node2;
  return 0;
}

간단하게 만들려고 했는데 꽤나 복잡하게 되었네요.

위 설정은 먼저 Builder 인스턴스로 데이터를 설정합니다. Director에 type 데이터별로 Node1또는 Node2의 인스턴스를 생성합니다.

그리고 Director에 Builder 인스턴스를 넣어서 데이터를 조립합니다. 물론 조립해서 나온 데이터를 사용해도 되고 혹은 execute 함수를 설정해서 생성된 인스턴스를 실행할 수도 있습니다.

여기서 특이사항은 Node1과 Node2 클래스가 Director 클래스 안에 인라인 클래스로 선언되어 있습니다.

이것은 선택사항입니다만 보통 소스의 관리를 위해서, 즉, 이 클래스를 사용하는 사용자(개발자)가 Node1과 Node2 클래스의 인스턴스를 꼭 Build 패턴을 이용해서 만들라는 제약입니다. 즉, 사양에 따라가 인라인 클래스가 아닌 일반 public 클래스로 만들어도 상관없습니다.


위에 있는 클래스 다이어그램과 소스를 확인하면 조금 차이가 있습니다. 저는 하나의 Builder와 하나의 Director를 통해서 Node의 인스턴스 종류별로 반환이 됩니다. 이것도 빌드 패턴이 맞습니다.

그러나 반대의 경우도 존재합니다. 즉, 여러개의 Builder와 하나의 Director를 통해서 하나의 Node 인스턴스가 생성되는 방법과 한개의 Builder와 여러개의 Director, 혹은 여러개의 Builder와 Director, Node 인스턴스로 생성하는 경우도 있습니다.


이번에는 여러개의 Builder와 한개의 Director, 한개의 Node를 반환하는 Java 예제입니다.

// 실행 함수가 있는 클래스
public class Program {
  // 실행 함수
  public static void main(String[] args) {
    // Director 인스턴스 생성
    Director director = new Director();
    // Builder 인스턴스 생성
    IBuilder builder = new Builder1(10);
    // Director 인스턴스로 Node 인스턴스 생성
    INode node = director.build(builder);
    // node 인스턴스의 print 함수 실행
    node.print();
    
    // Builder 인스턴스 생성
    builder = new Builder2(10);
    // Director 인스턴스로 Node 인스턴스 생성
    node = director.build(builder);
    // node 인스턴스의 print 함수 실행
    node.print();
  }
}
// Node 인터페이스
interface INode {
  void print();
}
// Builder 인터페이스
interface IBuilder {
  int getData();
}
// Builder1 클래스
class Builder1 implements IBuilder {
  private int data;
  // 생성자로 데이터 입력
  public Builder1(int data) {
    this.data = data;
  }
  // Builder1은 getData를 통해 100배 데이터를 리턴
  public int getData() {
    return this.data * 100;
  }
}
// Builder2 클래스
class Builder2 implements IBuilder {
  private int data;
  // 생성자로 데이터 입력
  public Builder2(int data) {
    this.data = data;
  }
  // Builder2은 getData를 통해 10배 데이터를 리턴
  public int getData() {
    return this.data * 10;
  }
}
// Director 클래스
class Director {
  // 인라인 클래스로 Node 클래스를 제어한다. 즉, 외부에서 Node클래스의 인스턴스를 만들 수 없다.
  class Node implements INode {
    // 맴버 변수
    private String data;
    // 출력 함수
    public void print() {
      System.out.println(data);
    }
  }
  // Node 인스턴스를 만들어서 리턴
  public INode build(IBuilder builder) {
    // 인스턴스 생성
    Node node = new Node();
    // 데이터 만들기
    node.data = builder.getClass().getName() + " - " + builder.getData();
    // 리턴
    return node;
  }
}

Java 예제는 Builder를 두개로 해서 만들었습니다. Node 클래스와 Director 클래는는 하나입니다.

즉, Builder 클래스 종류마다 Node 클래스에 데이터가 바뀌는 형태입니다. Node 클래스는 인라인 클래스이기 때문에 Builder 패턴을 통하지 않으면 생성이 되지 않습니다.


C#은 여러개의 Director 클래스와 하나의 Builder 클래스, 하나의 Node 클래스 예제입니다.

using System;

namespace Example
{
  // Node 클래스의 인터페이스
  interface INode
  {
    // 출력 함수
    void Print();
  }
  // Director 클래스의 인터페이스
  interface IDirector
  {
    // Node 클래스의 인스턴스를 만들기 위한 함수
    INode Build(Builder builder);
  }
  // 추상 클래스(Node 인라인 클래스를 만들기 위함)
  abstract class AbstractDirector : IDirector
  {
    // Node 클래스
    protected class Node : INode
    {
      // 맴버 변수
      public string Data { get; set; }
      // 출력 함수
      public void Print()
      {
        // 콘솔 출력
        Console.WriteLine("Node - " + Data);
      }
    }
    // Node 클래스의 인스턴스를 만들기 위한 함수 
    public abstract INode Build(Builder builder);
  }
  // Builder 클래스
  class Builder
  {
    // 맴버 변수
    public int Data { get; private set; }
    // 생성자
    public Builder(int data)
    {
      this.Data = data;
    }
  }
  // Director1 클래스
  class Director1 : AbstractDirector
  {
    // Node 클래스 인스턴스 생성 함수
    public override INode Build(Builder builder)
    {
      // 인스턴스 생성
      Node node = new Node();
      // 데이터 설정
      node.Data = " Director1 " + builder.Data;
      // 반환
      return node;
    }
  }
  // Director2 클래스
  class Director2 : AbstractDirector
  {
    // Node 클래스 인스턴스 생성 함수
    public override INode Build(Builder builder)
    {
      // 인스턴스 생성
      Node node = new Node();
      // 데이터 설정
      node.Data = " Director2 " + builder.Data;
      // 반환
      return node;
    }
  }
  // 실행 함수가 있는 클래스
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 빌더 클래스 인스턴스 생성
      Builder builder = new Builder(10);
      // Director1 인스턴스 생성
      IDirector director = new Director1();
      // Node 인스턴스 생성
      INode node = director.Build(builder);
      // 출력 함수 실행
      node.Print();

      // Director2 인스턴스 생성
      director = new Director2();
      // Node 인스턴스 생성
      node = director.Build(builder);
      // 출력 함수 실행
      node.Print();

      Console.WriteLine("Press Any Key...");
      Console.ReadKey();
    }
  }
}

Builder 패턴는 예제로 보면 매우 복잡해 보이고 이런 패턴을 왜 쓸까 싶기도 합니다. 저의 경우는 워낙 간단하게 예제로 설명하다 보니 반대로 복잡해 보이지만, 반대로 매우 복잡한 소스에서는 가독성을 엄청 개선시켜서 생각보다 매우 유용한 패턴이기도 합니다.

사실 자신이 작성한 소스를 보시면 알게 모르게 사용한 적도 많은 패턴이기도 합니다. 굉장히 많이 사용하죠.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Collections.Generic;

namespace Example
{
  // builder 역할의 클래스, 쿼리와 디비 정보가 있다.
  class DBInformation
  {
    public SqlCommand Sqlcommand
    {
      get; set;
    }
  }
  // Director 역할의 클래스, 디비와 연결해서 데이터를 취득하는 클래스
  class Dao
  {
    // 데이터 베이스의 데이터를 List<Test>값으로 반환받는다.
    public List<Test> Build(DBInformation info)
    {
      try
      {
        // 데이터 베이스 커넥션 오픈
        info.Sqlcommand.Connection.Open();
        // query 실행
        var dr = info.Sqlcommand.ExecuteReader();
        // 인스턴스 리스트 생성
        var ret = new List<Test>();
        // row 개수만큼 반복
        while (dr.Read())
        {
          // 인스턴스 생성
          Test node = new Test();
          // 인스턴스에 데이터를 입력
          node.Idx = dr.GetInt32(0);
          node.Data = dr.GetString(1);
          // 리스트에 추가
          ret.Add(node);
        }
        // 반환
        return ret;
      }
      finally
      {
        // 최종적으로 커넥션 종료
        info.Sqlcommand.Connection.Close();
      }
    }
  }
  // Node 역할의 클래스
  class Test
  {
    // 맴버 변수
    public int Idx { get; set; }
    public String Data { get; set; }
  }
  // 실행 함수가 있는 클래스
  class Program
  {
    // SqlCommand를 생성하는 함수
    private static SqlCommand GetConnection(string query)
    {
      // 인스턴스 생성
      var ret = new SqlCommand();
      ret = new SqlCommand();
      ret.Connection = new SqlConnection();
      // 서버 설정
      ret.Connection.ConnectionString = "Server=****; Database=****; User Id=****; Password=****;";
      ret.CommandType = CommandType.Text;
      // 쿼리 설정
      ret.CommandText = query;
      // 반환
      return ret;
    }
    /*
      CREATE TABLE TEST(
      idx int primary key IDENTITY(1,1),
      data nvarchar(255) null
      )

      insert into TEST values('a');
      insert into TEST values('b');
      insert into TEST values('c');
     */
    // 실행 함수
    static void Main(string[] args)
    {
      // 데이터 베이스 정보를 입력한다.
      var builder = new DBInformation();
      // 커넥션 생성
      builder.Sqlcommand = GetConnection("SELECT IDX, DATA FROM TEST");
      // 인스턴스 생성
      var director = new Dao();
      // 데이터를 Build한다. (데이터 베이스로부터 데이터를 취득한다.)
      var list = director.Build(builder);
      // 데이터 개수 만큼 반복문
      foreach (var node in list)
      {
        // 콘솔 출력
        Console.WriteLine("idx - " + node.Idx + " data - " + node.Data);
      }

      Console.WriteLine("Press any key...");
      Console.ReadKey();
    }
  }
}

위 예제는 실제 프로젝트에서 사용할 만한 예제입니다. 커넥션을 생성하는 함수는 간단하게 만들었지만 DB 종류(Mysql이나 Oracle)로 빌드 패턴을 만들 수 있는 부분입니다.

실제로 Connection 라이브러리는 싱글톤 패턴과 빌드 패턴등을 사용해서 작성되어 있습니다.


여기까지 디자인 패턴의 빌더 패턴(Builder pattern)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.