GoF 디자인 패턴 8 - [생성] 빌더 패턴

김정환·2024년 9월 4일
0

GoF 디자인패턴

목록 보기
8/9

빌더 패턴


  • 추상 팩토리를 확장 → 크고 복잡한 객체를 생성할 수 있다.

건축물


  • Builder : 건축가 / 건축기사 = 큰 구조의 큰 물체를 설계, 생성

객체 생성

  • 생성 패턴
    • 주요 목적 = 객체의 생성 과정을 한 곳에 집중화
    • 패턴으로 객체 생성, 관리하는 이유 :
      인스턴스화 과정에서 발생하는 강력한 의존 관계 해소를 위함.
  • 객체 지향
    • 객체
      • 단일 객체 : 하나의 클래스로 생성된 객체
      • 복합 객체 : 복수의 클래스로 구성된 객체
  • 클래스 → 기본적으로 하나의 객체 = 단일 객체 = 하나의 클래스로 생성
  • 객체
    • 데이터와 행동을 보유
    • 때로는 객체 확장을 위해 상속 구조를 적용
    • 팩토리, 팩토리 메서드, 추상 팩토리 ⇒ 단일 객체 사용

복합 객체

  • 전형적인 클래스 확장 방식 : 상속
    • 상위 클래스를 is-a 관계로 포괄해 큰 규모의 객체를 생성하는 기법
    • but,
      1. 강력한 상하 결합 관계
      2. 불필요한 모든 행위까지 포함

  • 객체 지향에서 상속의 단점 개선을 위해
    의존성 주입 사용
    - 의존성을 통해 복합 객체 생성, 사용을 권장
    - 복합 객체 : 하나의 객체가 다른 객체를 포함하는 관계 구조
    - 복합 객체
    - 구조적 의존 관계 ⇒ 객체 확장
    - 객체가 생성된 후에도 다른 객체와 관계를 설정
    → 동적 확장이 가능한 장점

  • 많은 디자인 패턴의 원리, 목적 :
    • 상속 결합 배제
    • 의존 관계의 복합 객체로 변경 처리

복잡한 객체

  • 복합 객체
    • 내부적으로 다른 클래스의 객체 포함
    • 관계 설정을 추가로 진행
      ⇒ 객체 생성 과정이 단일 객체보다 복잡
    • 복합 객체 생성 : 객체의 구조 순서에 맞게 단계별로 진행
  • 최신 객체 지향 트렌드 : 복합 객체 사용 중요 시
    • but, 팩토리, 팩토리 메서드, 추상 팩토리 → 복합 객체 생성 불가

      ⇒ 기존 생성 패턴의 한계 ⇒ 복합 객체 생성 처리를 위한 또 다른 패턴이 필요해짐

💡 빌더 패턴 : 복잡한 구조의 복합 객체를 생성하는 로직을 별도로 분리해 객체 생성 처리

객체 실습


  • 복합 객체 : 하나의 객체가 다른 클래스의 객체를 포함한다는 특징

기본 클래스

  • 복합 객체 생성을 위해 root 클래스 필요
    //////////////////// Computer.h ////////////////////
    #include <vector>
    
    #include "Cpu.h"
    #include "Memory.h"
    #include "Storage.h"
    
    #ifndef COMPUTER_H
    #define COMPUTER_H
    
    class Computer
    {
        private:
    				// 프로퍼티
            Cpu _cpu;
            vector<Memory> _ram;
            vector<Storage> _storage;
        public:
    				// 메서드
            Computer();
            ~Computer();
            string to_string();
            float memory();
            float storage();
    };
    
    #endif
    
    //////////////////// Computer.cpp ////////////////////
    #include <iostream>
    
    #include "./header/Computer.h"
    
    using namespace std;
    
    Computer::Computer(){}
    Computer::~Computer(){}
    
    string Computer::to_string(){
        return "이 컴퓨터의 사양\nCPU = " + _cpu.get() + "\n" + 
                "Ram = " + std::to_string(memory()) + " GB\n" +
                "Storage = " + std::to_string(storage()) + " GB\n";
    }
    
    float Computer::memory(){
        float size = 0.0f;
        
        for (int i = 0; i < _ram.size(); i++)
        {
            size += _ram[i].get_size();
        }
    
        return size;
    }
    
    float Computer::storage(){
        float size = 0.0f;
    
        for (int i = 0; i < _storage.size(); i++)
        {
            size += _storage[i].get_size();
        }
    
        return size;
    }

