규칙 2. 생성자에 매개변수가 많다면 빌더를 고려하라
이 장에서는 선택적 인자가 많을 때 객체 생성하는 방법들의 특징을 살펴보고, 왜 빌더Builder 를 고려해야하는지 얘기한다.
-
점층적 생성자 패턴telescoping constructor pattern
-
자바빈즈 패턴JavaBeans pattern
-
빌더 패턴Builder pattern
점층적 생성자 패턴
기본적으로 필수 인자만 받는 생성자를 정의하고, 선택적 인자를 받는 생성자를 추가하는 방법이다. 객체를 생성할 때 인자 갯수에 맞는 생성자를 골라 호출해야한다.
public class Person {
private final String name; // 필수
private final int age; // 필수
private final String mail;
private final String city;
private final String state;
public Person(String name, int age) {
this(name, age, "");
}
public Person(String name, int age, String mail) {
this(name, age, mail, "");
}
public Person(String name, int age, String mail, String city) {
this(name, age, mail, city, "");
}
public Person(String name, int age, String mail, String city, String state) {
this.name = name;
this.age = age;
this.mail = mail;
this.city = city;
this.state = state;
}
}
-
설정할 필요가 없는 필드에도 인자를 전달해야 해야 한다.
-
인자 수가 늘어날수록 가독성이 떨어진다.
자바빈즈 패턴
인자 없는 생성자로 객체를 할당받고, setter를 통해 나머지 값들을 설정하는 방법이다. 객체 생성도 쉽고 위 방법보다 가독성이 좋다.
public class Person {
private String name;
private int age;
private String mail;
private String city;
private String state;
public Person() {}
public void setName(String name) { this.name = name; }
public void setAge(int age) { this.age = age; }
....
}
Person me = new Person();
me.setName("yeongjun.kim");
me.setAge(27);
-
한번에 객체 생성을 끝낼 수 없으므로, 객체 일관성이 일시작으로 깨질 수 있다.
-
변경 불가능 클래스를 만들 수 없다. 해결하기 위해서 추가 구현할 코드가 많아진다.
-
setter를 추가로 만들어줘야한다. (내생각)
Tipsetter 생성방법보통 IDEA에 getter/setter를 자동으로 추가해주는 기능이 존재한다. 혹은 lombok의
@Setter
어노테이션을 활용할 수 있다.
빌더 패턴
필수 인자들을 생성자(또는 정적 팩터리 메서드)에 전달하여 빌더 객체를 만들고, 선택적 인자들을 추가한 뒤, 마지막에 build()
를 호출하여 Immutable 객체를 만드는 방법이다.
public class Person {
private final String name;
private final int age;
private final String mail;
private final String city;
private final String state;
// 빌더 객체
public static class Builder {
// 필수 인자
private final String name;
private final String age;
// 선택적 인자 - 기본값으로 초기화
private final String mail = "";
private final String city = "";
private final String state = "";
public Builder(String of, int age) {
this.name = name;
this.age = age;
}
public Builder mail(String mail) {
this.mail = mail;
return this;
}
public Builder city(String city) {
this.city = city;
return this;
}
public Builder state(String state) {
this.state = state;
return this;
}
public Person build() {
return new Person(this);
}
}
private Person(Builder builder) {
this.name = name;
this.age = age;
this.mail = mail;
this.city = city;
this.state = state;
}
}
Person me = Person.Builder("yeongjun.kim", 27)
.mail("opid911@gmail.com")
.build();
특징
-
빌더 클래스(Builder)는 빌더가 만드는 객체 클래스(Person)의 정적 맴버 클래스로 정의한다({#item22}[규칙 22]).
public class Person { public static class Builder { ... } }
-
불변식을 적용할 수 있으며,
build()
에서 불변식이 위반되었는지 검사할 수 있다.public class Person { public static class Builder { ... public Person build() { Person result = new Person(this); if(/* result의 값 검사 */) { throw new IllegalStateException(/* 위반 원인 */); } return result; } } }
-
빌더 객체에서 실제 객체로 인자가 복사된 다음에 불변식들을 검사할 수 있다는 것, 그리고 그 불변식을 빌더 객체의 필드가 아니라 실제 객체의 필드를 두고 검사할 수 있다는 것은 중요하다({#item39}[규칙 39]).
-
불변식을 위반한 경우,
build()
는IllegalStateException
을 던져야 한다({#item60}[규칙 60]). -
예외 객체를 살펴보면 어떤 불변식을 위반했는지 알아낼 수도 있어야 한다({#item63}[규칙 63]).
cf. 불변식을 강제하는 방법
-
불변식이 적용될 값 전부를 인자로 받는 setter를 정의하는 방법.
-
setter는 불변식이 만족하지 않으면 *IllegalArgumentException*을 던짐.
-
build()가 호출되기 전에 불변식을 깨뜨리는 인자가 전달되었다는 것을 신속하게 알 수 있는 장점.
public class Person {
...
public static class Builder {
public Builder setNameAndAge(String name, int ate) {
if(name == null) {
throw new IllegalArgumentException();
}
return this;
}
...
public Person build() {
return new Person(this);
}
}
...
}
-
메서드마다 하나씩, 필요한 만큼 varargs 인자를 받을 수 있다.
public class Person {
public static class Builder {
public Builder names(String... names) {
this.names = names;
return this;
}
public Builder foramily(String... names) {
this.farther = names[0];
this.marther = names[1];
return this;
}
}
...
}
-
유연하다. (e.g. 객체가 만들어질 때마다 자동적으로 증가하는 일련번호 같은 것을 채울 수 있다)
-
인자가 설정된 빌더는 훌륭한 [Abstract Factory][dp-abstract-factory]다. JDK1.5 이상을 사용하는 경우, 제네릭 자료형 하나면 어떤 자료형의 객체를 만드는 빌더냐의 관계 없이 모든 빌더에 적용할 수 있다.
public interface Builder<T> {
public T build();
}
public class Person {
public static class Builder implements Builder<Person> {
...
public Person build() {
return new Person(this);
}
}
}
**e.g.** *Code at package `java.util.stream`*
Stream.builder().add(1).add(2).add(3).build();
-
빌더 객체를 인자로 받는 메서드는 보통 *한정적 와일드카드 자료형~bounded wildcard type~*을 통해 인자의 자료형을 제한한다([규칙 28](#items28)).
Tree buildTree(Builder<? extends Node> nodeBuilder) {...}
-
자바가 제공하는 추상적 팩토리로는 Class 객체가 있으며, 이 객체의 newInstance() 가 build 메서드 구실을 한다.
**하지만,** newInstance()는 항상 무인자 생성자를 호출하려 하는데, 문제는 그런 생성자가 없을 수도 있다는 것. TO-DO
문제점
-
빌더 객체를 만드는 오버헤드가 문제가 될 수 있다(성능이 중요한 상황). 그러니 인자 갯수가 통제할 수 없을 정도로 많아지만 빌더 패턴을 적용하자.
요약
빌더 패턴은 인자가 많은 생성자나 정적 팩터리가 필요한 클래스를 설계할 때, 특시 대부분의 인자가 선택적 인자인 상황에 유용하다.
Tip
|
lombok 활용하여 빌더 만들기
|