본문 바로가기
Programming

디자인패턴 시리즈 7. 빌더 패턴 (Builder Pattern)

by LeeJ1Hyun 2023. 1. 19.

빌더 패턴 (Builder Pattern)

제품을 여러 단계로 나눠서 만들도록 제품 생산 단계를 캡슐화한다. 프로그래밍 할 때 가장 많이 쓰이는 때는 복잡한 객체를 생성하는 순간이다. 특히 선택적인(Optional) 속성이 많을 때 유용하다.

코드에 적용해보기

피자에는 토핑이 다양하게 들어간다. 기호에 따라 선호하거나 빼고 싶은 토핑이 있을 수도 있다. 또한 피자를 만드는 데에는 여러 단계와 다양한 절차를 거치게 된다. 이러한 과정을 거치며 객체를 만드는데 언뜻 팩토리 패턴과 유사하게 보일 수도 있다. 하지만 팩토리 패턴은 한 단계에서 모든 것을 처리한다는 점에서 다르다.

 

 

빌더를 이용하여 고기를 좋아하는 사람들을 위한 피자와 비건 피자를 만들어보겠다.

 

public class Pizza {
	String name;
	List<String> toppings;
	
	void addToppings(List<String> toppings) {
		this.toppings = toppings;
	}
 
	void prepare() {
		System.out.println("준비하는중 " + name);
		System.out.println("반죽을 던진다!");
		System.out.println("소스를 넣는다!");
		System.out.println("토핑을 넣는다!: ");
		for (String topping : toppings) {
			System.out.println("   " + topping);
		}
	}
  
	void bake() {
		System.out.println("350도에서 25분간 굽는다!");
	}
 
	void cut() {
		System.out.println("피자를 자른다!");
	}
  
	void box() {
		System.out.println("박스에 피자를 담는다!");
	}
 
	public void setName(String name) {
		this.name = name;
	}

	public String toString() {
		StringBuffer display = new StringBuffer();
		display.append("---- " + this.name + " ----\n");
		for (String topping : toppings) {
			display.append(topping + "\n");
		}
		return display.toString();
	}
}

 

Pizza 클래스에서는 어떤 피자인지와는 관계없이 일반적으로 피자를 만드는 과정에만 관심이 있다. 토핑들을 배열로 받아 addToppings() 메소드를 이용하여 만든다. 치즈 피자, 고구마 피자, 비건 피자 등 필요한 재료를 조합해서 첨가하는 과정은 모두 다르기 때문에 공통적으로 구현할 수 없기 때문이다.

 

각각의 토핑들을 필요에 따라 절차대로 첨가하는 과정을 빌더 패턴을 이용하여 만들어보자. 이때 클라이언트는 이 추상 인터페이스 또는 클래스(빌더)를 사용해서 피자를 만든다.

 

public abstract class PizzaBuilder {
	String name;
	List<String> toppings = new ArrayList<String>();
	
	public abstract PizzaBuilder addCheese();
	public abstract PizzaBuilder addSauce();
	public abstract PizzaBuilder addTomatoes();
	public abstract PizzaBuilder addGarlic();
	public abstract PizzaBuilder addOlives();
	public abstract PizzaBuilder addSpinach();
	public abstract PizzaBuilder addPepperoni();
	public abstract PizzaBuilder addSausage();
    
	public Pizza build() {
		Pizza pizza = new Pizza();
		pizza.setName(this.name);
		pizza.addToppings(toppings);
		return pizza;
	}
}

 

PizzaBuilder 추상 클래스는 토핑들을 절차대로 넣고 PizzaBuilder를 반환한다. build() 메소드를 사용하여 Pizza 인스턴스를 하나 만든 후 피자의 이름(종류)과 토핑을 세팅한 후 pizza를 반환한다.

 

public class MeatLoversPizzaBuilder extends PizzaBuilder {
	public MeatLoversPizzaBuilder() {
		this.name = "고기를 좋아하는 사람을 위한 피자";
	}
	public PizzaBuilder addCheese() {
		this.toppings.add("모짜렐라");
		return this;
	}
	public PizzaBuilder addSauce() {
		this.toppings.add("토마토 소스");
		return this;
	}
	public PizzaBuilder addTomatoes() {
		this.toppings.add("자른 토마토");
		return this;
	}
	public PizzaBuilder addGarlic() {
		this.toppings.add("마늘");
		return this;
	}
	public PizzaBuilder addOlives() {
		return this;
	}
	public PizzaBuilder addSpinach() {
		return this;
	}
	public PizzaBuilder addPepperoni() {
		this.toppings.add("페퍼로니");
		return this;
	}
	public PizzaBuilder addSausage() {
		this.toppings.add("소시지");
		return this;
	}	
}

 

