Post

이펙티브 자바 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;
}
}

점층적 생성자 패턴의 단점

  1. 선택적인 요소에 0같은 의미 없는 값을 채워넣어줘야 한다. 또한 매개변수의 숫자가 많아지면, 그에 따라 만들어야 하는 생성자의 개수로 기하급수적으로 늘어난다. 즉 클라이언트 코드를 작성하거나 읽기 어려워진다.
  2. 각 매개변수가 의미하는게 무엇인지 알아차리기 힘들어진다. 인텔리제이의 Ultimate 버전을 사용하면 ctrl + P 를 사용해서 쉽게 각 자리의 매개변수가 무엇을 의미하는지 파악할 수 있지만, 다른 ide를 사용하면 힘들어진다.
  3. 매개변수의 순서를 바꿔서 전달해도 컴파일러는 파악할 수 없다. 따라서 런타임에서 잘못된 값이 들어갈 수 있다.
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> 부분이 이해하기 어려울 수 있다. 해당 부분의 의미는 Pizza 클래스의 Builder 내부 클래스를 상속하는 모든 타입이 들어올 수 있다는 것이다. 즉 Pizza.Builder를 상속하는 아래 코드의 NyPizza.Builder가 T 타입으로 들어올 수 있는 것이다.

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인 것을 알 수 있다. 이처럼 오버라이딩한 메서드에서 상위 클래스의 반환 타입말고 그 하위 타입을 반환하는 기능을 공변 반환 타이핑이라고 한다. 명시적인 형 변환 없이 빌더를 사용할 수 있다!

This post is licensed under CC BY 4.0 by the author.