빌더 패턴(Builder Pattern)

 

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

 

예시로는 수제 햄버거를 주문할때 빵을 햄버거 번으로 할지 식빵으로할지 등등 주문하는 사람이 정하고,

들어가는 재료도 상추를 넣을지 토마토를 넣을지, 치즈를 넣을지 말지 등등 모두 주문하는 사람이 마음대로 결정한다.

이처럼 속재료들을 선택적으로 유연하게 받아 다양한 타입의 인스턴스를 생성할 수 있기에

클래스의 선택적 매개변수가 많은 상황에서 유용하게 사용된다.

 

 

 

빌더 패턴이 없다면?

 

빌더 패턴이 없을때 수제 햄버거를 만든다면 어떻게 해야할까?

 

1) 점층적 생성자 패턴

- 빵만 있는 버거를 위한 생성자

- 빵과 패티가 있는 버거를 위한 생성자

- 빵과 패티와 치즈가 있는 버거를 위한 생성자

- 빵과 패티와 치즈와 상추가 있는 버거를 위한 생성자

- .....

이런식으로 모든 경우의 생성자를 다 만드는 것이 점층적 생성자 패턴이다.

class Hamburger {
private:
    // 필수 매개변수
    int bun;
    int patty;

    // 선택 매개변수
    int cheese;
    int lettuce;
    int tomato;
    int bacon;

public:
    Hamburger(int bun, int patty, int cheese, int lettuce, int tomato, int bacon) {
        this->bun = bun;
        this->patty = patty;
        this->cheese = cheese;
        this->lettuce = lettuce;
        this->tomato = tomato;
        this->bacon = bacon;
    }

    Hamburger(int bun, int patty, int cheese, int lettuce, int tomato) {
        this->bun = bun;
        this->patty = patty;
        this->cheese = cheese;
        this->lettuce = lettuce;
        this->tomato = tomato;
    }

    Hamburger(int bun, int patty, int cheese, int lettuce) {
        this->bun = bun;
        this->patty = patty;
        this->cheese = cheese;
        this->lettuce = lettuce;
    }

    Hamburger(int bun, int patty, int cheese) {
        this->bun = bun;
        this->patty = patty;
        this->cheese = cheese;
    }

    Hamburger(int bun, int patty) {
        this->bun = bun;
        this->patty = patty;
    }
};

int main()
{
	// 모든 재료가 있는 햄버거
	Hamburger hamburger1(2, 1, 2, 4, 6, 8);

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

	// 빵과 패티 베이컨만 있는 햄버거
	Hamburger hamburger3(2, 1, 0, 0, 0, 6);

	return 0;
}

코드만 봐도 알겠지만 클래스의 인스턴스들이 많으면 많을수록 생성자에 들어갈 인자의 수도 늘어나고, 생성자의 갯수도 엄청나게 늘어나게 된다.

코드의 복잡성뿐 아니라 사용하는 프로그래머 입장에서도 헷갈릴 경우가 많다.

인자의 순서가 무엇인지 파악도 해야하며, 만약 빵과 베이컨만 있는 햄버거를 만든다면 매개변수를 (1, 0, 0, 0, 0, 1)처럼 써야하니 쓸데없이 0만 많이 넣게 된다.

 

2) 자바 빈(Java Beans) 패턴

위의 점층적 생성자 패턴을 보완하기 위해 만들어진 패턴

매개변수가 없는 생성자로 객체를 생성한 후 setter 메서드를 사용해 객체의 인스턴스들을 초기화해주는 방식

class Hamburger {
private:
    // 필수 매개변수
    int bun;
    int patty;

    // 선택 매개변수
    int cheese;
    int lettuce;
    int tomato;
    int bacon;

public:
    // 기본 생성자
    Hamburger() { }

    // setter 함수들
    void setBun(int bun) { this->bun = bun; }
    void setPatty(int patty) { this->patty = patty; }
    void setCheese(int cheese) { this->cheese = cheese; }
    void setLettuce(int lettuce) { this->lettuce = lettuce; }
    void setTomato(int tomato) { this->tomato = tomato; }
    void setBacon(int bacon) { this->bacon = bacon; }
};

