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

이 장에서는 선택적 인자가 많을 때 객체 생성하는 방법들의 특징을 살펴보고, 왜 빌더Builder 를 고려해야하는지 얘기한다.

선택적 인자가 많을 때 객체 생성하는 방법
  1. 점층적 생성자 패턴telescoping constructor pattern

  2. 자바빈즈 패턴JavaBeans pattern

  3. 빌더 패턴Builder pattern

점층적 생성자 패턴

기본적으로 필수 인자만 받는 생성자를 정의하고, 선택적 인자를 받는 생성자를 추가하는 방법이다. 객체를 생성할 때 인자 갯수에 맞는 생성자를 골라 호출해야한다.

public class Person {
	private final String name; // 필수
	private final int age; // 필수
	private final String mail;
	private final String city;
	private final String state;

	public Person(String name, int age) {
		this(name, age, "");
	}

	public Person(String name, int age, String mail) {
		this(name, age, mail, "");
	}

	public Person(String name, int age, String mail, String city) {
		this(name, age, mail, city, "");
	}

	public Person(String name, int age, String mail, String city, String state) {
		this.name = name;
		this.age = age;
		this.mail = mail;
		this.city = city;
		this.state = state;
	}
}
문제점
  • 설정할 필요가 없는 필드에도 인자를 전달해야 해야 한다.

  • 인자 수가 늘어날수록 가독성이 떨어진다.

자바빈즈 패턴

인자 없는 생성자로 객체를 할당받고, setter를 통해 나머지 값들을 설정하는 방법이다. 객체 생성도 쉽고 위 방법보다 가독성이 좋다.

public class Person {
    private String name;
    private int age;
    private String mail;
    private String city;
    private String state;

    public Person() {}

    public void setName(String name) { this.name = name; }
    public void setAge(int age) { this.age = age; }
    ....
}
Person me = new Person();
me.setName("yeongjun.kim");
me.setAge(27);
문제점
  • 한번에 객체 생성을 끝낼 수 없으므로, 객체 일관성이 일시작으로 깨질 수 있다.

  • 변경 불가능 클래스를 만들 수 없다. 해결하기 위해서 추가 구현할 코드가 많아진다.

  • setter를 추가로 만들어줘야한다. (내생각)

    Tip
    setter 생성방법

    보통 IDEA에 getter/setter를 자동으로 추가해주는 기능이 존재한다. 혹은 lombok의 @Setter 어노테이션을 활용할 수 있다.

빌더 패턴

필수 인자들을 생성자(또는 정적 팩터리 메서드)에 전달하여 빌더 객체를 만들고, 선택적 인자들을 추가한 뒤, 마지막에 build() 를 호출하여 Immutable 객체를 만드는 방법이다.

public class Person {
	private final String name;
	private final int age;
	private final String mail;
	private final String city;
	private final String state;

	// 빌더 객체
	public static class Builder {
		// 필수 인자
		private final String name;
		private final String age;
		// 선택적 인자 - 기본값으로 초기화
		private final String mail = "";
		private final String city = "";
		private final String state = "";

		public Builder(String of, int age) {
			this.name = name;
			this.age = age;
		}

		public Builder mail(String mail) {
			this.mail = mail;
			return this;
		}

		public Builder city(String city) {
			this.city = city;
			return this;
		}

		public Builder state(String state) {
			this.state = state;
			return this;
		}

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

	private Person(Builder builder) {
		this.name = name;
		this.age = age;
		this.mail = mail;
		this.city = city;
		this.state = state;
	}
}
Person me = Person.Builder("yeongjun.kim", 27)
	.mail("opid911@gmail.com")
	.build();

특징

