/////
Search
Duplicate
🎃

item 2. 생성자에 매개변수가 많다면 빌더를 고려하라

정적 팩토리와 생성자는 똑같은 제약을 하나 가지고 있다. 선택적 매개변수가 많을때 적절히 대응하기가 힘들다는 것이 바로 그것이다.
먼저 점층적 생성자 패턴에 대해서 알아보자.
public class NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; public NutritionFacts(int servingSize, int servings) { this(servingSize, servings, 0); } public NutritionFacts(int servingSize, int servings, int calories) { this(servingSize, servings, calories, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat) { this(servingSize, servings, calories, fat, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) { this(servingSize, servings, calories, fat, sodium, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) { this.servingSize = servingSize; this.servings = servings; this.calories = calories; this.fat = fat; this.sodium = sodium; this.cabohydrate = carbohydrate; } }
Java
복사
이 클래스의 인스턴스를 만들려면 원하는 매개변수를 모두 포함한 생성자 중 가장 짧은 것을 골라 호출하면 된다.
보통 이런 생성자는 사용자가 설정하길 원치 않는 매개변수까지 포함하기 쉬우나 어쩔 수 없이 그런 매개변수도 값을 설정해주어야 한다.
점층적 생성자 패턴도 사용이 어렵지는 않지만 매개변수가 만약 더 늘어난다면 클라이언트 코드를 작성하거나 읽기 어렵게 된다.
코드를 읽을 때 각 값의 의미가 무엇인지 파악하기가 힘들어진다.
매개변수가 몇 개인지도 주의해서 세어보아야하게 된다.
두 번째 대안으로는 자바빈즈 패턴이 존재한다.
매개변수가 없는 생성자로 객체를 만든 후, setter 메소드를 호출해 원하는 매개변수의 값만 설정하는 방식이다.
public class NutritionFacts { private int servingSize; private int servings; private int calories; private int fat; private int sodium; private int carbohydrate; public NutritionFacts() {} public void setServingSize(int val); public void setServings(int val); public void setCalories(int val); public void setFat(int val); public void setSodium(int val); public void setCarbohydrate(int val); }
Java
복사
점층적 생성자 패턴의 단점들이 더는 보이지 않는다. 코드가 길어지긴 했지만 인스턴스를 만들기 쉽고 읽기도 더 쉬워졌다.
불행히도 자바빈즈 패턴은 심각한 단점을 지니고 있다.
자바빈즈 패턴에서는 객체 하나를 만들려면 메소드를 여러개 호출해야 하고 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.
점층적 생성자 패턴에서는 매개변수들이 유효한지를 생성자에서만 확인하면 되었으나 그 장치가 완전히 사라진 것이다.
일관성이 깨진 객체가 만들어지면 버그가 존재하는 코드와 그 버그 때문에 런타임 상에서 문제가 발생하는 코드가 물리적으로 멀리 떨어져있으므로 디버깅이 쉽지 않다.
이처럼 일관성이 무너지는 문제 때문에 자바빈브 패턴에서는 객체를 불변(final)으로 만들 수 없으며 스레드 안전성을 확보하려면 프로그래머가 병렬 프로그래밍 관련 처리를 해주어야 한다.
다행히 우리에게는 세 번째 대안이 있다. 바로 빌더 패턴이다.
클라이언트는 필요한 객체를 만드는 대신 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다.
그런 다음 빌더 객체가 제공하는 일종의 세터 메소드들로 원하는 선택 매개변수들을 설정한다.
마지막으로 매개변수가 없는 build 메소드를 호출해 우리에게 필요한 객체를 얻는다.
빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어두는 것이 보통이다.
빌더의 동작방식 - 점층적 생성자 패턴과 자바빈즈 패턴의 장점만 취했다.
public class NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.carbohydrate; } public static class Builder { // 필수 매개변수 private final int servingSize; private final int servings; // 선택 매개변수 private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; // 필수 매개변수만을 담은 Builder 생성자 public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } // 선택 매개변수의 setter, Builder 자신을 반환해 연쇄적으로 호출 가능 public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public Builder carbohydrate(int val) { carbohydrate = val; return this; } // build() 호출로 최종 불변 객체를 얻는다. public NutritionFacts build() { return new NutritionFacts(this); } } }
Java
복사
NutritionFacts 클래스는 불변이며 모든 매개변수의 기본값들을 한곳에 모아두었다.
Builder 클래스의 set 메소드들은 빌더 클래스 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다.
이처럼 메소드 호출이 흐르듯 연결된다는 뜻으로 플루언트 API 혹은 메소드 체이닝이라고 한다.
빌더 패턴은 사용하기 쉽고 무엇보다도 가독성이 좋다. 이는 파이썬과 스칼라에 있는 명명된 선택적 매개변수를 흉내낸 것이다.
빌더 패턴은 계층적으로 설계된 클래스와 함께 사용하기 좋은데, 각 계층의 클래스에 관련 빌더를 멤버로 정의하자.
추상 클래스는 추상 빌더를 구체 클래스는 구체 빌더를 가지게 한다.
다음 예시를 확인해보자.
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)); 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.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; } } public class Calzone extends Pizza { private final boolean sauceInside; public static class Builder extends Pizza.Builder<Builder> { private boolean sauceInside = false; public Builder sauceInside() { sauceInside = true; return this; } @Override public Calzone build() { return new Calzone(this); } @Override protected Builder self() { return this; } } private Calzone(Builder builder) { super(builder); sauceInside = builder.sauceInside; } }
Java
복사
추상 클래스인 Pizza의 내부 Builder 클래스는 재귀적 한정 타입을 이용하는 제네릭 타입이다.
abstract static class Builder<T extends Builder<T>> 이 부분을 조금 더 상세하게 알아보자..
일단 추상 클래스 Builder는 제네릭을 이용해 자기 자신을 반환하는 메소드인 self()의 반환형 타입으로 규정하고 있다.
여기서 재귀적 한정 타입을 이용해 해당 제네릭의 타입을 자기 자신을 상속한 경우로 한정하였다.
이렇게 함으로써 Builder 클래스의 서브 클래스는 Builder 클래스 자신의 타입을 사용할 수 있게 된다.
일반적으로 타입 안정성과 확장성을 위해 이 기법을 사용한다.
각 하위 클래스의 빌더가 정의한 build 메소드는 해당하는 구체 클래스를 반환하도록 선언되었다.
하위 클래스의 메소드가 상위 클래스에서 정의된 반환 타입이 아닌 그 하위 타입을 반환하는 기능을 공변 반환 타이핑이라 한다.
이 기능을 이용하면 이 Builder를 사용하는 메소드에선 형변환을 신경쓰지 않고도 빌더를 사용할 수 있다.
외에도 빌더를 사용하면 가변인수 매개변수를 여러개 사용할 수 있다는 장점이 있다.
물론 단점도 존재한다.
객체를 만들려면 그에 앞서 빌더 객체부터 만들어야 한다. 성능에 민감한 경우 충분히 이슈가 될 수 있는 영역이다.
점층적 생성자 패턴보다는 코드 라인 수가 많아지므로 가급적 변수가 4개 이상인 경우에 사용할만 하다.
하지만 API는 기본적으로 시간이 지남에 따라 매개변수가 많아질 가능성이 항상 존재한다는 걸 명심하자.
생성자나 정적 팩토리로 만들어두었다가 매개변수가 많아질때 리팩토링해도 되지만 가급적 빌더로 시작하는 편이 나을 때가 많다.

핵심 정리

생성자나 정적 팩토리가 처리해야할 매개변수가 많다면 빌더 패턴을 사용하는 것이 낫다.
매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다.
빌더는 점층적 생성자 패턴보다 이를 호출하는 메소드 입장에서 가독성이 좋으며 자바빈즈 패턴보다 훨씬 안전하다.