int main()
{
    // 모든 재료가 있는 햄버거
    Hamburger hamburger1;
    hamburger1.setBun(2);
    hamburger1.setPatty(1);
    hamburger1.setCheese(2);
    hamburger1.setLettuce(4);
    hamburger1.setTomato(6);
    hamburger1.setBacon(8);

    // 빵과 패티 치즈만 있는 햄버거
    Hamburger hamburger2;
    hamburger2.setBun(2);
    hamburger2.setPatty(1);
    hamburger2.setCheese(2);

    // 빵과 패티 베이컨만 있는 햄버거
    Hamburger hamburger3;
    hamburger3.setBun(2);
    hamburger2.setPatty(1);
    hamburger3.setBacon(8);

	return 0;
}

이 방법은 점진적 생성자 방식에 비해 가독성이 좋은 편이며, 객체 매개변수를 자유롭게 조정할 수 있으니 유연한 객체생성이 가능하다. 하지만 이 방법도 일관성문제와 불변성문제가 나타난다.

 

- 일관성 문제

클래스에 주석으로 필수 매개변수라고 작성되어 있는 인스턴스처럼 필수로 값이 들어가야할 값이 있다면 값이 반드시 들어가야 하지만, 개발자의 실수로 생성자 호출 후 setBun(), setPatty() setter를 호출하지 않는다면 객체가 유효하지 않으며 일관성이 무너진 상태가 된다.

물론 필수 매개변수만 생성자 내부에서 초기화하고, 그 외의 매개변수들은 setter로 설정하는 방법도 가능하지만 아래의 불변성 문제가 남아있다.

 

- 불변성 문제

객체를 생성한 후 setter로 객체의 속성을 설정한 이후에도 setter함수들은 public으로 노출되어 있으므로 언제 어디서 setter메서드로 객체가 조작될지 알 수 없다.

 

 

 

빌더 패턴을 쓴다면?

 

빌더 패턴은 위의 문제들을 해결하기 위해 별도의 Builder클래스를 만들고

Builder클래스의 임시객체로 step-by-step으로 메소드를 호출한 후 값을 설정하고 다시 자기자신을 리턴하면서

최종적으로 build()메소드를 사용해 하나의 인스턴스를 생성하여 리턴하는 패턴

 

#include <iostream>
using namespace std;

class Student { // 만들고자 하는 객체는 Student 객체
private:
    int _id;
    string _name;
    string _grade;
    string _phoneNumber;

public:
    Student(int id, string name, string grade, string phoneNumber) {
        _id = id;
        _name = name;
        _grade = grade;
        _phoneNumber = phoneNumber;
    }

    void print() {
        cout << _id << ' ' << _name << ' ' << _grade << ' ' << _phoneNumber;
    }
};

class StudentBuilder { // Student의 정보를 때에따라 구현하기 위해 Builder클래스 구현
private: // 멤버 변수는 Student와 동일하게 구성
    int _id;
    string _name;
    string _grade;
    string _phoneNumber;

public: // setter메서드를 구현, 단 여기선 가독성과 기존의 setter와의 차이점을 부각하기 위해 set단어는 제외
    StudentBuilder id(int id) { // setID가 아닌 그냥 id로 써서 네이밍하는걸 추천한다.
        _id = id;
        return *this; // 이런식으로 계속해서 객체 자신을 반환함
    }

    StudentBuilder name(string name) {
        _name = name;
        return *this;
    }

    StudentBuilder grade(string grade) {
        _grade = grade;
        return *this;
    }

    StudentBuilder phoneNumber(string phoneNumber) {
        _phoneNumber = phoneNumber;
        return *this;
    }

    Student build() {
        return Student(_id, _name, _grade, _phoneNumber);
    }
};

int main()
{
    Student student = StudentBuilder() // 임시객체를 만들고
        .id(2016120091) // 임시객체로 이 함수를 호출하고, 함수가 또 객체 자신을 리턴
        .name("임꺽정") // 리턴된 객체로 또 함수를 호출
        .grade("Senior")
        .phoneNumber("010-5555-5555")
        .build(); // 마지막에 build함수 호출로 Student객체 리턴
	// 위처럼 자기 자신을 리턴하면서 연속적으로 메서드들을 호출하는걸 체이닝(Chaining)이라고 함

    student.print();

    return 0;
}

 

 

 

장단점

 

1. 장점

