(Effective Java 2/E) 102. Item 2 - Consider a builder when faced with many constructor parameters

Consider a builder when faced with many constructor

  • 2판 제목 : 생성자 인자가 많을 때는 Builder 패턴 적용을 고려하라.
  • 3판 제목 : 생성자에 매개변수가 많다면 빌더를 고려하라.

정적 팩토리 메서드나 생성자는 선택적인 인자가 많은 경우 대응이 힘들다는 문제점을 가지고 있다.

이를 어떻게 해결할 수 있을까?

1. 점층적 생성자 패턴(Telescoping Constructor Pattern)

모든 경우의 수를 기계적으로 대응한다면 점층적 생성자 패턴(Telescoping Constructor Pattern) 을 사용하면 된다.

아래 예제를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TelescopingConstructor {
private int a;
private int b;
private int c;

public TelescopingConstructor(int a) {
this(a, 0, 0);
}

public TelescopingConstructor(int a, int b) {
this(a, b, 0);
}

public TelescopingConstructor(int a, int b, int c) {
this.a = a;
this.b = b;
this.c = c;
}
}

클래스의 멤버변수가 3개일때에도 모든 경우의 수를 대응하기 위해 생성자가 3개나 필요하다.

사실 이 또한 멤버변수가 모두 int 타입이기에 이정도이지, 다양한 타입이 섞여있다면 더욱 늘어난다.

모든 멤버변수가 필수적으로 초기화되느냐 안되느냐에 따라서도 생성자는 늘어난다.

이러한 과정을 TelescopingConstructor 클래스의 디자인이 변경될 때마다 반복해야한다.

개발자가 실수할 가능성도 증가하는 것은 불 보듯 뻔한 일이다.

요약하자면 점층적 생성자 패턴은 잘 동작하지만 인자 수가 늘어나면 클라이언트 코드를 작성하기가 어려워지고, 가독성이 떨어지게 된다.

조금 더 효율적 방법은 없을까?

2. 자바빈 패턴(JavaBeans Pattern)

점층적 생성자 패턴 보다 효율적인 패턴으로 자바빈 패턴이 존재한다.

자바빈 패턴은 먼저 인자없이 생성자를 호출하여 객체를 만든 뒤, 이후 setter 들을 호출하여 필요한 멤버변수의 값을 초기화하는 방식이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class JavaBeans {
private int a;
private int b;
private int c;

public JavaBeans() {
// Do Nothigns
}

public setA(int a) { this.a = a; }
public setB(int b) { this.b = b; }
public setC(int c) { this.c = c; }
}

이제 괜찮아졌을까?

자바빈 패턴은 객체 지향적 측면에서 두 개, 인지적 관점에서 하나씩 단점이 존재한다.

먼저 생성자 하나만 호출하여 객체의 생성이 끝나지않으므로 객체의 일관성이 깨질 수 있다.

두 번째로 자바빈 패턴은 불변 클래스를 생성할 수 없기에 thread-safe를 위해 별도의 작업이 추가적으로 필요하다.

마지막으로 인지적 관점에서 꼭 필요한 멤버변수의 초기화를 깜빡하기 쉽다는 것이다.

거기에 멤버변수의 증가에 따라 실수할 확률도 선형적으로 증가한다.

3. 빌더 패턴(Builder Pattern)

이제 점층적 생성자 패턴의 안정성과 자바빈 패턴의 가독성을 결합한 빌더 패턴에 대해 알아보자.

객체를 직접적으로 생성하는 대신 필수 파라미터들을 별도의 생성자에 전달하여 빌더 객체를 만들어낸다.

이 후 빌더 객체에 정의된 setter 메서드들을 모두 호출한 후, build 함수를 호출하여 불변 객체를 만들어낸다.

아래 예제를 보자.

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 Target {
private int a;
private int b;
private int c;

private Target(Builder builder) {
this.a = builder.getA();
this.b = builder.getB();
this.c = builder.getC();
}

public static class Builder {
private int a;
private int b;
private int c;

public Builder(int a) {
this.a = a;
}

public Builder setB(int b) {
this.b = b;
}
public int getB() {
return b;
}

public Builder setC(int c) {
this.c = c;
}
public int getC() {
return c;
}

public Target build() {
return new Target(this);
}
}
}

실제로 객체를 생성할 땐 아래와 같이 호출한다.

1
2
3
4
Target target = new Target.Builder(1)
.setB(2)
.setC(3)
.build();

빌더 클래스의 setter는 빌더 객체 자신을 반환하므로 예시처럼 setter들을 연달아서 사용할 수 있다.

빌더 패턴은 작성하기도 쉽고, 가독성도 뛰어나다.

하지만 이런 빌더 패턴도 단점은 존재한다.

객체를 생성하기 위해 먼저 빌더 객체를 생성해야하는데, 성능이 매우 중요시 되는 환경이라면 이는 명백한 단점이다.

또한 패턴의 구현을 위해 점층적 생성자 패턴보다도 많은 코드를 필요로 하기도 한다.