객체의 구성

  • 복합 객체 - 다른 클래스의 객체를 포함

    • 복합 객체가 다른 객체를 갖는 방법
      1. 내부적으로 직접 관련된 클래스의 객체를 생성
        • 객체 생성자로 관련된 객체들을 생성하여 결합
        • 간단한 팩토리 패턴 같이 메서드를 활용해 관련 객체들을 생성해 결합하기도 함
      2. 외부로부터 객체의 의존성 전달 받기
  • 디자인 패턴에서 복합 객체 구성 :
    의존성 주입 형태를 권장
    - 의존성 주입이 이루어진 객체들 →
    복합 객체의 내부 프로퍼티에 저장
    → 프로퍼티에 저장된 객체 - 속성에 따라 접근 제한이 가능
    → public : 복합 객체와 연결된 객체에 제한없이 접근 가능

  • 의존성 주입이 이루어진 객체의 접근 권한 설정 시 몇가지 로직이 추가됨
    • public : 제한없이 접근 가능
    • private / protected : 외부에서 프로퍼티에 접근 불가 ⇒ 접근 권한 관리를 위한 별도의 메서드 필요 (setter / getter)
      • 단 : 코드 양이 늘어남, 번거로움
      • 장 : 객체 접근 제한 가능, 은닉성

부속 클래스

  • Computer 클래스의 부속 클래스 Memory, Storage
#ifndef MEMORY_H
#define MEMORY_H

class Memory
{
private:
    float _size;
public:
    Memory():Memory(0){};
    Memory(float _s) { _size = _s; };
    ~Memory(){};
    void set_size(float _s = 0){ _size = _s;};
    float get_size(){ return _size; };
};

#endif
#ifndef STORAGE_H
#define STORAGE_H

class Storage
{
private:
    float _size;
public:
    Storage():Storage(0){};
    Storage(float _s){ _size = _s;};
    ~Storage(){};
    void set_size(float _s = 0){ _size = _s;};
    float get_size(){ return _size; };
};

#endif

빌더


  • 빌더 패턴 : 복잡한 구조를 가진 복합 객체의 생성 과정을 분리해 처리
    → 생성 과정을 단계별로 분리 ⇒ 복합 객체의 생성을 일반화

빌더 패턴

  • 팩토리 패턴도 요청한 객체의 생성 과정을 분리
    but, 단일 클래스만 생성, 반환 ⇒ 복합 객체에 적용 불가
  • 복합 객체의 특징
    • 동적으로 객체를 확장할 수 있다 ⇒ 보다 효율적
      • 하나의 객체에 여러 객체를 포함, 포함된 객체에 또 다른 객체 포함 가능
    • 계층적 구조 관계
      • 복합 객체 생성이 쉽지 않은 이유
      • 내부 구조 - 상하 확장 / 다수의 leaf를 가지기도 함
        • 복잡한 구조를 가졌기에 하나의 방식으로 정의하기 매우 어려움
        • 목적에 따라 수없이 많은 종류의 복합 구조가 탄생 가능
💡 이처럼 다양한 구조의 복합 객체를 팩토리 패턴으로 구현하기엔 한계가 있음. ⇒ 각 구조에 맞게 생성을 처리할 수 있도록 과정을 분리해 처리

생성 로직

  • 복잡한 구조의 복합 객체 - 한 단계만으로 생성할 순 없음 ⇒ 복합 객체의 내부 구조에 맞게 단계별로 객체 생성 분리 및 관계 결합 과정 필요
    • 복합 객체의 구조 : 종속적
      ⇒ 종속된 순서의 역순으로 객체 생성, 결합
  • 복합 객체 ← 구조에 맞게 객체 생성, 관계 설정을 위한 로직 필요
    • 생성 로직 - 일반적으로 클라이언트 코드 안에 작성
      • 복합 객체의 생성 로직을 일반 코드로 작성 시,
        객체 생성 과정을 효율적으로 관리하기 어려움

