[Effective Java] Item 2. 생성자 대신 정적 팩터리 메서드를 고려하라
생성자에 선택적인 매개변수가 많을 때, 보통 어떻게 생성자를 작성하시나요?
대부분 매개변수의 개수를 다르게 하여 여러 개의 생성자를 사용하고 계실겁니다.
class User {
String name;
String email;
String password;
String address;
int age;
boolean sex;
public User(String name, String email, String password) {
this.name = name;
this.email = email;
this.password = password;
}
public User(String name, String email, String password, String address) {
this.name = name;
this.email = email;
this.password = password;
this.address = address;
}
public User(String name, String email, String password, String address, int age) {
this.name = name;
this.email = email;
this.password = password;
this.address = address;
this.age = age;
}
public User(String name, String email, String password, int age) {
this.name = name;
this.email = email;
this.password = password;
this.age = age;
}
}
점층적 생성자 패턴(Telescoping Constructor Pattern)
이렇게 필수 매개변수만 받는 생성자, 필수와 선택 매개변수 1개를 받는 생성자, 2개를 받는 생성자, … 형태로 선택 매개변수를 전부 받는 생성자까지 늘려가는 방식을 점층적 생성자 패턴이라고 부릅니다.
이 패턴은 매개변수의 개수가 많아지면 생성자의 개수도 많아져 클라이언트 코드를 작성하거나 읽기 어렵다는 단점이 있습니다.
선택 매개변수가 많을 때 점층적 생성자 패턴 대신 활용할 수 있는 두번째 대안으로 자바빈즈 패턴이 있습니다.
자바빈즈 패턴(JavaBeans Pattern)
매개변수가 없는 생성자로 객체를 만든 후, setter 메서드를 호출해 원하는 매개변수의 값을 설정하는 방식입니다.
class User {
String name;
String email;
String password;
String address;
int age;
boolean sex;
public User(String name, String email, String password) {
this.name = name;
this.email = email;
this.password = password;
}
public setAddress(String address) {
this.address = address;
}
public setAddress(int age) {
this.age = age;
}
public setSex(boolean sex) {
this.sex = sex;
}
}
public static void main(String[] args) {
User user = new User("유저1", "abcd@gmail.com", "password");
user.setAddress("대한민국 서울특별시");
user.setAge(30);
}
점층적 생성자 패턴보다는 읽기 쉽지만 객체 하나를 만들기 위해 여러 개의 메소드를 호출해야 합니다. 객체가 완벽하게 생성되기 전까지 일관성이 무너진 상태에 놓입니다. 즉, 클래스는 불변적이지 않기 때문에 스레드에서 안전성을 얻기 위해 추가 작업을 해주어야 합니다.
마지막으로 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 빌더 패턴이 있습니다.
빌더 패턴(Builder Pattern)
필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻습니다. 그 다음 빌더 객체가 제공하는 일종의 setter 메서드로 원하는 선택 매개변수들을 설정합니다. 마지막으로 매개변수가 없는 build() 메소드를 호출해 필요한 객체를 얻을 수 있습니다.
// 롬복에서는 @Builder 하나만으로 사용 가능
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// 필수 매개변수
private final int servingSize;
private final int servings;
// 선택 매개변수
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
calories = val;
return this;
}
...
public NutritionFacts build() {
return new NutrientFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
...
}
}
빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기 좋습니다.
각 계층의 클래스에 관련 빌더를 멤버로 정의한 다음 추상 클래스는 추상 빌더를, 구체 클래스는 구체 빌더를 갖게 합니다.
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
// 하위 클래스는 이 메서드를 재정의해 'this'를 반환하도록 한다.
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
public class NyPizza extends Pizza {
public enum Size { SMALL, MEDIUM, LARGE }
private final Size size;
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override
public NyPizza build() {
return new NyPizza(this);
}
@Override
protected Builder self() { return this; }
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
}
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false; //기본값
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override
public Calzone build() {
return new Calzone(this);
}
@Override
protected Builder self() { return this; }
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
}
public static void main(String[] args) {
NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM).sauceInside().build();
}
생성자와 달리 빌더를 이용하면 가변인수(varargs) 매개변수를 여러 개 사용할 수 있습니다.
또는 Pizza 클래스의 addTopping() 메서드처럼 메서드를 여러 번 호출하도록 하고 각 호출 때 넘겨진 매개변수들을 하나의 필드로 모을 수도 있습니다.
빌더 패턴의 장점
- 유연하다.
빌더 패턴의 단점
- 객체를 만들기 전에 빌더부터 만들어야 하기 때문에 아주 약간 성능이 감소한다.
- 코드가 장황하기 때문에 선택 매개변수가 4개 이상이어야 값어치를 한다.