이펙티브 자바 Item2. 생성자에 매개변수가 많다면 빌더를 고려하라
정적 팩터리와 생성자를 통한 객체 생성은 선택적 매개변수의 수가 많을때 제약이 있다.
1
2
3
4
5
6
7
8
public class NutritionFacts {
private final int servingSize; // 필수
private final int servings; // 필수
private final int calories; // 선택
private final int fat; // 선택
....
}
선택적 매개변수란 위의 NutritionFacts를 생성할때 꼭 필요하지 않은 변수를 말한다. 예를 들어 calories와 fat이 영양 정보를 표시하는데 꼭 필요한 정보가 아니라면, 당장 생성자를 통해 초기화 해주지 않아도 된다. 이런 상황에서 대처할 수 있는 방법들을 알아보자.
대안 1. 점층적 생성자 패턴
필수 매개변수만 받는 생성자부터 모든 매개변수를 다 받는 생성자까지 점층적으로 다 만들어 주는 패턴이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class NutritionFacts {
private final int servingsSize;
private final int servings;
private final int calories;
public NutritionFacts(int servingsSize, int servings)
{
// this(...)는 해당 매개변수를 가진 클래스 자기자신의 생성자를 호출해주는 것이다.
this(servingsSize,servings,0);
}
public NutritionFacts(int servingsSize, int servings, int calories)
{
this.servingsSize = servingsSize;
this.servings = servings;
this.calories = calories;
}
}
점층적 생성자 패턴의 단점
- 선택적인 요소에 0같은 의미 없는 값을 채워넣어줘야 한다. 또한 매개변수의 숫자가 많아지면, 그에 따라 만들어야 하는 생성자의 개수로 기하급수적으로 늘어난다. 즉 클라이언트 코드를 작성하거나 읽기 어려워진다.
- 각 매개변수가 의미하는게 무엇인지 알아차리기 힘들어진다. 인텔리제이의 Ultimate 버전을 사용하면 ctrl + P 를 사용해서 쉽게 각 자리의 매개변수가 무엇을 의미하는지 파악할 수 있지만, 다른 ide를 사용하면 힘들어진다.
- 매개변수의 순서를 바꿔서 전달해도 컴파일러는 파악할 수 없다. 따라서 런타임에서 잘못된 값이 들어갈 수 있다.
1
2
3
4
5
6
7
8
9
10
// 단점1 : 각 매개변수가 의미하는 바가 무엇인지 모호하다.
public ConstructorExample(int a, int b, int c){
}
// 단점2 : 만약 매개변수의 순서를 바꿔서 전달해도 컴파일러는 모른다
public NutritionFacts(int servings, int servingsSize) // 원래 순서는 servingsSize, servings 이다
{
...
}
대안 2. 자바 빈즈 패턴
자바 빈즈 패턴은 매개변수 없는 객체를 생성한 뒤 setter 메서드로 값을 모두 초기화 해주는 것이다.
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
public class NutritionFacts {
private int servingSize = 0;
private int servings = 0;
private int calories = 0;
public NutritionFacts() { }
public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}
public void setServings(int servings) {
this.servings = servings;
}
public void setCalories(int calories) {
this.calories = calories;
}
}
public static void main(String[] args)
{
NutritionFacts nutritionFacts = new NutritionFacts(); // 빈 생성자로 인스턴스 생성
nutritionFacts.setServingSize(20); // 직접 setter를 통해 변수를 초기화해준다.
nutritionFacts.setServings(10);
nutritionFacts.setCalories(20);
}
자바 빈즈 패턴을 사용하면, 점층적 생성자 패턴의 단점으로 지적되었던 부분들을 해결할 수 있다. 멤버 변수의 이름이 들어간 setter 메서드를 통해 매개변수의 의미를 명확하게 파악할 수 있기 때문이다. 하지만 자바 빈즈 패턴에는 큰 단점이 있다.
- 단점 1. 멤버 변수를 초기화 하기 위해서 setter 메서드가 너무 많이 필요하다.
- 단점 2. 빈 생성자를 통해 객체를 생성한 후, setter를 통해 초기화 하기 때문에 불변 객체로 만들 수 없다.
불변 vs 불변식
불변 객체(Immutable class)는 내부 값을 변경할 수 없는 객체를 말한다. 대표적으로 String이 불변 객체에 속한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ImmutableClass{
// 변수들은 final 키워드로 한 번 초기화 이후 값이 안바뀌게 해야 한다.
private final int value1;
private final int value2;
// 생성자 내부에서 모든 값을 다 초기화 해줘야 한다.
public ImmutableClass(int value1, int value2)
{
this.value1 = value1;
this.value2 = value2;
}
// setter는 사용 금지
// 사용하려면 private으로 외부에서 접근 못하게 만들어야함
...
}
불변식(Invariant)은 프로그램이 실행되는 동안 지켜야 하는 규칙을 말한다. 가변 객체에 불변식이 적용되면 조건을 지키는 한에서 변수값을 변경할 수 있다. 불변식이 극단으로 간게 불변이다. 아래가 불변식의 예이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class InvariantClass{
private int value;
public InvariantClass(int value)
{
this.value = value;
}
public minusValue(int n)
{
// 불변식 부분
if(value < 0)
{
throw new IllegalStateException("value 값이 0보다 작으면 안된다.");
}
value = value - n;
}
}
대안 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
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
public static class Builder {
// 필수 매개변수
private final int servingSize;
private final int servings;
// 선택 매개변수 - 기본값으로 초기화한다.
private int calories = 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 NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
}
}
필수 값을 미리 초기화 하고 갈 수 있기 때문에 안전성이 보장되고 final 키워드를 통해 불변성이 보장된다. setter 메서드와 유사한 메서드 체이닝을 통해 간결성도 보장할 수 있다. 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기 좋다.
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
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));
// 여기서 this를 사용하면 Pizza.Builder가 반환된다.
// 우리가 원하는 것은 하위 타입의 빌더를 반환하는 것이기 때문에
// self()를 사용하서 하위 클래스로 위임한다.
return self();
}
abstract Pizza build();
// 하위 클래스는 이 메서드를 재정의(overriding)하여
// "this"를 반환하도록 해야 한다.
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
<T extends Builder
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
public class NyPizza extends Pizza {
public enum Size { SMALL, MEDIUM, LARGE }
private final Size size;
/*
Pizza.Builder<NyPizza.Builder> 부분을 보면 Pizza 클래스의
Builder<T extends Builder<T>> 부분을 충족한 것을 알 수 있다.
*/
public static class Builder extends Pizza.Builder<NyPizza.Builder> {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override public NyPizza build() {
return new NyPizza(this);
}
// 하위 클래스에서 this를 반환하게 함으로써 하위 클래스의 빌더 타입으로
// 메서드 체이닝을 할 수 있게 되었다.
@Override protected Builder self() { return this; }
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
@Override public String toString() {
return toppings + "로 토핑한 뉴욕 피자";
}
}
공변 반환 타이핑(covariant return typing)
1
2
3
4
5
6
7
8
9
// Pizza.class
abstract Pizza build();
// NyPizza.class
@Override
public NyPizza build()
{
return new Pizza(this);
}
Pizza라는 상위 클래스의 build()의 반환타입은 Pizza이지만, 오버라이딩한 NyPizza의 build()의 반환 타입은 NyPizza인 것을 알 수 있다. 이처럼 오버라이딩한 메서드에서 상위 클래스의 반환 타입말고 그 하위 타입을 반환하는 기능을 공변 반환 타이핑이라고 한다. 명시적인 형 변환 없이 빌더를 사용할 수 있다!