[디자인패턴] 빌더 패턴 (Builder Pattern)

koline·2023년 8월 13일
0

디자인패턴

목록 보기
4/24

빌더 패턴


빌더 패턴은 복잡한 객체의 생성 과정과 표현 방법을 분리하여 다양한 구성의 인스턴스를 만드는 생성 패턴이다. 생성자에 들어갈 매개 변수를 메서드로 하나하나 받아들이고 마지막에 통합 빌드해서 객체를 생성하는 방식이다.

빌더 패턴을 사용하면 같은 제작 코드를 사용하여 객체의 다양한 유형들과 표현을 제작할 수 있다.

대표적인 예로 햄버거 제작을 들 수 있다. 햄버거를 주문할때 빵이나 패티 등 속재료들은 주문하는 사람이 마음대로 결정된다. 어느 사람은 치즈를 빼달라고 할수 있고 어느 사람은 토마토를 빼달라고 할수 있다. 이처럼 선택적 속재료들을 보다 유연하게 받아 다양한 타입의 인스턴스를 생성할수 있어, 클래스의 선택적 매개변수가 많은 상황에서 유용하게 사용된다.



탄생 배경


점층적 생성자 패턴


점층적 생성자 패턴은 가장 기본적인 생성자 형태로 각 매개변수 별로 생성자를 오버로딩하여 인스턴스를 생성하는 방식이다.

예를 들어 햄버거를 구성하는 요소로 빵, 패티, 채소, 치즈, 소스가 있고 빵과 패티는 필수적으로 들어가야할 요소라고 할 때 아래와 같이 햄버거 인스턴스를 생성 할 수 있다.

class Hamburger {
	
    // 필수 요소
    private int bread;
    private int patty;
    
    // 선택 요소 (동일한 매개변수 갯수와 타입의 메소드 오버로딩 불가)
    private int vegetable;
    private long cheese;
    private char sauce;
    
    public Hamburger(int bread, int patty) {
    	this.bread = bread;
        this.patty = patty;
    }
    
    public Hamburger(int bread, int patty, int vegetable) {
    	this.bread = bread;
        this.patty = patty;
        this.vegetable = vegetable;
    }
    
    public Hamburger(int bread, int patty, long cheese) {
    	this.bread = bread;
        this.patty = patty;
        this.cheese = cheese;
    }
    
    public Hamburger(int bread, int patty, char sauce) {
    	this.bread = bread;
        this.patty = patty;
        this.sauce = sauce;
    }
    
    public Hamburger(int bread, int patty, int vegetable, long cheese) {
    	this.bread = bread;
        this.patty = patty;
        this.vegetable = vegetable;
        this.cheese = cheese;
    }
    
    public Hamburger(int bread, int patty, int vegetable, char sauce) {
    	this.bread = bread;
        this.patty = patty;
        this.vegetable = vegetable;
        this.sauce = sauce;
    }
    
    public Hamburger(int bread, int patty, long cheese, char sauce) {
    	this.bread = bread;
        this.patty = patty;
        this.cheese = cheese;
        this.sauce = sauce;
    }
    
    public Hamburger(int bread, int patty, int vegetable, long cheese, char sauce) {
    	this.bread = bread;
        this.patty = patty;
        this.vegetable = vegetable;
        this.cheese = cheese;
        this.sauce = sauce;
    }
}
public static void main(String[] args) {
	// 모든 재료가 있는 햄버거
	Hamburger hamburger1 = new Hamburger(2, 1, 2, 4L, 'a');

	// 빵과 패티, 치즈만 있는 햄버거
	Hamburger hamburger2 = new Hamburger(2, 1, 1L);

	// 빵과 패티, 소스, 채소만 있는 햄버거
	Hamburger hamburger3 = new Hamburger(2, 0, 0, 0L, 'b');
}

이처럼 점층적 생성자 패턴을 사용할 경우, 선택적 매개변수의 종류에 따라 생성자를 일일히 작성 해줘야 한다. 게다가 같은 갯수와 종류의 매개변수를 가진 메서드는 오버로드 할수 없다.

이 방식을 사용할 경우 매개변수의 수가 늘어날수록 작성해야 하는 코드가 상당히 늘어나며, 순서도 모두 기억해서 작성해야한다..



자바 빈 패턴 (Java Beans Pattern)


그래서 등장한 것이 자바 빈 패턴으로 매개변수가 없는 생성자로 객체 생성후 Setter 메소드를 이용해 클래스 필드의 초깃값을 설정하는 방식이다.

class Hamburger {
	
    // 필수 요소
    private int bread;
    private int patty;
    
    // 선택 요소 (오버로딩할 필요가 없어진다)
    private int vegetable;
    private int cheese;
    private int sauce;
    
    public Hamburger() {}
    
    public void setBread(int bread) {
    	this.bread = bread;
   	}
    
    public void setPatty(int patty) {
    	this.patty = patty;
    }
    
    public void setVegetable(int vegetable) {
    	this.vegetable = vegetable;
    }
    
    public void setCheese(int cheese) {
    	this.cheese = cheese;
    }
    