1) 객체 생성 과정이 일관되며 인자로 어떤 값이 설정되는지 한눈에 파악하기 좋아서 가독성이 좋음

2) 자바의 경우 함수 매개변수에 default값을 설정할수 없다고 한다. 자세히는 모르겠지만 이걸 빌더패턴을 이용해 디폴트 매개변수를 사용하는것처럼 사용할 수 있다고 함

3) 필수멤버와 선택적멤버를 분리 가능, 아래의 코드처럼 필수멤버만 생성자의 매개변수로 넣고, 선택멤버들은 설정 하거나 안하거나 선택 가능

Student student1 = 
        StudentBuilder(2016120091) // 필수 멤버
        .name("홍길동") // 선택 멤버
        .build();

Student student2 = 
        StudentBuilder(2016120091) // 필수 멤버
        .name("임꺽정") // 선택 멤버
        .grade("freshman") // 선택 멤버
        .build();

Student student3 = 
        StudentBuilder(2016120091) // 필수 멤버
        .name("주몽") // 선택 멤버
        .grade("Senior") // 선택 멤버
        .phoneNumber("010-5555-5555") // 선택 멤버
        .build();

4) 객체 생성을 단계별로 구성하거나, 지연하거나, 재귀적으로 구성할 수 있음

// 1. 빌더 클래스 전용 리스트 생성
vector<StudentBuilder> builders;

// 2. 객체를 최종 생성 하지말고 초깃값만 세팅한 빌더만 생성
builders.push_back(
    StudentBuilder(2016120091)
    .name("홍길동")
);

builders.push_back(
    StudentBuilder(2016120092)
    .name("임꺽정")
    .grade("senior")
);

builders.push_back(
    StudentBuilder(2016120093)
    .name("박혁거세")
    .grade("sophomore")
    .phoneNumber("010-5555-5555")
);

// 3. 나중에 빌더 리스트를 순회하여 최종 객체 생성을 주도
for(StudentBuilder b : builders) {
    Student student = b.build();   // 설정만 미리 해둔 객체들을 실제로 생성하고 싶은 시점에 build() 호출
}

5) 초기화 검증을 멤버별로 분리

만약 Student생성자를 이용해 객체를 생성한다면 객체 생성시에 멤버 인스턴스의 값이 범위를 벗어나지 않는지 확인하기 위해 생성자 내에서 학년은 1~4인지, 전화번호는 형식이 맞는지 등을 if로 체크해서 이상하면 예외를 던져줘야 한다.

 

하지만 Builder 패턴으로 구현한다면 멤버 인스턴스를 설정하는 영역이 하나씩 구분되서 함수로 구성되므로 함수 하나에서 하나의 인스턴스에 대한 검증만 하면 되므로 코드가 간결해지고, 구분하기 좋아지며, 유지보수하기 좋아진다.

setter를 이용하는것과 비슷한데 setter를 먼저 구현한 후 생성자 내에서 값 설정을 setter함수를 호출하는 식으로 하는것과 같은 역할을 한다고 보면 됨

 

6) setter로 인한 불변성 문제가 발생할 가능성 최소화할 수 있다.

먼저 setter로 변화가 생길 수 있는 가변객체와 setter를 제공하지 않아 변화가 불가능한 불변객체 중 불변객체를 선호하는 이유는

- 불변객체는 Thread-safe하여 동기화를 고려하지 않아도 된다.

- 가변객체는 작업중 exception이 발생하면 해당 객체가 불안정한 상태에 빠질 수 있어 2차 오류발생 가능성이 있다.

- 다른 사람이 개발한 함수를 불변객체로 사용하면 위험없이 이용을 보장할 수 있기에 협업이나 유지보수에 유용하다.

그러므로 클래스들은 정말 필요한 경우가 아니면 불변객체로 만들어야 하며, 불변으로 만드는것이 불가능하다면 변경 가능성을 최소화 해야 한다.

 

2. 단점

1) 코드 복잡성 증가

- 빌더 패턴을 적용하려면 클래스가 N개 있다면 빌더 클래스도 N개 만들어야 하므로 클래스의 수가 엄청나게 늘어나게 됨

이로인해 관리해야 할 클래스도 많아지고, 구조가 복잡해질 수 있음

 

2) 생성자보다는 성능이 떨어짐