public class VeggieLoversPizzaBuilder extends PizzaBuilder {
	public VeggieLoversPizzaBuilder() {
		this.name = "채소를 좋아하는 사람을 위한 피자";
	}
	public PizzaBuilder addCheese() {
		this.toppings.add("파마산");
		return this;
	}
	public PizzaBuilder addSauce() {
		this.toppings.add("소스");
		return this;
	}
	public PizzaBuilder addTomatoes() {
		this.toppings.add("으깬 토마토");
		return this;
	}
	public PizzaBuilder addGarlic() {
		this.toppings.add("마늘");
		return this;
	}
	public PizzaBuilder addOlives() {
		this.toppings.add("올리브");
		return this;
	}
	public PizzaBuilder addSpinach() {
		this.toppings.add("시금치");
		return this;
	}
	public PizzaBuilder addPepperoni() {
		return this;
	}
	public PizzaBuilder addSausage() {
		return this;
	}
	
}

 

MeatLoversPizzaBuilder, VeggieLoversPizzaBuilder는 PizzaBuilder를 상속 받아 필요한 토핑을 첨가하는 과정을 구현한다. 아주 단순하다. 이렇게 만든 빌더를 사용하는 방법은 다음과 같다.

 

public class PizzaClient {
	public static void main(String[] args) {
		PizzaBuilder veggieBuilder = new VeggieLoversPizzaBuilder();
		Pizza veggie = veggieBuilder
        		.addSauce()
                        .addCheese()
                        .addOlives()
                        .addTomatoes()
                        .addSausage()
                        .build();
                        
		veggie.prepare();
		veggie.bake();
		veggie.cut();
		veggie.box();
		System.out.println(veggie);
        
// 준비하는중 채소를 좋아하는 사람을 위한 피자
// 반죽을 던진다!
// 소스를 넣는다!
// 토핑을 넣는다!: 
//    소스
//    파마산
//    올리브
//    으깬 토마토
// 350도에서 25분간 굽는다!
// 피자를 자른다!
// 박스에 피자를 담는다!
// ---- 채소를 좋아하는 사람을 위한 피자 ----
// 소스
// 파마산
// 올리브
// 으깬 토마토
		
		PizzaBuilder meatBuilder = new MeatLoversPizzaBuilder();
		Pizza meat = meatBuilder
        		.addSauce()
                        .addTomatoes()
                        .addCheese()
                        .addSausage()
                        .addPepperoni()
                        .build();
                        
		meat.prepare();
		meat.bake();
		meat.cut();
		meat.box();
		System.out.println(meat);
        
// 준비하는중 고기를 좋아하는 사람을 위한 피자
// 반죽을 던진다!
// 소스를 넣는다!
// 토핑을 넣는다!: 
//    토마토 소스
//    자른 토마토
//    모짜렐라
//    소시지
//    페퍼로니
// 350도에서 25분간 굽는다!
// 피자를 자른다!
// 박스에 피자를 담는다!
// ---- 고기를 좋아하는 사람을 위한 피자 ----
// 토마토 소스
// 자른 토마토
// 모짜렐라
// 소시지
// 페퍼로니
	}
}

 

클라이언트가 피자 빌더 추상 클래스(빌더)에게 피자를 생성해달라고 한다. 구상 빌더는 실제 피자(특정한 종류의 피자들)를 만든다. 클라이언트는 토핑을 얹는 몇 단계를 거쳐서 피자를 생성해달라고 요청한 다음 build() 메소드를 통해 최종적으로 피자를 만들었다.

 

이렇게 복합 객체 생성 과정(특정한 종류들의 피자를 만드는 과정)을 캡슐화했다. 덕분에 제품 내부 구조를 클라이언트로부터 보호할 수 있어졌다. 또한 클라이언트는 추상 인터페이스 혹은 클래스만 볼 수 있으므로 피자를 구현한 코드는 쉽게 바꿀 수 있게 된다.

 

빌더 패턴 다이어그램

 

복합 객체의 생성 과정, 표현 방법을 분리해서 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 형태이다. 즉, 피자를 만들 때 만드는 과정과 피자를 표현하는 방법을 분리하여 같은 빌더를 상속 받아 만들고 있지만 각각 고기가 들어간 피자, 비건 피자가 만들어질 수 있게 되었다.

 

DTO를 만들 때 빌더 패턴을 쓰기도 한다. 대신 인자가 3개이상이 되는 등 복잡한 조건의 매개변수를 받을 때 유용하다.

 

UserDTO userDTO = new UserDTO("louis", "1998", null, true)

UserDTO userDTO = new UserDTO.Builder("louis")
				.setBirthYear("1998")
                        	.setGender(null)
                        	.setAgree(true)
                		.build();

 

조건이 복잡한 인자들이 여러개 있을 때 다음과 같이 빌더 패턴을 적용하여 DTO 객체를 만들 수 있다. 훨씬 간단하고 어떤 값들이 필수인지, 어떤 형식으로 들어가는지 한눈에 파악하기 좋다.

 

 

 

 

 

* 아래의 자료들을 참고하였습니다.

에릭 프리먼 · 엘리자베스 롭슨 · 케이시 시에라 · 버트 베이츠, 『헤드퍼스트 디자인패턴 개정판』, 서환수 옮김, 한빛미디어(2022), p634-635.

댓글