⇒ 빌더 패턴은 복합 객체 생성 과정을 별도의 독립된 클래스로 관리

빌더 추상화

  • 빌더
    • 추상화를 통해 다양한 종류의 복합 객체를 생성 관리
    • 공통된 로직을 분리
// AbstractBuilder.h
#include "BuilderAlgorithm.h"

#pragma once

class AbstractBuilder
{
    protected:
        BuilderAlgorithm* p_algorithm; // 알고리즘 객체
    public:
        AbstractBuilder();
        ~AbstractBuilder();
        void set_algorithm(BuilderAlgorithm* algorithm){ p_algorithm = algorithm; };
        void get_instance(){ p_algorithm->get_instance(); };
        
        // 추상 메서드 선언, 복합 객체 생성 로직을 하위 클래스에 위임
        virtual void build(); 
};

ConcreteBuilder

  • 추상 클래스 Builder를 상속 받아 실제로 복합 객체를 생성하는 하위 클래스
// ConcreteBuilder.h
#include "AbstractBuilder.h"

#include <iostream>
using namespace std;

#pragma once

class ConcreteBuilder : public AbstractBuilder
{
public:
    ConcreteBuilder(){};
    ConcreteBuilder(BuilderAlgorithm* algorithm){ set_algorithm(algorithm); };
    ~ConcreteBuilder(){};

    AbstractBuilder* build();
};

// ConcreteBuilder.cpp
#include "./header/ConcreteBuilder.h"

AbstractBuilder* ConcreteBuilder::build(){    
    cout << "Build start.\n";

    // 단계별 빌더의 메서드 호출
    this->p_algorithm->set_cpu();
    this->p_algorithm->set_ram();
    this->p_algorithm->set_storage();
    
    return this;
}
  • 추상화 적용으로 여러 개의 하위 클래스 작성 ⇒ 다형성 적용 ⇒ 다양한 복합 객체의 생성 로직을 하위 클래스로 구현 가능

추상 메서드

  • 빌더 ← 추상 메서드 사용 ⇒ 복합 객체 생성 방법을 달리 적용 가능
    • 빌더 패턴
      • 복합 객체의 생성 로직을 직접 클라이언트 코드로 구현 or 메서드를 호출하지 않음 ⇒ 독립적인 단계별 구축 공정을 분리해 처리
      • 추상 메서드 build() 안에 복합 객체 생성을 위한 처리 로직이 들어있음.
      • 빌더 생성 로직을 별도의 알고리즘으로 분리해 외부로부터 주입 받을 수 있음.

  • 빌더 패턴
    • 다양한 종류의 복합 객체 생성 로직을 구분
    • 추상 메서드 / 외부 알고리즘
      ⇒ 객체의 실제 생성 로직을 외부로부터 은닉하는 효과

알고리즘


  • 빌더 패턴
    • 복합 객체의 생성 로직을 별도의 클래스로 분리
      ⇒ 분리한 로직 = 알고리즘
      - 분리한 알고리즘 객체 → 다시 빌더에 전달되어 복합 객체 생성

전략 패턴

  • 복합 객체
    • 생성 과정이 복잡
    • 1개의 빌더로 다양한 복합 객체 생성을 원함 ⇒ 생성 로직을 분리하는 것이 좋음
  • 전략 패턴 (= 외부에서 처리 객체를 전달 받아 수행하는 패턴)
    • 알고리즘이라 했을 때, 가장 먼저 떠오르는 패턴
    • 빌더 패턴 또한 분리된 처리 로직을 객체화해 전달 가능 ⇒ 이때 빌더 패턴 + 전략 패턴의 결합된 형태가 됨
  • ConcreteBuild 예제에서 생성자를 통해 알고리즘 객체를 의존성 주입 받음
    • 알고리즘
      • 전략 패턴에서 생성자를 통해 의존성을 주입
      • 의존성을 전달 받은 전략 패턴(=알고리즘)
        • 내부 프로퍼티에 저장
        • 외부로 공개된 메서드를 통해 실행
      • 실체 객체 생성 요청객체를 생성하는 알고리즘이 분리
        • 빌더 - 전략 패턴의 알고리즘을 응용 → 복합 객체를 생성

