빌더 패턴은 복잡한 객체의 생성 과정과 표현 방법을 분리하여 다양한 구성의 인스턴스를 만드는 생성 패턴이다. 생성자의 매개 변수를 메서드로 각각 받아들여 마지막에 통합하여 생성하는 방식이다.
기존의 생성자 패턴의 한계가 존재하기 때문에 빌더 패턴이 생겨났다. 기존의 생성 방식의 문제점은 무엇이었을까?
점층적 생성자 패턴은 생성자를 오버라이딩하여 필수 매개변수와 선택 매개변수에 따라 생성자를 여러개 만드는 방식이다.
public class TelescopeBurger {
//필수 인자
private int bun;
private int patty;
//선택적 인자
private boolean cheese;
private boolean tomato;
public TelescopeBurger(int bun, int patty) {
this.bun = bun;
this.patty = patty;
}
public TelescopeBurger(int bun, int patty, boolean cheese) {
this(bun, patty);
this.cheese = cheese;
}
public TelescopeBurger(int bun, int patty, boolean cheese, boolean tomato) {
this(bun, patty, cheese);
this.tomato = tomato;
}
}
@Test
void 점층적_생성자_패턴_테스트(){
//필수 인자만 전달
TelescopeBurger telescopeBurger = new TelescopeBurger(1, 2);
//필수 인자 + 선택적 인자 전달
TelescopeBurger telescopeBurger1 = new TelescopeBurger(1, 2, true);
}
점층적 생성자 패턴의 단점은 인스턴스 필드들이 많을 수록 생성자에 들어갈 인자의 수가 늘어나 몇버째 인자가 어떤 필드였는지 헷갈리는 경우가 생기게 된다. 또한 매개변수 특성 상 순서를 따라야 함으로 필드를 선택적으로 초기화 할 수 없다. 무엇보다도 가독성과 유지보수성이 떨어진다.
점층적 패턴의 단점을 보안하기 위해서 나온 방식이 자바 빈 패턴이다. 자바 빈 패턴은 매개변수가 없는 생성자로 객체를 생성후 setter 메서드를 통해 인스턴스를 초기화 하는 방식이다.
public class JavaBeanBurger {
//필수 인자
private int bun;
private int patty;
//선택적 인자
private boolean cheese;
private boolean tomato;
public JavaBeanBurger() {
}
public void setBun(int bun) {
this.bun = bun;
}
public void setPatty(int patty) {
this.patty = patty;
}
public void setCheese(boolean cheese) {
this.cheese = cheese;
}
public void setTomato(boolean tomato) {
this.tomato = tomato;
}
}
@Test
void 자바빈_패턴_테스트(){
JavaBeanBurger javaBeanBurger = new JavaBeanBurger();
javaBeanBurger.setBun(1);
javaBeanBurger.setPatty(2);
javaBeanBurger.setCheese(true);
}
자바 빈 패턴은 점층적 생성자의 패턴의 문제인 가독성과 유지보수성, 유연적 객체 생성을 보완했다. 그러나 이러한 방식은 객체 생성 시점에 모든 값들을 주입하지 않아 일관성 문제와 불변성 문제가 있다.
이러한 기존의 생성 방식의 문제점을 해결한 것이 빌더 패턴이다.
public class Burger {
//필수 인자
private int bun;
private int patty;
//선택적 인자
private boolean cheese;
private boolean tomato;
public Burger(int bun, int patty, boolean cheese, boolean tomato) {
this.bun = bun;
this.patty = patty;
this.cheese = cheese;
this.tomato = tomato;
}
}
public class BurgerBuilder {
// 필수 인자
private int bun;
private int patty;
// 선택적 인자
private boolean cheese;
private boolean tomato;
public BurgerBuilder bun(int bun) {
this.bun = bun;
return this;
}
public BurgerBuilder patty(int patty) {
this.patty = patty;
return this;
}
public BurgerBuilder cheese(boolean cheese) {
this.cheese = cheese;
return this;
}
public BurgerBuilder tomato(boolean tomato) {
this.tomato = tomato;
return this;
}
public Burger build() {
return new Burger(bun, patty, cheese, tomato);
}
}
@Test
void 빌더_패턴_테스트(){
//필수 인자만 전달
Burger burger = new BurgerBuilder()
.bun(1)
.patty(2)
.build();
//필수 인자 + 선택적 인자 전달
Burger burger1 = new BurgerBuilder()
.bun(1)
.patty(2)
.cheese(true)
.tomato(true)
.build();
}
Burger라는 클래스에 대한 객체 생성만을 담당하는 별도의 BurgerBuilder라는 클래스를 만든다.
BurgerBuilder 클래스에는 필드 멤버 구성을 만들고자 Burger 클래스 멤버 구성과 똑같이 구성한다. 그리고 해당 필드에 대한 setter 메서드를 구성한다. 기존의 setter와의 특성을 다르게 하기 위해 set 단어를 빼도 된다.
각 setter 마지막에 있는 return this가 중요한 부분인데 여기서 this는 BurgerBuilder와 같은 빌더 클래스여야 한다. 왜냐하면 빌더 객체 자신을 리턴함으로써 메서드 호출 후 연속적으로 빌더 메서드를 체이닝하여 호출할 수 있게 된다.
마지막에 build라는 메서드를 구성해서 Burger라는 생성하고자 하는 객체를 생성하기 위해 BurgerBuilder의 필드를 생성자 필드에 넣어주어서 객체를 생성한다.
생성자 방식으로 객체를 생성하는 경우는 매개변수가 많아질 수록 각 인자 순서 마다 이값이 어떤 멤버에 해당되는지 파악이 힘들어진다. 빌더 패턴을 사용하면 어떤 인자에 어떤 값이 들어가는지 알 수 있어 가독성이 향상된다.
빌더 패턴을 직접 구현하게 되면 디폴트로 빌더 인자에 값을 초기화 해놓은뒤 객체를 생성하게 되면 따로 인자를 초기화 하지 않는 이상 디폴트로 값을 배정할 수 있게 해준다.
롬복의 @Builder를 사용하게 되면 초기화를 해야 하는 필드를 누락할 수 있는 가능성이 생기게 된다. 이로 인해 개발자의 의도와 다르게 클래스의 기능을 제대로 동작하지 않고 오류가 발생할 수 있다는 단점을 가지고 있다.
빌더 클래스를 통해 초기화가 필수인 필드는 빌더의 생성자로 받게 하여 필수 멤버를 설정해줘서 빌더 객체가 생성되도록 유도하고, 선택적 사항은 빌더 객체의 메서드를 통해서 받으면 필수와 선택사항을 구분할 수 있게 된다.
빌더 패턴은 setter 메서드를 사용하지 않고 빌더를 통한 생성자 함수만을 이용함으로 필드에 대한 변경 가능성을 최소화 시켜주는 장점을 가진다. (완전한 불변성을 지니는 것은 아니지만, 최소한의 불변성을 지키기 위한 노력 정도로 이해하면 될 것 같다)
롬복의 @Builder가 자동 생성해주는 빌더로써 이펙티브 자바에서 소개한 빌더 패턴이다. GoF의 빌더 패턴과는 다른 방식이다. 심플 빌더 패턴은 생성자가 많을 경우 또는 변경 불가능한 불변 객체가 필요한 경우 코드의 가독성과 일관성, 불변성을 유지하는 것에 중점을 둔다.
public class SimpleBuilderBurger {
private int bun;
private int patty;
private boolean cheese;
private boolean tomato;
public static SimpleBuilderBurger.Builder builder() {
return new SimpleBuilderBurger.Builder();
}
private SimpleBuilderBurger(Builder builder) {
this.bun = builder.bun;
this.patty = builder.patty;
this.cheese = builder.cheese;
this.tomato = builder.tomato;
}
public static class Builder {
private int bun;
private int patty;
private boolean cheese;
private boolean tomato;
public Builder bun(int bun) {
this.bun = bun;
return this;
}
public Builder patty(int patty) {
this.patty = patty;
return this;
}
public Builder cheese(boolean cheese) {
this.cheese = cheese;
return this;
}
public Builder tomato(boolean tomato) {
this.tomato = tomato;
return this;
}
public SimpleBuilderBurger build() {
return new SimpleBuilderBurger(this);
}
}
}
@Test
void 심플_빌더_패턴_테스트(){
SimpleBuilderBurger burger = SimpleBuilderBurger.builder()
.bun(1)
.patty(2)
.cheese(true)
.tomato(true)
.build();
}
참고 코드
참고 : 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