///
Search
💡

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

Builder패턴부터 알아보자

빌더 패턴은 여러 멤버변수를 가진 객체의 경우 Builder라는 팩토리 객체를 통해 인스턴스를 만들어주는 패턴이다. 예시로 바로 이해해보자. 다음은 영양성분 정보들을 담고 있는 NutritionFacts 객체와, 내부 빌더이다.
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 static class Builder { // 필수 매개변수 private final int servingSize; private final int servings; // 선택 매개변수 private int calories = 0; private int fat = 0; private int sodiium = 0; private int carbohydrate = 0; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder sodium(int val) { sodiium = val; return this; } public Builder carbohydrate(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; fat = builder.fat; sodium = builder.sodiium; carbohydrate = builder.carbohydrate; } }
Java
복사
필수 값인 멤버변수는 빌더를 생성할 때 인자로 받고, 나머지 값은 빌더의 메서드 연쇄을 통해 삽입할 수 있도록 한다. (메서드 연쇄가 가능한 API를 플루언트 API라고 하기도 한다.)

빌더 패턴이 필요한 이유

멤버 변수가 많을 때, 모든 필드를 매개변수로 받는 하나의 생성자만 제공하는 경우 해당 클래스를 활용하기 매우 어렵다. 그래서 주 생성자와 부 생성자를 분리한 점층적 생성자 패턴, 객체를 일단 생성한 후 값을 나중에 채워넣는 자바빈즈 패턴을 사용하곤 한다. 링크
그러나 점층적 생성자 패턴은 매개변수의 개수가 많아질수록 어떤 생성자를 이용하는 중인지 깊은 주의가 필요하다. 깊은 주의가 필요함은 반드시 휴먼 에러가 일어날 것임을 암시한다.
자바빈즈 패턴은 객체가 준비된 상태임을 보증하기가 어렵다. 일단 생성 후, 필요한 값을 삽입하지 않고 사용되는 것을 방지하려면 null check 등 불편한 수단을 사용해야 한다. 이렇게 필요한 값을 제대로 가지고 있지 않은 객체를 일관성이 깨진 객체라고 부르는데, 이는 버그를 불러일으키고 생성시기와 값이 삽입된 시기가 다르기 때문에 디버깅도 쉽지 않다.
이런 경우 빌더 패턴은 좋은 대안이다. 이 패턴은 파이썬과 스칼라에 있는 명명된 선택적 매개변수를 흉내낸 것이다.
빌더 패턴을 사용하는 경우 다음처럼 활용할 수 있다. 의미가 명확하지 않은가?
NutritionFacts cocacola = new NutritionFacts.Builder(240, 8) .calories(100) .sodium(35) .carbohydrate(27) .build();
Java
복사

유효성 검사는?

각각의 삽입 메서드에서 진행하고, build에서 불변식(해당 객체가 지켜야할 규칙)을 검사한다. 검사 중 매개변수가 잘 못 되었다면, 어떻게 잘 못 되었는지 상세한 에러메시지를 담아 IllegalArgumentException 클래스를 던지면 된다.

계층적 설계에서의 빌더 활용

빌더는 계층적 설계와 잘 어울린다. 각 계층에 클래스에 관련 빌더를 멤버로 정의하자. 추상 클래스는 추상 빌더를, 구체 클래스는 구체 빌더를 갖게 한다. 다음은 피자 추상 클래스의 구현이다.
public abstract class Pizza { public enum Topping { HAM, ONION, GALIC, SAUSAGE, BULGOGI } Set<Topping> toppingList; abstract static class Builder<T extends Builder<T>>{ EnumSet<Topping> toppingList = EnumSet.noneOf(Topping.class); public T addTopping(Topping topping){ toppingList.add(Objects.requireNonNull(topping)); return self(); } abstract Pizza build(); protected abstract T self(); } Pizza(Builder<?> builder){ toppingList = builder.toppingList.clone(); // 아이템 50 참조 } }
Java
복사
상위 타입에서 제네릭을 이용해 하위 타입이 빌더의 중복 내용을 제거할 수 있도록 도울 수 있다. 눈 여겨 볼 것은 self()이다. 추상 메서드인 self를 통해 하위 클래스에서 형변환 없이도 메서드 연쇄를 지원할 수 있다. self 타입이 없는 자바를 위해 활용한 이 우회 방법을 시뮬레이트한 셀프 타입 관용구라고 한다.

추가적인 이점과 단점

빌더를 이용하면 가변인수 매개변수를 여러개 사용할 수 있다. 빌더 패턴은 유연하다. 빌더 하나로 여러 객체를 순회하며 만들 수 있고, 빌더에 넘기는 매개변수에 따라 다른 객체를 생성할 수 도 있다. 객체마다 부여되는 일련번호는 스스로 채우게 할 수 도 있다.
빌더 패턴이 가진 단점은, 객체를 만들기 전에 빌더를 만들어야 한다는 것이다. 빌더 생성 비용이 크진 않지만 성능이 매우 민감한 프로그램이라면 문제가 될 수 도 있다. 또한 위에서 봤듯이 필드가 장황해지기 때문에 멤버 필드가 4개 이상은 되어야 값어치를 한다. 하지만 프로그램을 하다보면 멤버 변수가 늘어나는 일이 흔하기 때문에 미리 빌더 패턴을 적용해 놓는 것이 나을 때가 많다.

핵심 정리

생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 낫다. 매개 변수 중 다수가 nullable하거나 같은 타입이면 더더욱 그렇다. 점층적 생성자 패턴보다 간결하고, 자바빈즈보다 훨씬 안전하다.

용어 정리

한글명
영어명
메서드 연쇄
method chaining
플루언트 API
fluent API
멤버 변수
member variable
멤버 필드
member field
점층적 생성자 패턴
Telescoping Constructor Pattern
자바 빈즈 패턴
java beans pattern
일관성
consistency
명명된 선택적 매개변수
named optional parameters
불변식
immutability
추상 클래스
abstract class
구체 클래스
concrete class
시뮬레이트한 셀프 타입
simulate self-type
가변 인수
varargs