    public void setSauce(int sauce) {
    	this.sauce = sauce;
    }
}
public static void main(String[] args) {
	// 햄버거 인스턴스 생성
	Hamburger hamburger = new Hamburger();

	hamburger.setBread(1);
    hamburger.setPatty(2);
    hamburger.setSauce(3);
}

코드의 길이가 혁신적으로 줄었다. 또한 기존 생성자 오버로딩에서 나타났던 가독성 문제점이 사라지고 선택적인 파라미터에 대해 해당되는 Setter 메서드를 호출함으로써 유연적으로 객체 생성이 가능해졌다. 하지만 이러한 방식은 객체 생성 시점에 모든 값들을 주입 하지 않아 일관성(consistency) 문제와 불변성(immutable) 문제가 나타나게 된다.

1) 일관성 문제

필수 매개변수란 객체가 초기화될때 반드시 설정되어야 하는 값이다. 하지만 개발자가 깜빡하고 setBun() 이나 setPatty() 메서드를 호출하지 않았다면 이 객체는 일관성이 무너진 상태가 된다. 즉, 객체가 유효하지 않은 것이다. 만일 다른곳에서 햄버거 인스턴스를 사용하게 된다면 런타임 예외가 발생할 수도 있다.

이는 객체를 생성하는 부분과 값을 설정하는 부분이 물리적으로 떨어져 있어서 발생하는 문제점이다. 물론 이는 어느정도 생성자(Constructor)와 결합하여 극복은 할 수 있다. 하지만 다음에 소개할 불변성의 문제 때문에 자바 빈즈 패턴은 지양해야 한다.

2) 불변성 문제

자바 빈즈 패턴의 Setter 메서드는 객체를 처음 생성할때 필드값을 설정하기 위해 존재하는 메서드이다. 하지만 객체를 생성했음에도 여전히 외부적으로 Setter 메소드를 노출하고 있으므로, 협업 과정에서 언제 어디서 누군가 Setter 메서드를 호출해 함부로 객체를 조작할수 있게 된다. 이것을 불변함을 보장할 수 없다고 얘기한다.

마치 완성된 햄버거에 중간에 치즈를 교체한다고 햄버거를 막 분리하는 것과 같다.



빌더 패턴 (Builder Pattern)


빌더 패턴은 이러한 문제들을 해결하기 위해 별도의 Builder 클래스를 만들어 메소드를 통해 step-by-step 으로 값을 입력받은 후에 최종적으로 build() 메소드로 하나의 인스턴스를 생성하여 리턴하는 패턴이다.

HamburgerBuilder 빌더 클래스의 메서드를 체이닝(Chaining) 형태로 호출함으로써 자연스럽게 인스턴스를 구성하고 마지막에 build() 메서드를 통해 최종적으로 객체를 생성한다.

class Hamburger {

    private int bread;
    private int patty;
    private int vegetable;
    private int cheese;
    private int sauce;

    public Hamburger(int bread, int patty, int vegetable, int cheese, int sauce) {
        this.bread = bread;
        this.patty = patty;
        this.vegetable = vegetable;
        this.cheese = cheese;
        this.sauce = sauce;
    }
}
class HamburgerBuilder {

    // 필수 요소
    private int bread;
    private int patty;

    // 선택 요소 (동일한 매개변수 갯수와 타입의 메소드 오버로딩 불가)
    private int vegetable;
    private int cheese;
    private int sauce;

    public HamburgerBuilder bread(int bread) {
        this.bread = bread;
        return this;
    }

    public HamburgerBuilder patty(int patty) {
        this.patty = patty;
        return this;
    }

    public HamburgerBuilder vegetable(int vegetable) {
        this.vegetable = vegetable;
        return this;
    }

    public HamburgerBuilder cheese(int cheese) {
        this.cheese = cheese;
        return this;
    }

    public HamburgerBuilder sauce(int sauce) {
        this.sauce = sauce;
        return this;
    }

    public Hamburger build() {
        return new Hamburger(bread, patty, vegetable, cheese, sauce);
    }

}
public static void main(String[] args) {
	// 햄버거 인스턴스 생성
	Hamburger hamburger = new HamburgerBuilder().bread(1).patty(3).cheese(2).build();
}

인스턴스를 생성하는 코드가 한줄로 간결해졌다. 이와 같이 빌더 패턴을 적용하면 직관적으로 어떤 데이터에 어떤 값이 설정되는지 한눈에 파악할 수 있게 된다. 특히 연속된 동일 타입의 매개 변수를 많이 설정할 경우에 발생할 수 있는 설정 오류와 같은 실수를 방지할 수 있다.

또한 아래와 같이 디폴트 값을 설정하거나 초기화 검증을 멤버별로 구분하여 실행할 수도 있다.

class Hamburger {
	private int bread = 1;
    private int patty = 1;
    ...
    
    public Hamburger cheese(int cheese) {
    	if (cheese == 0) {
        	throw new IllegalArgumentException(cheese);
        }
        
        this.cheese = cheese;
        return this;
    }
}


참고

[디자인패턴] 디자인패턴이란? - 생성패턴, 구조패턴, 행위패턴

빌더(Builder) 패턴 - 완벽 마스터하기

profile
개발공부를해보자

0개의 댓글