빌더 패턴
복잡한 객체를 생성하는 방법과 표현하는 방법을 정의하는 클래스를 별도로 분리하여 서로 다른 표현이라도 이를 생성할 수 있는 동일한 절차를 제공하는 패턴이다. 생성자에 들어갈 매개변수들을 받아 객체를 생성하는 데 도움을 준다.
구조
- Builder - Product 객체의 일부 요소들을 생성하기 위한 추상 인터페이스를 정의한다.
- ConcreteBuilder - Builder클래스에 정의된 인터페이스의 구현 서브클래스로, 제품의 부품들을 모아 빌더를 복합한다. 생성한 요소의 표현을 정리하고 관리한다.
- Director - Builder 인터페이스를 사용하는 객체를 합성한다.
- Product - 생성할 복합 객체를 표현한다. ConcreteBuilder는 제품의 내부 표현을 구축하고 복합 객체가 어떻게 구성되는지에 관한 절차를 정의한다.
기본 인터페이스
//빌더 인터페이스
public interface IBuilder
{
public IBuilder StepA();
public IBuilder StepB();
public IBuilder StepC();
public Product Build();
}
//빌더 구현 클래스
public class ProductABuilder : IBuilder
{
public IBuilder StepA()
{
//(A단계)
return this;
}
public IBuilder StepB()
{
//(B단계)
return this;
}
public IBuilder StepC()
{
//(C단계)
return this;
}
public Product Build()
{
return new Product(this);
}
}
// 제품 클래스
public class Product
{ //(멤버 변수...)
public Product(IBuilder builder)
{
//(빌더를 통해 멤버 초기화...)
}
}
public class MainTest
{
public static int Main(string[] args)
{
IBuilder builder = new ProductABuilder();
Product product = builder.StepA()
.StepB()
.StepC()
.Build();
return 0;
}
}
왜 사용해야 할까?
캡슐화는 외부 클래스로부터 정의된 기능과 속성들을 은닉하고 보호하여 각 객체의 독립성을 유지하고 변화를 최소화하는 데 목적을 둔 특징이다. 이 캡슐화를 가장 쉽게 보장하는 방법은, 생성 이후로 속성에 일체 접근하지 못하도록 설정하는 것이다. 생성자에서 모든 속성값을 설정하고 Getter Setter 함수가 없다면, 외부 클래스에선 해당 객체의 속성들에 접근도 못하고 수정할 수도 없기 때문에 불변성(Immunability)를 보장할 수 있다.
한 예시로 피자라는 객체를 만든다고 가정하자. 피자는 여러 가지 재료가 들어가서 만들어지고, 피자가 만들어진 후엔 재료의 변화가 일체 없어야 한다. 즉, 생성자 호출을 통해 모든 속성들에 접근하고, 이후 속성들에 접근하는 함수가 없어야 한다.
public class Pizza
{
private float _salt;
private float _cheese;
private int _onion;
private int _mushroom;
private int _salami;
private int _tomato;
private int _potato;
//생성자에서 모든 속성들을 설정한다.
public Pizza(float salt, float cheese, int onion, int mushroom, int salami, int tomato, int potato)
{
_salt = salt;
_cheese = cheese;
_onion = onion;
_mushroom = mushroom;
_salami = salami;
_tomato = tomato;
_potato = potato;
}
//피자에 들어간 재료들 출력
public void Print()
{
Console.WriteLine("===만든 피자 재료===");
Console.WriteLine($"소금 : {_salt}g | 치즈 : {_cheese}g | 양파 : {_onion}개 | 버섯 : {_mushroom}개 | 햄 : {_salami}개 | 토마토 : {_tomato}개 | 감자 : {_potato}개 ");
}
}
public class MainTest
{
public static int Main(string[] args)
{
//피자 제작
Pizza pizza = new Pizza(2.5f, 10f, 2, 4, 2, 2, 2);
pizza.Print();
//출력 == 소금 : 2.5g | 치즈 : 10g | 양파 : 2개 | 버섯 : 4개 | 햄 : 2개 | 토마토 : 2개 | 감자 : 2개
return 0;
}
}
그런데, 손님이 3개의 다른 종류의 피자를 주문했다고 해보자. 각 피자는 들어가는 재료의 수도, 재료의 양도 차이가 발생한다.
보면 콤비네이션은 7개의 재료가, 치즈피자는 4개의 재료, 페페로니 피자는 3개의 재료를 사용한다. 하지만 현재 우리가 준비한 피자의 생성자는 1개밖에 존재하지 않기 때문에 사용하지 않는 재료라도 값을 입력해야한다. 최악의 경우, 재료의 양을 잘못 입력하거나 잘못된 재료를 넣거나 생성자를 잘못 작성할 가능성도 존재한다.
public class MainTest
{
public static int Main(string[] args)
{
//콤비네이션피자
//재료 = 소금 : 2.5g | 치즈 : 10g | 양파 : 2개 | 버섯 : 4개 | 햄 : 2개 | 토마토 : 2개 | 감자 : 2개
Pizza combinationPizza = new Pizza(2.5f, 10f, 2, 4, 2, 2, 2);
//치즈 피자
//재료 = 소금 : 2.5g | 치즈 : 20g | 양파 : 1개 | 버섯 : 0개 | 햄 : 1개 | 토마토 : 0개 | 감자 : 0개
Pizza cheesePizza = new Pizza(2.5f, 20f, 1, 1, 0, 0, 0); // 재료가 잘못 들어감
//페페로니 피자
//재료 = 소금 : 3g | 치즈 : 10g | 양파 : 0개 | 버섯 : 0개 | 햄 : 6개 | 토마토 : 0개 | 감자 : 0개
Pizza peperoniPizza = new Pizza(2.5f, 10f, 0, 0, 6, 0); // 매개변수 부족
return 0;
}
}
그렇다면 이를 해결하기 위해 생성자를 더 만들어야 할까? 소금, 치즈, 햄만 넣는 방법이나 치즈만 넣는 경우들을 구현한답시고 만들어낼 경우 생성자는 끝도 없을 것이고, 같은 개수와 타입의 매개 변수들을 받는 생성자들이 여럿 발생할 가능성도 존재한다. 어찌저찌 생성자를 여럿 만들었다고 하더라도, 피자 클래스의 멤버들이 수정될 경우 생성자들 또한 전부 수정해야하기 때문에 유지보수가 매우 힘들어진다.
이를 위해 생성자를 직접 호출하여 객체를 생성하지 않고, 대신 생성자를 호출해주는 빌더 클래스를 만드는 것이다. 빌더 클래스를 통해 피자 생성자에 입력하고 싶은 내용들을 원하는 개수만큼, 원하는 순서대로 골라서 입력할 수 있다! 다른 사람들과 협업할 때에도 팀원들이 이 코드를 보고 피자에 들어간 재료들을 빠르게 파악할 수 있다.
public class MainTest
{
public static int Main(string[] args)
{
PizzaBuilder builder = new StandardPizzaBuilder();
//어떤 값을 넣는지 명확하게 파악할 수 있다.
Pizza combinationPizza = builder.SetSalt(2.5f)
.SetCheese(10f)
.SetOnion(2)
.SetMushroom(4)
.SetSalami(2)
.SetTomato(2)
.SetPotato(2)
.Build();
//필요한 멤버만 입력할 수 있다.
Pizza cheesePizza = builder.SetSalt(2.5f)
.SetCheese(20f)
.SetOnion(1)
.SetSalami(1)
.Build();
return 0;
}
}
디렉터 추가
빌더를 통해 객체를 만드는 클래스를 만들었다면, 이젠 객체를 만드는 클래스를 관리하는 클래스를 만들 때다.
위 피자 객체를 보면 어떤 피자를 만드느냐에 따라 재료들이 다르다. 빌더 클래스를 관리하는 디렉터 클래스를 추가하고 디렉터 클래스를 통해 어떤 종류의 피자를 만들지만 클라이언트가 요구하면, 해당 피자를 바로 만들도록 빌더 클래스에게 요구하고, 빌더 클래스가 피자를 만들어 디렉터에게, 그리고 클라이언트에게 주면 된다.
public class PizzaDirector
{
private PizzaBuilder _originBuilder;
private PizzaBuilder _builder;
public PizzaDirector(PizzaBuilder builder)
{
this._originBuilder = builder;
}
public Object MakeCombinationPizza()
{
return _builder.SetSalt(2.5f)
.SetCheese(10f)
.SetOnion(2)
.SetMushroom(4)
.SetSalami(2)
.SetTomato(2)
.SetPotato(2)
.Build();
}
}
아니면 피자가 아닌 아예 다른 객체를 만들 수도 있다. 똑같은 값을 입력받더라도 입력받은 값을 통해 피자를 만들지, 피자를 만드는 내용이 담긴 피자 레시피라는 객체를 만들지는 어떤 빌더를 사용하느냐에 따라 달라질 수 있다. 아래에서는 똑같은 입력이 주어지더라도 피자를 만드는 PizzaBuilder, 피자 레시피를 만드는 PizzaRecipeBuilder인지에 따라 피자를 만들지, 피자 레시피를 만들지 설정할 수 있다.
public class MainTest
{
public static int Main(string[] args)
{
PizzaDirector pizzaMakeDirector = new PizzaDirector(new StandardPizzaBuilder());
Pizza combinationPizza = (Pizza)pizzaMakeDirector.MakeCombinationPizza();
Pizza cheesePizza = (Pizza)pizzaMakeDirector.MakeCheeasePizza();
Pizza peperoniPizza = (Pizza)pizzaMakeDirector.MakePeperoniPizza();
combinationPizza.Print();
cheesePizza.Print();
peperoniPizza.Print();
PizzaDirector pizzaRecipeDirector = new PizzaDirector(new PizzaRecipeBuilder());
PizzaRecipe combinationRecipe = (PizzaRecipe)pizzaRecipeDirector.MakeCombinationPizza();
PizzaRecipe cheeseRecipe = (PizzaRecipe)pizzaRecipeDirector.MakeCheeasePizza();
PizzaRecipe peperoniRecipe = (PizzaRecipe)pizzaRecipeDirector.MakePeperoniPizza();
combinationRecipe.PrintRecipe();
combinationRecipe.PrintCalrories();
cheeseRecipe.PrintRecipe();
cheeseRecipe.PrintCalrories();
peperoniRecipe.PrintRecipe();
peperoniRecipe.PrintCalrories();
return 0;
}
}
코드 정리
Product
//피자 클래스
public class Pizza
{
private float _salt;
private float _cheese;
private int _onion;
private int _mushroom;
private int _salami;
private int _tomato;
private int _potato;
public Pizza(float salt, float cheese, int onion, int mushroom, int salami, int tomato, int potato)
{
_salt = salt;
_cheese = cheese;
_onion = onion;
_mushroom = mushroom;
_salami = salami;
_tomato = tomato;
_potato = potato;
}
public void Print()
{
Console.WriteLine("===만든 피자 재료===");
Console.WriteLine($"소금 : {_salt}g | 치즈 : {_cheese}g | 양파 : {_onion}개 | 버섯 : {_mushroom}개 | 햄 : {_salami}개 | 토마토 : {_tomato}개 | 감자 : {_potato}개 ");
}
}
//피자 레시피 클래스
public class PizzaRecipe
{
private float _salt;
private float _cheese;
private int _onion;
private int _mushroom;
private int _salami;
private int _tomato;
private int _potato;
public PizzaRecipe(float salt, float cheese, int onion, int mushroom, int salami, int tomato, int potato)
{
_salt = salt;
_cheese = cheese;
_onion = onion;
_mushroom = mushroom;
_salami = salami;
_tomato = tomato;
_potato = potato;
}
public void PrintRecipe()
{
Console.WriteLine("===피자 레시피===");
Console.WriteLine($"소금 : {_salt}g | 치즈 : {_cheese}g | 양파 : {_onion}개 | 버섯 : {_mushroom}개 | 햄 : {_salami}개 | 토마토 : {_tomato}개 | 감자 : {_potato}개 ");
}
public void PrintCalrories()
{
float calories = 600 + _cheese * 40f + _onion * 20f + _mushroom * 10 + _salami * 200 + _tomato * 40 + _potato * 120;
Console.WriteLine($"피자 칼로리 : {calories}kcals");
}
}
Builder
//피자 빌더 인터페이스
public interface PizzaBuilder
{
public Object Build();
public PizzaBuilder SetSalt(float gram);
public PizzaBuilder SetCheese(float gram);
public PizzaBuilder SetPotato(int count);
public PizzaBuilder SetMushroom(int count);
public PizzaBuilder SetOnion(int count);
public PizzaBuilder SetSalami(int count);
public PizzaBuilder SetTomato(int count);
}
public class StandardPizzaBuilder : PizzaBuilder
{
private float _salt;
private float _cheese;
private int _onion;
private int _mushroom;
private int _salami;
private int _tomato;
private int _potato;
public Object Build()
{
return new Pizza(_salt, _cheese, _onion, _mushroom, _salami, _tomato, _potato);
}
public PizzaBuilder SetSalt(float value)
{
_salt = value;
return this;
}
public PizzaBuilder SetCheese(float value)
{
_cheese = value;
return this;
}
public PizzaBuilder SetOnion(int value) {
_onion = value;
return this;
}
public PizzaBuilder SetMushroom(int value)
{
_mushroom = value;
return this;
}
public PizzaBuilder SetSalami(int value)
{
_salami = value;
return this;
}
public PizzaBuilder SetTomato(int value)
{
_tomato = value;
return this;
}
public PizzaBuilder SetPotato(int value)
{
_potato = value;
return this;
}
}
public class PizzaRecipeBuilder : PizzaBuilder
{
private float _salt;
private float _cheese;
private int _onion;
private int _mushroom;
private int _salami;
private int _tomato;
private int _potato;
public Object Build()
{
return new PizzaRecipe(_salt, _cheese, _onion, _mushroom, _salami, _tomato, _potato);
}
public PizzaBuilder SetSalt(float value)
{
_salt = value;
return this;
}
public PizzaBuilder SetCheese(float value)
{
_cheese = value;
return this;
}
public PizzaBuilder SetOnion(int value) {
_onion = value;
return this;
}
public PizzaBuilder SetMushroom(int value)
{
_mushroom = value;
return this;
}
public PizzaBuilder SetSalami(int value)
{
_salami = value;
return this;
}
public PizzaBuilder SetTomato(int value)
{
_tomato = value;
return this;
}
public PizzaBuilder SetPotato(int value)
{
_potato = value;
return this;
}
}
Director
public class PizzaDirector
{
private PizzaBuilder _originBuilder;
private PizzaBuilder _builder;
public PizzaDirector(PizzaBuilder builder)
{
this._originBuilder = builder;
}
public Object MakeCombinationPizza()
{
Reset();
return _builder.SetSalt(2.5f)
.SetCheese(10f)
.SetOnion(2)
.SetMushroom(4)
.SetSalami(2)
.SetTomato(2)
.SetPotato(2)
.Build();
}
public Object MakeCheeasePizza()
{
Reset();
return _builder.SetSalt(2.5f)
.SetCheese(20f)
.SetOnion(1)
.SetSalami(1)
.Build();
}
public Object MakePeperoniPizza()
{
Reset();
return _builder.SetSalt(3f)
.SetCheese(10f)
.SetSalami(6)
.Build();
}
private void Reset()
{
// _originBuilder의 유형을 가져온다.
Type builderType = _originBuilder.GetType();
// _originBuilder의 유형을 사용하여 해당 유형의 인스턴스를 생성한다.
_builder = Activator.CreateInstance(builderType) as PizzaBuilder;
}
}
Main
public class MainTest
{
public static int Main(string[] args)
{
PizzaDirector pizzaMakeDirector = new PizzaDirector(new StandardPizzaBuilder());
Pizza combinationPizza = (Pizza)pizzaMakeDirector.MakeCombinationPizza();
Pizza cheesePizza = (Pizza)pizzaMakeDirector.MakeCheeasePizza();
Pizza peperoniPizza = (Pizza)pizzaMakeDirector.MakePeperoniPizza();
combinationPizza.Print();
cheesePizza.Print();
peperoniPizza.Print();
PizzaDirector pizzaRecipeDirector = new PizzaDirector(new PizzaRecipeBuilder());
PizzaRecipe combinationRecipe = (PizzaRecipe)pizzaRecipeDirector.MakeCombinationPizza();
PizzaRecipe cheeseRecipe = (PizzaRecipe)pizzaRecipeDirector.MakeCheeasePizza();
PizzaRecipe peperoniRecipe = (PizzaRecipe)pizzaRecipeDirector.MakePeperoniPizza();
combinationRecipe.PrintRecipe();
combinationRecipe.PrintCalrories();
cheeseRecipe.PrintRecipe();
cheeseRecipe.PrintCalrories();
peperoniRecipe.PrintRecipe();
peperoniRecipe.PrintCalrories();
return 0;
}
}
장점
- 생성자를 많이 만들지 않고 필요한 값만 설정할 수 있다.
- 객체를 단계별로 생성하거나 생성 단계들을 연기하거나, 재귀적으로 단계들을 실행할 수 있다.
- 생성과 표현에 필요한 코드를 분리했기 때문에 Product 생성 시 디렉터 객체들이 빌더 코드를 재사용할 수 있다.
- 객체 생성을 단계별로 분리했기 때문에 객체를 생성하는 절차를 좀 더 세밀하게 나눌 수 있다.
- 코드 가독성과 유연성이 증가하고 유지보수에 용이해진다.
단점
- 다수의 클래스를 생성해야 하기 때문에 코드 복잡도가 증가한다.
참고자료
'프로그래밍 이론 > 디자인 패턴' 카테고리의 다른 글
05. [Design Pattern] 싱글톤(Singleton) 패턴 (0) | 2023.11.16 |
---|---|
04. [Design Pattern] 프로토타입(Prototype) 패턴 (0) | 2023.11.07 |
02. [Design Pattern] 추상 팩토리(Abstract Factory) 패턴 (0) | 2023.10.27 |
01. [Design Pattern] 팩토리 메서드(Factory Method) 패턴 (0) | 2023.10.27 |
00. [Design Pattern] 디자인 패턴이란? (0) | 2023.10.25 |