Item 2 - 생성자에 매개변수가 많다면 빌더를 고려하라
Item2 - 생성자에 매개변수가 많다면 빌더를 고려하라
정적 팩터리 메서드와 생성자는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다. 🤦♀️
이러한 제약의 대안으로 프로그래머들은 다음과 같은 방법을 사용했다.
- 점층적 생성자 패턴
- 자바 빈즈 패턴
- 빌더 패턴
(1) 점층적 생성자 패턴 (확장의 어려움)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class NutritionFacts {
private final int servingSize; // (mL, 1회 제공량) 필수
private final int servings; // (회, 총 n회 제공량) 필수
private final int calories; // (1회 제공량당) 선택
private final int fat; // (g/1회 제공량) 선택
private final int sodium; // (mg/1회 제공량) 선택
private final int carbohydrate; // (g/1회 제공량) 선택
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
public static void main(String[] args) {
NutritionFacts cocaCola =
new NutritionFacts(240, 8, 100, 0, 35, 27);
}
}
- 필수 매개변수만 받는 생성자 / 필수 매개변수 + 선택 매개변수 1개를 받는 생성자 / 필수 매개변수 + 선택 매개변수를 2개 받는 생성자 / … 형태로 선택 매개변수를 전부 다 받는 생성자까지 점층적으로 늘려가는 방식
단점 👎
- 매개변수의 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.
- 클라이언트가 실수로 매개변수의 순서를 바꿔 건네도 컴파일러는 알아채지 못하고, 결국 런타임에 엉뚱한 동작을 하게 된다. → 찾기 어려운 버그 유발!
- 확장이 어렵다.
(2) 자바 빈즈 패턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class NutritionFacts {
private int servingSize = -1; // 필수; 기본값 없음
private int servings = -1; // 필수; 기본값 없음
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public NutritionFacts() { }
// setter 메서드들
public void setServingSize(int val) { servingSize = val; }
public void setServings(int val) { servings = val; }
public void setCalories(int val) { calories = val; }
public void setFat(int val) { fat = val; }
public void setSodium(int val) { sodium = val; }
public void setCarbohydrate(int val) { carbohydrate = val; }
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
}
}
- 매개변수가 없는 생성자로 객체를 만든 후에 setter 메서드로 값을 설정한다.
- 점층적 생성자 패턴에 비해 인스턴스를 만들기 쉽고, 가독성이 좋아졌다.
단점 👎
- 하나의 객체를 만들기 위해 여러 개의 메서드가 호출된다.
- 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.
- 일관성이 무너지면서 클래스를 불변으로 만들 수 없게 된다.
일관성이 무너지면 런타임 시 디버깅에 어려움을 겪을 수 있고 🤦♀️, 불변 클래스로 만들 수 없게 되면서 Thread safe하지 않게 된다.
(3) 빌더 패턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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 Builder fat(int val)
{ fat = val; return this; }
public Builder sodium(int val)
{ sodium = val; return this; }
public Builder carbohydrate(int val)
{ carbohydrate = val; return this; }
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
- 점층적 생성자 패턴의 안정성과 자바 빈즈 패턴의 가독성을 겸비한 것이 빌더 패턴이다.
- 필수 매개 변수만으로 생성자를 호출하여 빌더 객체를 얻은 후에 빌더 객체가 제공하는 setter 메서드들로 원하는 매개변수들을 설정한다. 이후 매개변수가 없는 build 메서드를 호출해 필요한 (보통은 불변인) 객체를 얻는다.
- 빌더의 setter 메서드는 빌더를 반환하기 때문에 연쇄적 호출이 가능하다. (fluent API)
1
2
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
필수 매개변수를 넣고, 원하는 매개변수를 선택적으로 넣어준 후 bulid()를 통해 객체를 얻고 있다.
이 코드는 읽기 쉽게 이루어져 명확한 정보 전달을 하고 있다.
빌더 패턴의 활용방법
- 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.
- 각 계층의 클래스에 관련 빌더를 멤버로 정의하고 추상 클래스는 추상 빌더, 구체 클래스는 구체 빌더를 가진다.
- 추상 클래스(Pizza)의 Builder 클래스는 재귀적 타입 한정을 이용하는 제네릭 타입으로, 하위 클래스에서는 형변환하지 않고도 메서드 연쇄가 가능하다.
- 각 하위 클래스의 빌더가 정의한 build()는 해당 구체 하위 클래스를 반환한다. (공변 반환 타이핑)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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();
// 하위 클래스는 이 메서드를 재정의(overriding)하여
// this를 반환하도록 해야 한다.
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 공변 반환 타이핑
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;
}
}
1
2
3
NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SUASAGE).addTopping(ONION)
.build();
핵심정리 💡
- 생성자나 정적 팩터리가 처리해야할 매개변수가 많다면 빌더 패턴을 선택하는게 낫다.
- 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 낫다.
참고 🕶️
자바빈 스펙
https://download.oracle.com/otndocs/jcp/7224-javabeans-1.01-fr-spec-oth-JSpec
RuntimeException
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/RuntimeException.html
https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html
This post is licensed under CC BY 4.0 by the author.