- 임시객체를 생성해서 함수호출을 여러번 한 후 생성되므로

- 근데 이건 성능을 극도로 신경쓸때 아니면 대부분의 경우 큰건 아니긴 함

 

3) 지나친 빌더 남용은 금지

클래스의 필드의 갯수가 4개보다 적어서 매개변수 작성이 어렵지 않거나,

필드의 변경 가능성이 애초에 없어서 불변성을 신경쓸 필요가 없다면 괜히 코드만 길어지게 사용해야할 이유가 없다.

이럴땐 그냥 기본적인 생성자나 정적 팩토리 메소드를 이용하는것이 좋다.

 

 

 

Builder 패턴의 두 종류

 

Builder패턴에는 두가지 종류가 있다.

- Effective Java의 Builder Pattern인 심플 빌더 패턴

- GOF의 Builder Pattern인 디렉터 빌더 패턴

  심플 빌더 패턴 디렉터 빌더 패턴
특징 일반적으로 개발자들이 말하는 빌더 패턴  
소개된 곳 Effective Java GoF
소개된 때 2001년 1994년
목적성 객체의 일관성 불변성  
사용 시점 생성시 지정해야할 인자가 많을 때 객체의 생성 단계 순서를 결정해두고 각 단계를 다양하게 구현하고 싶을 때

 

1) 심플 빌더 패턴

- 생성자가 많을 경우 또는 변경 불가능한 불변 객체가 필요한 경우

- 코드의 가독성과 일관성, 불변성을 유지하는것에 중점

- 위에서 배운 빌더와 크게 다를것은 없지만 Builder 클래스가 구현할 클래스의 정적 내부 클래스(static inner class)로 구현된다는 차이가 있음

#include <iostream>
#include <list>
using namespace std;

class Student {
private:
    int _id;
    string _name;
    string _grade;
    string _phoneNumber;    
    
    // 생성자를 private로
    Student(int id, string name, string grade, string phoneNumber) {
        _id = id;
        _name = name;
        _grade = grade;
        _phoneNumber = phoneNumber;
    }

public:
	// static inner 빌더 클래스
	static class StudentBuilder {
	private:
	    int _id;
	    string _name;
	    string _grade;
	    string _phoneNumber;
	
	public:
	    StudentBuilder id(int id) {
	        _id = id;
	        return *this;
	    }

    	StudentBuilder name(string name) {
	        _name = name;
	        return *this;
	    }

    	Student build() {
	        return Student(_id, _name, _grade, _phoneNumber);
	    }
	};

    void print() {
        cout << _id << ' ' << _name << ' ' << _grade << ' ' << _phoneNumber;
    }
};

int main()
{
    Student student = StudentBuilder()
        .id(2016120091)
        .name("임꺽정")
        .build(); 

    student.print();

    return 0;
}

위와 같은 형태로 클래스의 내부에 빌더 클래스를 static으로 만드는 것

static inner class를 사용하는 이유는

- 하나의 빌더 클래스는 하나의 객체 생성만을 위해 사용되므로 두 클래스를 물리적으로 합치는 것

- 대상 객체는 오로지 빌더 객체를 통해서만 초기화되므로 생성자를 외부에 노출시키지 않기 위해 생성자를 private로 하고, 내부 빌더 클래스에서 private 생성자를 호출하는 것

- static을 쓰는 이유는 static이 없다면 외부 클래스 객체를 생성한 후 내부 클래스를 생성해야 하는데 목적성과 정반대가 되므로

- Managed Language에서는 static을 안쓰면 메모리누수가 생길 수 있다고 한다.

 

2) 디렉터 빌더 패턴

GoF의 객체의 생성 알고리즘과 조립 방법을 분리하는것이 목적

다양한 디자인 패턴을 짬뽕시킨 패턴이라고 하며 객체 생성 자체의 목적뿐 아니라 여러가지 빌드 형식을 고도화시킨 패턴이라고 한다.

자세한 내용은 다음기회에 보는걸로....

 

 

 

 

 

 

 

 

※ 참고문헌

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EB%B9%8C%EB%8D%94Builder-%ED%8C%A8%ED%84%B4-%EB%81%9D%ED%8C%90%EC%99%95-%EC%A0%95%EB%A6%AC