티스토리 뷰
https://ch4njun.tistory.com/233
위에서 정적 팩토리 메서드에 대해서 소개했는데 이러한 정적 패고리 메서드와 public 생성자에는 공통적인 애로사항이 있다. 생성자에 선택적 매개변수가 많아진다면 그에 대응하기 어렵다는 점이다.
선택적 매개변수란 특정 인스턴스 변수를 반드시 초기화하지 않아도 되는 것들에 대한 매개변수를 말한다. 예를 들어 특정 클래스에 A, B, C 인스턴스 변수가 있다고 가정했을 때 A 인스턴스 변수는 반드시 생성자를 통해 초기화 해줘야 하지만 B, C는 반드시 초기화하지 않아도 된다고 했을 때 아래와 같은 생성자들을 만들어줘야 한다.
public class Example {
private String A;
private String B;
private String C;
public example(String A) {
this(A, "");
}
public example(String A, String B) {
this(A, B, "");
}
public example(String A, String B, String C) {
this.A = A;
this.B = B;
this.C = C;
}
}
위 코드에서 볼 수 있듯이 선택적 매개변수가 많아지게되면 만들어줘야할 생성자의 수가 많아질 수밖에 없다. 위 코드와같이 this 키워드를 통해 점층적 생성자 패턴을 사용할 수 있겠지만 매개변수의 수가 늘어나면 해당 클래스를 사용하는 클라이언트 코드를 작성하거나 읽기 어려워질 수 있다.
이러한 문제를 해결하기 위한 방법에 대해서 살펴보자.
빌더 패턴
빌더 패턴은 점층적 생성자 패턴의 안정성과 자바빈즈 패턴의 가독성을 겸비한 패턴으로 위 문제를 해결하기 위해 사용될 수 있다. 빌더 패턴에 대해서는 디자인 패턴 포스팅에서 더 자세히 다루기로 한다.
그러면 빌더패턴이 구성되는 예시를 살펴보자.
public class Example {
private String A;
private String B;
private String C;
public static class Builder {
private final String A; // 필수
// 선택
private String B = "";
private String C = "";
public Builder(String A) {
this.A = A;
}
public Builder setB(String B) {
this.B = B;
return this;
}
public Builder setC(String C) {
this.C = C;
return this;
}
public Example build() {
return new Example(this);
}
}
private Example(Builder builder) {
this.A = builder.A;
this.B = builder.B;
this.C = builder.C;
}
}
선택적 매개변수를 초기화하기 위한 메서드들은 Builder 객체를 반환하기 때문에 아래 코드와 같이 메서드 체이닝을 통해 객체를 생성할 수 있다.
또한 Example 클래스의 생성자를 private 접근제어자로 함으로써 클라이언트가 Example 클래스의 생성자를 직접 호출할 수 없고 반드시 Builder를 사용해 객체를 생성하도록 강제할 수 있다.
Example example = Example.Builder("A")
.setB("B")
.build();
Example example2 = Example.Builder("B")
.setB("B")
.setC("C")
.build();
위 예제코드에서 볼 수 있듯이 빌더 패턴의 가장 큰 장점은 클라이언트가 유연하게 객체를 생성할 수 있도록 해준다는 점이다.
이러한 빌더 패턴은 계층적으로 설계된 클래스에서 사용하면 좋다. 즉, 하나의 클래스에 자식 클래스가 존재할 때 사용하기 좋다는 말이다. 아래 예제를 통해 무슨 말인지 자세히 살펴보자.
public abstract class Pizza {
public enum Topping = { HAM, MUSHROOM, ONION, PEPPER, SSAUSAGE };
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.requiredNonNull(topping);
return self();
}
abstract Pizza build();
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.requiredNonNull(size);
}
@Override
public Pizza Build() {
return new NyPizza(this);
}
@Override
protected Builder self() {
return this;
}
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
}
Pizza.Builder 클래스는 재귀적 타입 한정을 이요하는 제네릭 타입을 가진다. 여기서 재귀적 타입 한정이란 이 Pizza.Builder 클래스를 상속받은 Builder 클래스를 제네릭 타입으로 받는단 말이다. 여기에 추상메서드인 self를 더해 하위 클래스에서는 형변환을 하지 않고도 메서드 체이닝을 지원할 수 있다.
(이러한 우회 방법을 Simulated self-type 이라고 한다.)
이러한 계층적 빌더를 사용하면 아래 코드를 사용해 유연하게 객체를 생성할 수 있다.
NyPizza pizza = NyPizza.Builder(SMALL)
.addTopping(SAUSAGE)
.addTopping(ONION)
.build();
핵심 정리
빌더 패턴은 생성자나 정적 팩토리 메서드가 처리해야할 매개변수가 많다면 선택하기 좋다. 즉, 선택적 매개변수가 많아질수록 구현해야할 생성자 수가 늘어나는데 이러한 구성은 클라이언트 코드를 작성할 때 불편함을 가져올 수 있다.
이러한 상황에서 빌더 패턴을 사용함으로써 클라이언트가 유연하게 객체를 생성할 수 있도록 하는 것이 좋다.
또한 빌더 패턴을 사용하면 생성자에서 구현할 수 없었던 "여러 개의 가변인수"를 구현할 수 있다.
'개발 > Effective JAVA' 카테고리의 다른 글
[아이템9] try-finally보다는 try-with-resources를 사용하라 (0) | 2021.06.22 |
---|---|
[아이템7] 다 쓴 객체 참조를 해제하라 (0) | 2021.06.22 |
[아이템6] 불필요한 객체 생성을 피하라 (0) | 2021.06.20 |
[아이템4] 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2021.06.16 |
[아이템1] 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2021.06.15 |