빌더 패턴
은 복잡한 객체의 생성을 정의하는 클래스와 표현 방법을 정의하는 클래스를 분리함으로써 동일한 생성에 대해서 서로 다른 표현을 만드는 방법을 제공하는 패턴입니다.
빌더 패턴은 주로 객체가 Optional한 속성(필드)를 가질 때 사용하게 됩니다.
다음과 같이 Optional한 속성의 갯수가 늘어나면 속성의 타입이나 개수를 관리하기가 어려워지는 문제가 발생하게 됩니다.
다음은 Optional한 속성이 있는 객체의 생성자 오버로딩을 통해 각기 다른 객체를 만드는 방식입니다.
public class Sandwich {
//각 속성(필드)는 토핑의 개수를 의미
private int tomato;
private int lettuce;
private int cheese;
private int cucumber;
private int ham;
//모든 재료가 들어간 샌드위치 객체 생성자
public Sandwich(int tomato, int lettuce, int cheese, int cucumber, int ham) {
//초기화 구문
}
//토마토가 빠진 샌드위치 객체 생성자
public Sandwitch(int lettuce, int cheese, int cucumber, int ham) {
//초기화 구문
}
//토마토, 오이가 빠진 샌드위치 객체 생성자
public Sandwich(int lettuce, int cheese, int ham) {
//초기화 구문
}
//햄이 빠진 샌드위치 객체 생성자
public Sandwitch(int lettuce, int cheese, int cucumber) {
//초기화 구문
}
}
//각 샌드위치 생성
//모든 재료가 들어간 샌드위치
Sandwich sandwich1 = new Sandwich(1, 2, 1, 1, 3);
//토마토가 빠진 샌드위치
Sandwich sandwich2 = new Sandwich(3, 2, 2, 4);
//토마토, 오이가 빠진 샌드위치
Sandwich sandwich3 = new Sandwich(3, 3, 1);
//햄이 빠진 샌드위치
Sandwich sandwich4 = new Sandwich(1, 1, 3, 2);
//햄만 있는 샌드위치
Sandwich sandwich5 = new Sandwich(0, 0, 0, 0, 1);
각 샌드위치를 생성하는 구문을 봤을때 어떤 속성이 어떤 값을 갖는지 쉽게 파악이 되시나요? 아마 그렇지 않을 것 입니다.
심지어 마지막 햄만 있는 샌드위치는 생성자의 매개변수를 함부로 생략할 수 없기 때문에 원하는 생성자가 없는 경우 0을 일일이 넣어주어야하는 문제도 발생하고 있음을 볼 수 있습니다.
위와 같은 방법을 점층적 생성자 패턴
이라고 하는 메소드 오버로드 방식이라고 부르는데요. 이 방식은 코드가 길어지면서 유지보수나 가독성이 크게 떨어진다는 문제가 발생하게 됩니다.
점층적 생성자 패턴의 문제를 해결하기 위해 등장한 패턴이 하나 있는데요. 바로 자바 빈 패턴
입니다. 자바 빈 패턴
은 매개변수가 없는 생성자를 가진 객체를 만들고 속성의 초기화를 setter
를 이용해서 초기화하는 방식입니다.
public class Sandwich {
private int tomato;
private int lettuce;
private int cheese;
private int cucumber;
private int ham;
public Sandwich() {}
public void setTomato(int tomato) {
this.tomato = toamto;
}
(...)
//setter를 필드 수 만큼 추가
}
//모든 재료가 들어간 샌드위치
Sandwich sandwich1 = new Sandwich();
sandwich.setTomato(1);
sandwich.setLettuce(3);
sandwich.setCheese(2);
sandwich.setCucumber(1);
sandwich.setHam(4);
//치즈와 오이가 빠진 샌드위치
Sandwich sandwich2 = new Sandwich();
sandwich.setTomato(1);
sandwich.setLettuce(3);
sandwich.setHam(4);
setter를 이용하니 가독성이 좋아진 것이 느껴집니다. 그리고 필요한 필드의 setter만 호출하게 됨으로써 코드도 간결해졌습니다.
하지만 이 방식도 두 가지 문제를 가지고 있습니다.
불변성: 객체 생성 이후 setter를 호출하기 때문에 외부에서 마음대로 setter에 접근하여 값을 조작할 수 있는 문제가 발생한다. 이는 객체의 불변성을 해치기 때문에 자바 빈 패턴을 지양해야하는 주요한 이유가 되기도 합니다.
일관성: 위 예제에선 없었지만 필수 매개변수가 있는 경우, 필수 매개변수의 setter를 호출하지 않아 누락되는 경우 객체의 일관성이 사라지게 됩니다. 따라서 일관성이 사라진 객체를 잘못 호출하게 된다면 에러를 발생시킬 가능성이 있습니다.
점층적 생성자 패턴, 자바 빈 패턴
의 문제를 해결하고자 빌더 패턴
이 등장하게 되었습니다.
빌더 패턴
의 동작을 살펴보면 객체 생성을 위한 별도의 Builder 클래스를 하나 만들고 속성을 하나하나 받습니다. 그리고 build()
라는 이름의 메소드를 호출해서 객체 인스턴스를 만들고 반환하도록 되어있습니다.
다음과 같은 Member 클래스를 빌더 패턴
을 이용해서 인스턴스화해보겠습니다.
public class Member {
private String name;
private int age;
private String address;
public Member(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
@Override
public String toString() {
return "Member [name=" + name + ", age=" + age + ", address=" + address + "]";
}
}
위 클래스를 인스턴스화 하기 위한 빌더 패턴 코드는 다음과 같습니다.
public class MemberBuilder {
private String name;
private int age;
private String address;
public MemberBuilder name(String name) {
this.name = name;
return this;
}
public MemberBuilder age(int age) {
this.age = age;
return this;
}
public MemberBuilder address(String address) {
this.address = address;
return this;
}
public Member build() {
return new Member(name, age, address);
}
}
각 필드 명과 일치하는 이름의 setter 메소드를 통해 필드 값을 받습니다. 그리고 최종적으로 build()
메소드에서 Member의 생성자를 호출해서 객체를 생성하고 반환하게 됩니다.
필드를 받는 setter 메소드의 이름이
setXxxxx()
형태가 아닌 이유는 일반적인 setter와 구분하기 위해서 일반적으로 필드명을 그대로 메소드 명으로 사용한다고 합니다.
각 setter의 반환이 return this;
형태로 되어있는데요. 여기서 반환하는 this
는 자기 자신인 MemberBuilder
를 가리키고 있습니다. 자기 자신을 반환함으로써 setter를 메소드 체인의 형식(ex) name().age().address().build()
)으로 호출할 수 있게 됩니다.
build()
에서는 Member의 생성자를 호출하면서 setter로 설정된 builder 클래스의 필드 값을 파라미터로 전달하게 됩니다. 이렇게 필드의 값이 채워진 Member 인스턴스가 생성되게 됩니다.
public static void main(String[] args) {
Member member = new MemberBuilder()
.name("스티브")
.age(11)
.address("서울특별시")
.build();
System.out.println(member.toString());
}
위 예제는 모든 매개변수가 필수인 상황이었는데요. 추가적으로 required 매개변수와 optional 매개변수가 있는 상황에서의 빌더 패턴도 소개드리려고 합니다.
동일한 Member
에서 address
만 Optional 필드라고 가정하고 진행해보겠습니다.
public class Member {
//필수 필드 name, age
private String name;
private int age;
//선택적 필드 address
private String address;
public Member(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
@Override
public String toString() {
return "Member [name=" + name + ", age=" + age + ", address=" + address + "]";
}
}
빌더 패턴
에서 필수, 선택 필드를 구분하는 방법은 다음과 같습니다.
필수 필드는 Builder 생성자를 통해 초기화하도록 만들고, 선택 필드는 setter를 이용해서 추가적으로 값을 받게 만들어줍니다.
public class MemberBuilder {
//필수 필드 name, age
private String name;
private int age;
//선택적 필드 address
private String address;
//필수 필드는 생성자를 통해 초기화
public MemberBuilder(String name, int age) {
this.name = name;
this.age = age;
}
//선택 필드는 setter를 통해 초기화
public MemberBuilder address(String address) {
this.address = address;
return this;
}
public Member build() {
return new Member(name, age, address);
}
}
public static void main(String[] args) {
Member member1 = new MemberBuilder("스티브", 11)
.address("서울특별시") //선택 필드 address
.build();
Member member2 = new MemberBuilder("앨리스", 14).build();
System.out.println(member1.toString());
System.out.println(member2.toString());
}