추상화

  • 복합 객체 생성 방법 → 다양함 → 다양한 객체 생성, 처리를 위해 다형성을 적용 ⇒ 빌더 패턴 : 일관적인 알고리즘 적용 + 다형성 유지 ⇒ 추상 구조 적용
  • 빌더
    • 추상 팩토리의 확장
    • 알고리즘 → 추상화 적용 ⇒ 생성 과정을 단계별로 캡슐화 ⇒ 추상화된 알고리즘
      • 각각의 단계를 구조화 → 객체를 생성할 수 있는 로직으로 전달

        // BuilderAlgorithm.h
        #pragma once
        
        class AbstractBuilder;
        
        class BuilderAlgorithm // 알고리즘의 공통된 동작을 위한 추상 클래스 선언
        {
            protected:
                AbstractBuilder* p_composite; // 빌더 객체를 저장
            public:
                BuilderAlgorithm();
                ~BuilderAlgorithm();
                
                virtual void set_cpu(); // 객체 생성에 필요한 추상 메서드 선언
                virtual void set_ram(); // -> 각 알고리즘으로 재정의될 것
                virtual void set_storage();
                virtual AbstractBuilder* get_instance() { return this->p_composite; };
        };
      • 객체 생성에 필요한 추상 메서드 선언,
        공통된 로직은 메서드나 프로퍼티로 연결될 수 있음

      • 복합 객체의 생성 알고리즘 분리
        → 내부 구조를 외부로부터 보호 가능

하위 클래스

  • 복합 객체 생성 - 단계별 과정 필요
    • 빌더 : 추상 클래스를 통해 이런 과정을 약속
    • 빌더 객체 :
      • 약속된 생성 과정만 호출
      • 빌더 객체에 전달되는 생성 알고리즘은 하위 클래스에서 구현
        #include "header/ConcreteAlgorithm.h"
        
        #include <iostream>
        using namespace std;
        
        ConcreteAlgorithm::ConcreteAlgorithm(){
            Computer comp;
            p_composite = &comp;
        }
        
        ConcreteAlgorithm::~ConcreteAlgorithm(){}
        
        void ConcreteAlgorithm::set_cpu(std::string cpu) {
            p_composite->_cpu = Cpu(cpu);
        }
        
        void ConcreteAlgorithm::set_ram(vector<float> rams){
            for (int i = 0; i < rams.size(); i++)
            {
                p_composite->_ram.push_back(Memory(rams[i]));
            }
            
        }
        
        void ConcreteAlgorithm::set_storage(vector<float> storages){
        
            for (int i = 0; i < storages.size(); i++)
            {
                p_composite->_storage.push_back(Storage(storages[i]));
            }
        }
        • 알고리즘 하위 클래스
          • 복합 객체 생성을 위한 단계별 행동이 정의

            → 이러한 과정의 조합→ 실제 제품 생산 시 or 중간 과정 확장 시 매우 유용

  • ConcreteBuilder에서 전달 받은 알고리즘의 메서드 호출 코드의 일부
#include "./header/ConcreteBuilder.h"

AbstractBuilder* ConcreteBuilder::build(){    
    cout << "Build start.\n";

    // 단계별 빌더의 메서드 호출
    cout << get_algorithm() << "\n";
    
    get_algorithm()->set_cpu("i7");
    cout << "add cpu" <<'\n';

    vector<float> rams = {16, 16};
    get_algorithm()->set_ram(rams);
    cout << "add ram" <<'\n';

    vector<float> storages = {1024, 1024};
    get_algorithm()->set_storage(storages);
    cout << "add storage" <<'\n';

    return this;
}
  • 분리된 단계별 메서드를 빌더 객체에서 호출, 조합
