빌더 패턴(Builder Pattern)은 복잡한 객체의 생성 과정과 표현 방법을 분리하여 다양한 구성의 인스턴스를 만드는 생성 패턴입니다.
생성자에 들어갈 매개변수를 메서드로 하나하나 받아들이고 마지막에 통합 빌드해서 객체를 생성하는 방식입니다.
점층적 생성자 패턴(Telescoping Constructor Pattern)은 필수 매개변수와 함께 선택 매개변수를 0개, 1개, 2개 .. 받는 형태를 말합니다. 우리가 다양한 매개변수를 입력받아 인스턴스를 생성하고 싶을때 사용하던 생성자를 오버로딩 하는 방식입니다.
Hamburger 클래스의 필드에 따라 생성자를 작성한 예제입니다.
class Hamburger {
// 필수 매개변수
private int bun;
private int patty;
// 선택 매개변수
private int cheese;
private int lettuce;
private int tomato;
private 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;
}
public 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;
}
public Hamburger(int bun, int patty, int cheese, int lettuce) {
this.bun = bun;
this.patty = patty;
this.cheese = cheese;
this.lettuce = lettuce;
}
public Hamburger(int bun, int patty, int cheese) {
this.bun = bun;
this.patty = patty;
this.cheese = cheese;
}
...
}
Hamburger 인스턴스를 다양한 생성자로 초기화하는 예제입니다.
public static void main(String[] args) {
// 모든 재료가 있는 햄버거
Hamburger hamburger1 = new Hamburger(2, 1, 2, 4, 6, 8);
// 빵과 패티 치즈만 있는 햄버거
Hamburger hamburger2 = new Hamburger(2, 1, 1);
// 빵과 패티 베이컨만 있는 햄버거
Hamburger hamburger3 = new Hamburger(2, 0, 0, 0, 0, 6);
}
하지만 이러한 방식은 클래스 인스턴스 필드들이 많으면 많을 수록 생성자에 들어갈 인자의 수가 늘어나 몇번째 인자가 어떤 필드였는지 햇갈릴 경우가 생기게 됩니다. 만일 여러 종류의 햄버거를 생성하기 위해 Hamburger 생성자의 몇번째 인수가 양상추의 갯수인지 토마토의 갯수인지 파악할 필요가 있습니다.
또한 매개변수 특성상 순서를 따라야 하기 때문에 위의 '빵과 베이컨만 있는 햄버거'를 원할경우 억지로 파라미터에 0을 전달해야 됩니다. 생성자로만으로는 필드를 선택적으로 생략할 수 있는 방법이 없기 때문입니다. 그리고 무엇보다 타입이 다양할 수록 생성자 메서드 수가 기하급수적으로 늘어나 가독성이나 유지보수 측면에서 좋지 않습니다.
자바 빈(Java Beans) 패턴은 점층적 생성자 패턴의 이러한 단점을 보완하기 위해 Setter 메소드를 사용한 자바 빈(Bean) 패턴이 고안 되었습니다. 매개변수가 없는 생성자로 객체 생성후 Setter 메소드를 이용해 클래스 필드의 초깃값을 설정하는 방식입니다.
Hamburger 클래스의 필드에 따라 Setter 메서드를 작성한 예제입니다.
class Hamburger {
// 필수 매개변수
private int bun;
private int patty;
// 선택 매개변수
private int cheese;
private int lettuce;
private int tomato;
private int bacon;
public Hamburger() {}
public void setBun(int bun) {
this.bun = bun;
}
public void setPatty(int patty) {
this.patty = patty;
}
public void setCheese(int cheese) {
this.cheese = cheese;
}
public void setLettuce(int lettuce) {
this.lettuce = lettuce;
}
public void setTomato(int tomato) {
this.tomato = tomato;
}
public void setBacon(int bacon) {
this.bacon = bacon;
}
}
Hamburger 인스턴스를 Setter를 통하여 초기화하는 예제입니다.
public static void main(String[] args) {
// 모든 재료가 있는 햄버거
Hamburger hamburger1 = new Hamburger();
hamburger1.setBun(2);
hamburger1.setPatty(1);
hamburger1.setCheese(2);
hamburger1.setLettuce(4);
hamburger1.setTomato(6);
hamburger1.setBacon(8);
// 빵과 패티 치즈만 있는 햄버거
Hamburger hamburger2 = new Hamburger();
hamburger2.setBun(2);
hamburger2.setPatty(1);
hamburger2.setCheese(2);
// 빵과 패티 베이컨만 있는 햄버거
Hamburger hamburger3 = new Hamburger();
hamburger3.setBun(2);
hamburger2.setPatty(1);
hamburger3.setBacon(8);
}
기존 생성자 오버로딩에서 나타났던 가독성 문제점이 사라지고 선택적인 파라미터에 대해 해당되는 Setter 메서드를 호출함으로써 유연적으로 객체 생성이 가능해졌습니다. 하지만 이러한 방식은 객체 생성 시점에 모든 값들을 주입 하지 않아 일관성(consistency) 문제와 불변성(immutable) 문제가 나타나게 됩니다.
필수 매개변수란 객체가 초기화될때 반드시 설정되어야 하는 값입니다. 하지만 개발자가 깜빡하고 setBun()
이나 setPatty()
메서드를 호출하지 않았다면 이 객체는 일관성이 무너진 상태가 됩니다. 즉, 객체가 유효하지 않은 것입니다. 만일 다른곳에서 햄버거 인스턴스를 사용하게 된다면 런타임 예외가 발생할 수도 있습니다.
이는 객체를 생성하는 부분과 값을 설정하는 부분이 물리적으로 떨어져 있어서 발생하는 문제점입니다. 물론 이는 어느정도 생성자(Constructor)와 결합하여 극복은 할 수 있습니다. 하지만 다음에 소개할 불변성의 문제 때문에 자바 빈즈 패턴은 지양해야 합니다.
자바 빈즈 패턴의 Setter 메서드는 객체를 처음 생성할때 필드값을 설정하기 위해 존재하는 메서드입니다. 하지만 객체를 생성했음에도 여전히 외부적으로 Setter 메소드를 노출하고 있으므로, 협업 과정에서 언제 어디서 누군가 Setter 메서드를 호출해 함부로 객체를 조작할수 있게 됩니다. 이것을 불변함을 보장할 수 없다고 얘기합니다.
마치 완성된 햄버거에 중간에 치즈를 교체한다고 햄버거를 막 분리하는 것과 같은 이치입니다.
빌더(Builder) 패턴은 이러한 문제들을 해결하기 위해 별도의 Builder 클래스를 만들어 메소드를 통해 step-by-step 으로 값을 입력받은 후에 최종적으로 build()
메소드로 하나의 인스턴스를 생성하여 리턴하는 패턴입니다.
빌더 패턴 사용법을 잠시 살펴보면, StudentBuilder
빌더 클래스의 메서드를 체이닝(Chaining) 형태로 호출함으로써 자연스럽게 인스턴스를 구성하고 마지막에 build()
메서드를 통해 최종적으로 객체를 생성하도록 되어있음을 볼 수 있습니다.
public static void main(String[] args) {
// 생성자 방식
Hamburger hamburger = new Hamburger(2, 3, 0, 3, 0, 0);
// 빌더 방식
Hamburger hamburger = new Hamburger.Builder(10)
.bun(2)
.patty(3)
.lettuce(3)
.build();
}
빌더 패턴을 이용하면 더이상 생성자 오버로딩 열거를 하지 않아도 되며, 데이터의 순서에 상관없이 객체를 만들어내 생성자 인자 순서를 파악할 필요도 없고 잘못된 값을 넣는 실수도 하지 않게 됩니다. 점층적 생성자 패턴과 자바빈즈 패턴 두 가지의 장점만을 취하였다고 볼 수 있습니다.
빌더 패턴 구조는 빌더 패턴 구현 자체는 난이도가 어렵지 않으니 빠르고 쉽게 구성이 가능합니다.
다음은 Student 클래스에 대한 객체 생성만을 담당하는 별도의 빌더 클래스 예제입니다.
class Student {
private int id;
private String name = "아무개";
private String grade = "freshman";
private String phoneNumber = "010-0000-0000";
public Student(int id, String name, String grade, String phoneNumber) {
this.id = id;
this.name = name;
this.grade = grade;
this.phoneNumber = phoneNumber;
}
@Override
public String toString() {
return "Student { " +
"id='" + id + '\'' +
", name=" + name +
", grade=" + grade +
", phoneNumber=" + phoneNumber +
" }";
}
}
먼저 Builder
클래스를 만들고 필드 멤버 구성을 만들고자 하는 Student
클래스 멤버 구성과 똑같이 구성합니다.
class StudentBuilder {
private int id;
private String name;
private String grade;
private String phoneNumber;
}
그리고 각 맴버에 대한 Setter
메서드를 구현합니다. 이때 가독성을 좋게 하면서도 기존 Setter
와의 다른 특성을 가지고 있는 점을 알리기 위해서, set
단어는 빼주고 심플하게 멤버이름으로만 메서드명을 지어줍니다.
class StudentBuilder {
private int id;
private String name;
private String grade;
private String phoneNumber;
public StudentBuilder id(int id) {
this.id = id;
return this;
}
public StudentBuilder name(String name) {
this.name = name;
return this;
}
public StudentBuilder grade(String grade) {
this.grade = grade;
return this;
}
public StudentBuilder phoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
}
여기서 주목할 부분은 각 Setter
함수 마지막 반환 구문인 return this
부분입니다. 여기서 this
란 StudentBuilder
객체 자신을 말합니다. 즉, 빌더 객체 자신을 리턴함으로써 메서드 호출 후 연속적으로 빌더 메서드들을 체이닝(Chaining) 하여 호출할 수 있게 됩니다. 예를 들어 new StudentBuilder().id(값).name(값)
작성합니다.
마지막으로 빌더의 목표였던 최종 Student
객체를 만들어주는 build()
메서드를 구성합니다. 빌더 클래스의 필드들을 Student
생성자의 인자에 넣어줌으로써 멤버 구성이 완료된 Student
인스턴스를 얻게 되는 것입니다.
class StudentBuilder {
private int id;
private String name;
private String grade;
private String phoneNumber;
public StudentBuilder id(int id) { ... }
public StudentBuilder name(String name) { ... }
public StudentBuilder grade(String grade) { ... }
public StudentBuilder phoneNumber(String phoneNumber) { ... }
public Student build() {
return new Student(id, name, grade, phoneNumber); // Student 생성자 호출
}
}
public static void main(String[] args) {
Student student = new StudentBuilder()
.id(2016120091)
.name("임꺽정")
.grade("Senior")
.phoneNumber("010-5555-5555")
.build();
System.out.println(student);
}
빌더 패턴의 멤버 설정 메서드 네이밍 방식에는 대표적으로 3가지 정도 존재합니다.
멤버이름()
set멤버이름()
with멤버이름()
Student student = new StudentBuilder(2016120091)
.name("홍길동")
.grade("freshman")
.phoneNumber("010-5555-5555")
.build();
Student student = new StudentBuilder(2016120091)
.setName("홍길동")
.setGrade("freshman")
.setPhoneNumber("010-5555-5555")
.build();
Student student = new StudentBuilder(2016120091)
.withName("홍길동")
.withGrade("freshman")
.withPhoneNumber("010-5555-5555")
.build();
네이밍 형식 중에서 멤버 이름()
으로만 메서드명을 짓는 첫번째 네이밍 방식이 추천하고 있습니다. 두번째 네이밍 방식은 정통적인 자바(Java) 스러운 네이밍 형식인데 일반 Setter
메소드와 햇깔릴 소지가 있습니다. 세번째는 Setter
와 구분키 위해 'with'
라는 키워드를 사용한것이며, 빌더 지연 생성 방식에서 미리 빌더를 설정할때 쓰이기도 합니다.
객체 생성 과정을 일관된 프로세스로 표현한다.
생성자 방식으로 객체를 생성하는 경우는 매개변수가 많아질수록 가독성이 급격하게 떨어집니다. 클래스 변수가 4개 이상만 되어도 각 인자 순서 마다 이 값이 어떤 멤버에 해당되는지 바로 파악이 힘듭니다.
반면 다음과 같이 빌더 패턴을 적용하면 직관적으로 어떤 데이터에 어떤 값이 설정되는지 한눈에 파악할 수 있게 됩니다. 특히 연속된 동일 타입의 매개 변수를 많이 설정할 경우에 발생할 수 있는 설정 오류와 같은 실수를 방지할 수 있습니다.
다음 예제는 생성자 방식과 빌더 방식의 객체 생성 과정입니다.
// 생성자 방식
Student student1 = new Student(2016120091, "홍길동", "freshman", "010-5555-5555");
// 빌더 방식
Student student2 = new StudentBuilder()
.id(2016120091)
.name("임꺽정")
.grade("Senior")
.phoneNumber("010-5555-5555")
.build();
다만 빌더 패턴이 탄생하게된 옛 시대와 달리, 요즘에는 인텔리제이나 이클립스 같은 왠만한 IDE에선 아래와 같이 생성자 매개변수에 대한 미리보기 힌트 기능을 제공해주기 때문에 이 부분은 요즘 트렌드에는 맞지 않을 수도 있습니다.
본래 디폴트 매개변수라는 건 인자 값을 설정해줘도 되고 설정 안하고 생략해도 되는것을 말합니다. 그런데 파이썬이나 자바스크립트와 달리 자바 언어에선 기본적으로 메서드에 대한 디폴트 매개변수를 지원하지 않습니다.
따라서 디폴트 매개변수를 구현하기 위해선 클래스 필드 변수에 초깃값을 미리 세팅하고, 초깃값이 세팅된 필드 인자를 제외시킨 생성자를 따로 구현하는 식으로 설계해야 합니다. 하지만 이는 결국 지나친 생성자 오버로딩 열거를 통한 본래의 문제점을 회귀한 꼴이 됩니다.
class Student {
private int id;
private String name;
private String grade = "freshman"; // 디폴트 매개변수 역할
private String phoneNumber;
public Student(int id, String name, String grade, String phoneNumber) {
...
}
// 디폴트 매개변수를 제외한 인자들을 받는 생성자 오버로딩
public Student(int id, String name, String phoneNumber) {
...
}
@Override
public String toString() {
return "Student { " +
"id='" + id + '\'' +
", name=" + name +
", grade=" + grade +
", phoneNumber=" + phoneNumber +
" }";
}
}
빌더 패턴에서도 디폴트 매개변수를 구현하는 방법은 똑같습니다. 다만 빌더라는 객체 생성 전용 클래스를 경유하여 이용함으로써 디폴트 매개변수가 설정된 필드를 설정하는 메서드를 호출하지 않는 방식으로 마치 디폴트 매개변수를 생략하고 호출하는 효과를 간접적으로 구현할수 있게 됩니다.