ITEM 2 "생성자에 매개변수가 많다면 빌더를 고려하라"
이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 2편 해설 영상을 보는 것을 추천한다.
열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.
이번 장에서는 생성자에 매개변수가 많다면 빌더 사용을 권장하고 있다.
public class Pizza {
private final String name;
private final String size;
private final String[] toppings;
// 이름만 가지는 생성자
public Pizza(String name) {
this.name = name;
}
// 이름과 크기를 가지는 생성자
public Pizza(String name, String size) {
this.name = name;
this.size = size;
}
// 모든 필드를 가지고 있는 생성자
public Pizza(String name, String size, String[] toppings) {
this.name = name;
this.size = size;
this.topings = toppings;
}
}
여기 피자에 대한 간략한 정보를 가지는 클래스가 있다.
피자라는 객체를 만드려고 할 때, 이름 매개변수만 필요한 경우가 있을 수 있고, 이름과 크기를 모두 가지는 경우, 필드 모두가 필요한 경우가 있을 수 있다. 그 때마다 보통 위 코드와 같이 점층적으로 생성자를 만들곤 한다.
그런데 이런 점층적 생성자 패턴은 아래와 같은 단점을 가지고 있다.
1 . 매개변수 개수가 많아지면 클라이언트 코드 부분에서 작성하다가 실수할 수도 있고 읽기도 어렵다.
Pizza("치즈", "L", "오이, 불고기"); 이렇게 작성했을 때 각각이 가지고 있는 필드가 어떤 의미를 나타내는지 정확히 알 수 없다.
2 . 사용자가 설정하길 원하지 않는 매개변수도 강제로 포함해서 값을 지정해주어야 한다.
예를 들어 사이즈는 상관없는 피자를 만들고 싶은데 강제로 사이즈 값을 넣어주어야 한다. 선택적인 매개변수가 중간에 위치해 있는 경우, 필수 매개변수가 되어버린다.
그렇다면 어떻게 해야 필수 매개 변수와 선택적인 매개 변수를 적재적소에 넣어 셋팅할 수 있을까?
대안 1 자바 빈즈 패턴
매개변수가 없는 생성자로 객체를 만든 후, Setter 메서드를 호출해서 원하는 매개변수의 값을 설정하는 방식이다.
Pizza myPizza = new Pizza();
myPizza.setName("컴비네이션");
myPizza.setToppings("치즈, 올리브");
이렇게 지정하면 중간에 원하지 않은 매개변수는 피해서 셋팅할 수 있고 읽기 쉬운 것처럼 보인다.
하지만, 객체 하나를 만드려고 메서드를 여러 개 호출해야 되고 객체가 완성이 되기 전까지는 일관성이 무너진다. 중간에 다른 코드에서 setter 함수가 불릴 수 있으며 불변 객체로 만들 수 없다. 또 이런 코드는 Thread-Safe 하지 않다.
이런 문제점을 해결하고자 자바스크립트 같은 언어에서는 freeze 라는 메서드를 지원하고 있지만, 모든 언어 체계에서 지원하는 것은 아니다. 설령 이 방법을 쓴다고 하더라도 프로그래머가 이 객체에 freeze 하는 구간이 어디 있는지 소스코드를 보며 찾아야 한다는 단점이 있다.
대안 2 빌더 패턴
필수 매개 변수만으로 생성자를 호출해 빌더 객체를 얻고 그 빌더 객체가 제공하는 일종의 setter 메서드로 원하는 선택 매개변수들을 설정하는 방법이다. 마지막에는 build() 라는 메서드를 호출해 하나의 체인 형태로 구성한다.
Pizza myPizza = new Pizza.Builder("콤비네이션")
.size("L")
.toppings("불고기, 치즈, 감자")
.build();
빌더의 생성자나 메서드에서 여러 매개 변수들을 혼합해서 유효성 검사도 할 수 있다는 장점이 있으니 잘못된 매개변수를 검증하는 코드도 추가하자. 보통 매개변수를 객체로 복사해온 다음 확인하고, 검증에 실패하면 IllegalArgumentException 에러 메세지를 던져 어떤 매개변수가 잘못 되었는지 확인한다.
이 빌더패턴을 활용하면 클래스 계층 구조를 잘 활용할 수 있다. 아래와 같이 추상 빌더를 가지고 있는 추상 클래스를 만들고 하위 클래스에서 추상 클래스를 상속받으면, 각 하위 클래스에서 추상 빌더를 상속받아 만들 수 있다.
public abstract class Pizza {
public enum Topping {
HAM, MUSHROOM, ONION, PEEPER, 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();
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
추상 빌더에서 재귀적인 타입 매개변수를 사용하고 self 라는 메서드를 통해 자기 자신을 호출하는 부분이 흥미롭다.
하위 클래스에서 build 메서드의 리턴 타입으로 자기 자신을 리턴하는 Covariant 리턴 타이핑을 사용하면 클라이언트 코드에서 타입 캐스팅을 하지 않아도 된다.
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;
}
}
이렇게 설계해두면 가변 인자 매개변수를 여러 개 사용할 수 있다는 장점도 있고, addTopping 메서드를 여러 번 호출해서 전달받은 매개변수들을 하나의 필드에 모아두는 것도 가능하다. 매번 생성하는 객체를 조금씩 다르게 변화를 줄 수도 있다.
이 책에서는 빌더 패턴은 생성자 이전에 빌더 객체를 만들어야 하므로 성능 이슈를 가져올 수도 있다고 하는데 그렇게 크지 않아 보인다. 점차 요구사항이 늘어나면 매개변수가 많아질 가능성이 많으니 빌더 패턴을 잘 학습하고 알아두었다가 앞으로 늘어날 가능성이 있는 클래스 생성자에 적극적으로 활용해보자.
"생성자와 팩토리 메서드에 매개 변수가 많을 경우, 빌더 패턴을 고려하자"
'독후감 > Effective JAVA' 카테고리의 다른 글
[Effective JAVA] 6 "불필요한 객체 생성을 피하라" (0) | 2022.04.28 |
---|---|
[Effective JAVA] 5 "자원을 직접 명시하지 말고 의존 객체 주입을 사용하라" (0) | 2022.04.27 |
[Effective JAVA] 4 "인스턴스화를 막으려거든 private 생성자를 사용하라" (0) | 2022.04.26 |
[Effective JAVA] 3 "private 생성자나 열거 타입으로 싱글턴임을 보증하라" (0) | 2022.04.26 |
[Effective JAVA] 1 "생성자 대신 정적 팩터리 메서드를 고려하라" (0) | 2022.04.20 |