  1. 빌더 클래스(Builder)는 빌더가 만드는 객체 클래스(Person)의 정적 맴버 클래스로 정의한다({#item22}[규칙 22]).

    public class Person {
    	public static class Builder {
    		...
    	}
    }
  2. 불변식을 적용할 수 있으며, build() 에서 불변식이 위반되었는지 검사할 수 있다.

    public class Person {
    	public static class Builder {
    		...
    		public Person build() {
    			Person result = new Person(this);
    			if(/* result의 값 검사 */) {
    				throw new IllegalStateException(/* 위반 원인 */);
    			}
    			return result;
    		}
    	}
    }
  3. 빌더 객체에서 실제 객체로 인자가 복사된 다음에 불변식들을 검사할 수 있다는 것, 그리고 그 불변식을 빌더 객체의 필드가 아니라 실제 객체의 필드를 두고 검사할 수 있다는 것은 중요하다({#item39}[규칙 39]).

  4. 불변식을 위반한 경우, build()IllegalStateException 을 던져야 한다({#item60}[규칙 60]).

  5. 예외 객체를 살펴보면 어떤 불변식을 위반했는지 알아낼 수도 있어야 한다({#item63}[규칙 63]).

cf. 불변식을 강제하는 방법

  • 불변식이 적용될 값 전부를 인자로 받는 setter를 정의하는 방법.

  • setter는 불변식이 만족하지 않으면 *IllegalArgumentException*을 던짐.

  • build()가 호출되기 전에 불변식을 깨뜨리는 인자가 전달되었다는 것을 신속하게 알 수 있는 장점.

public class Person {
	...

	public static class Builder {

		public Builder setNameAndAge(String name, int ate) {
			if(name == null) {
				throw new IllegalArgumentException();
			}
			return this;
		}

		...

		public Person build() {
			return new Person(this);
		}
	}
	...
}
  • 메서드마다 하나씩, 필요한 만큼 varargs 인자를 받을 수 있다.

	public class Person {
		public static class Builder {
			public Builder names(String... names) {
				this.names = names;
				return this;
			}

			public Builder foramily(String... names) {
				this.farther = names[0];
				this.marther = names[1];
				return this;
			}
		}
		...
	}
  • 유연하다. (e.g. 객체가 만들어질 때마다 자동적으로 증가하는 일련번호 같은 것을 채울 수 있다)

  • 인자가 설정된 빌더는 훌륭한 [Abstract Factory][dp-abstract-factory]다. JDK1.5 이상을 사용하는 경우, 제네릭 자료형 하나면 어떤 자료형의 객체를 만드는 빌더냐의 관계 없이 모든 빌더에 적용할 수 있다.

	public interface Builder<T> {
		public T build();
	}
	public class Person {
		public static class Builder implements Builder<Person> {
			...
			public Person build() {
				return new Person(this);
			}
		}
	}
**e.g.** *Code at package `java.util.stream`*
	Stream.builder().add(1).add(2).add(3).build();
  • 빌더 객체를 인자로 받는 메서드는 보통 *한정적 와일드카드 자료형~bounded wildcard type~*을 통해 인자의 자료형을 제한한다([규칙 28](#items28)).

	Tree buildTree(Builder<? extends Node> nodeBuilder) {...}
  • 자바가 제공하는 추상적 팩토리로는 Class 객체가 있으며, 이 객체의 newInstance() 가 build 메서드 구실을 한다.

    **하지만,** newInstance()는 항상 무인자 생성자를 호출하려 하는데, 문제는 그런 생성자가 없을 수도 있다는 것. TO-DO

문제점

  • 빌더 객체를 만드는 오버헤드가 문제가 될 수 있다(성능이 중요한 상황). 그러니 인자 갯수가 통제할 수 없을 정도로 많아지만 빌더 패턴을 적용하자.

요약

빌더 패턴은 인자가 많은 생성자나 정적 팩터리가 필요한 클래스를 설계할 때, 특시 대부분의 인자가 선택적 인자인 상황에 유용하다.

Tip
lombok 활용하여 빌더 만들기
@Value // immutable(private, final 적용)
@Builder
public class Person {
	String name;
	int age;
	String mail;
	String city;
	String state;
}