💡 빌더 패턴 구현 시, 먼저 생성, 조합을 위한 모델을 만들어야 함.

교환 가능성

  • 객체 지향에서 복합 객체의 구조는 너무 다양함 ⇒ 활용하기 어려움
    • 빌더 패턴은 이를 보완하기 위함.
      1. 생성 / 처리 로직을 분리
      2. 처리 로직 분리 시, 전략 패턴을 사용

⇒ 생성 단계를 위한 알고리즘(=처리 로직) 전략 패턴으로 전달

→ 다양한 종류의 복합 객체를 쉽게 생성할 수 있게 됨.

→ 전략 패턴을 적용한 빌더 패턴 - 생성자를 통해 알고리즘 객체를 전달(의존성) 받음

⇒ 빌더 클래스

  • 어떤 복합 객체가 만들어지는지 구체적으로 알지 못함
  • 미리 약속된 동작으로만 객체 생성 과정을 호출
  • 실체 객체는 알고리즘에 의해 생성
  • 알고리즘 교체
  • 빌더 패턴
    • 언제든지 전달되는 알고리즘을 교체 가능. → 다양한 복합 객체를 동적으로 생성할 수 있음.

빌더 선택

  • 알고리즘만 사용해 다양한 복합 객체를 생성하는 것 → 불충분할 수도 있음
    • 알고리즘
      • 빌더 클래스에 정의된 단계별로 동작을 호출하여 복합 객체를 생성
      • 만약 단계가 변경된 다른 복합 객체 생성이 필요한 경우 ⇒ 다수의 ConcreteBuilder 하위 클래스를 구성 → 그룹을 생성 (추상 팩토리와 유사)
  • 빌더 패턴
    • 추상화의 다형성 이용 ⇒ 그룹별로 복합 객체의 종류 설계
      • 객체 생성 그룹을 분리 ⇒ 분리된 생성 그룹을 빌더 패턴의 인자로 전달 ⇒ 선택된 그룹의 선언된 메서드를 복합 객체를 생성

생성 요청


  • 빌더 패턴을 이용한 컴퓨터를 의미하는 복합 객체 생성

알고리즘 생성

  • 복합 객체 생성을 위해
  1. 먼저 생성 알고리즘을 선택
    • 알고리즘을 다양하게 구성 가능 → 생성된 알고리즘 객체를 빌더에 전달

빌더 객체

  1. 복합 객체를 제작하는 빌더 객체 생성

    → 빌더 패턴 내부에 복합 객체 생성을 수행하는 알고리즘이 있음

    → 알고리즘은 전략 패턴을 결합하여 구현 동장

  2. 전랙 패턴인 알고리즘을 빌더의 생성자로 하여 의존성 주입

    → 생성자로 의존성 주입되면 입력된 알고리즘으로 복합 객체 생성을 동적으로 수행

    • setter 메서드로 알고리즘을 주입할 수 있음 → 별도의 setter 메서드 준비 필요

빌드

  1. 클라이언트 코드에서 최종 복합 객체 생성

    → 클라이언트 코드 - 빌더 객체에 실제 복합 객체 생성을 요청

    → 빌더는 의존성 주입된 알고리즘에 따라 복합 객체 생성

정리


  • 빌더 패턴
    • 추상 팩토리의 확장
    • 복잡한 단계(step)을 가진 복합 객체 생성 가능
    • 생성 단계를 중점으로 설계
      • 추상 팩토리 - 유사한 객체의 생성 과정을 중심으로 제품군 설계
  • 추상 팩토리 패턴 → 유사한 객체의 제품군을 알고리즘화 → 다양한 복합 객체 생성 및 관리
    ⇒ 빌더
  • 빌더
    • 관계된 서브 객체의 단계별 생성 절차가 완료된 후 복합 객체 생성 및 반환
    • 만들고자 하는 부품들이 모여야 의미가 있음
  • 추상 팩토리
    • 객체 생성한 즉시 반환
    • 각각의 부품에만 의미 부여
profile
만성피로 개발자

0개의 댓글