규칙2 생성자 인자가 많을 때는 Builder 패턴 적용을 고려하라

규칙2 생성자 인자가 많을 때는 Builder 패턴 적용을 고려하라

생성자 인자가 많을때 프로그래머들은 보통 점층적 생성자 패턴 ( telescoping constructor pattern )을 적용한다.

  • 필수 인자를 받는 생성자와 선택적 인자를 추가해 생성자를 더 추가하는 형식.
Class constructor{
	public constructor ( int i1, int i2 ) { … }
	public constructor ( int i1, int i2, string h1 ) { … }
	public constructor ( int i1, int i2, string h1, int i3 ) { … }
	public constructor ( int i1, int i2, string h1, int i3, double d1 ) { … }
	……
}

이렇게 하다 보면, 설정할 필요가 없는 필드에도 인자를 전달해야 할 수도 있다.

  • 점층적 생성자 패턴은 잘 동작하지만 인자 수가 늘어나면 클라이언트 코드를 작성하기가 어려어지고, 무엇보다 읽기 어려운 코드가 되고 만다.
  • 그 많은 인자가 무슨 값인지 알기 어렵게 되고 그 의미를 알려면 인자를 주의깊게 보아야한다.
  • 클라이언트가 두 인자의 순서를 뒤집어도 컴파일러는 알지 못하고 프로그램 실행 도중 문제가 발생할 수도 있다.

생성자에 전달되는 인자 수가 많을 때 적용 가능한 두 번째 대안은 ( JavaBeans ) 패턴이다.

  • 인자 없는 생성자를 호출하여 객체부터 만든다.
  • 설정 메서드들을 호출하여 필수 필드 뿐만 아니라 선택적 필드의 값들 까지 채운다.
Class JavaBeansPattern {
	private int beanSIze = -1;
	private int patternSize = 0;

	public JavaBeansPattern () { }

	public void setBeanSize( int val ) { beanSize = val; }
	public void setPatternSize( int val ) { patternSize = val; }
}
  • 점층적 생성자 패턴에 있던 문제는 없다.
  • 코드의 양이 많아 질 수 있따.
  • 객체를 생성하기 쉽다.
  • 읽기도 좋다.

하지만, 1회의 함수 호출로 객체 생성을 끝낼 수 없으므로, 객체 일관성 ( Consistency )가 일시적으로 깨질 수 있다.

  • 일관성이 꺠진 객체를 사용할 때 생기는 문제는 실제 버그 위치에서 한참 떨어진 곳에서 발생하므로 디버깅이 어렵다.

이와 관련된 또 다른 문제는, 자바빈 패턴으로는 변경 불가능 ( immutable ) 클래스를 만들 수 없다는 것이다.

또 쓰레드 안전성 ( Thread-Safety ) 를 제공하기 위해 해야할 일도 더 많아진다.

점층적 생성자 패턴의 안전성에 자바빈 패턴의 가독성을 결합한 세 번째 대안은 바로 빌더 패턴이다.

  1. 필요한 객체를 직접 생성하는 대신, 클라이언트는 먼저 필수 인자들을 생성자에 ( or 정적 팩터리 메서드에 ) 전달해 빌더 객체 ( builder object )를 만든다.
  2. 그런 다음 빌더 객체에 정의된 설정 메서드들을 호출하여 선택적 인자들을 추가해 나간다.
  3. 그리고 마지막으로 아무런 인자 없이 build 메서드를 호출하여 변경 불가능 ( immutable ) 객체를 만드는 것이다.
Public class BuilderPattern {
	private final int gateWayPort;
	private final int gateWayIp;
	private final int waySize;
	private final int gateSize;

	public static class Builder {
		// 필수 인자
		private final int gateWayPort
		private final int gateWayIp;

		// 기본값으로 초기화
		private int waySize = 0;
		private int gateSize = 0;

		public Builder(int gateWayPort, int gateWayIp) {
			this.gateWayPort = gateWayPort;
			this.gateWayIp = gateWayIp;
		}

		public Builder waySize(int val)
			{ waySize = val; return this; }
		public Builder gateSize(int val)
			{ gateSize = val; return this; }

		public BuilderPattern build() {
			return new BuilderPatern(this);
		}
	}

	private BuilderPattern(Builder builder) {
		gateWayPort = builder.gateWayPort;
		gateWayIp = builder.gateWayIp;
		waySize = builder.waySize;
		gateSize = builder.gateSize;
	}
}
  • BuilderPattern 객체는 변경 불가능하다.
  • 모든 인자의 기본값이 한곳에 모여있다.
  • 빌더에 정의된 설정 메서드는 빌더 객체 자신을 반환하므로, 설정 메서드를 호출하는 코드는 쭉 이어서 쓸 수 있다.
BuilderPattern bpObject = new BuilderPattern.Builder(22, 19216802).
									waySize(20).gateSize(50);

이 코드는 작성하기 쉽고 무엇보다 읽기 쉽다.

Ada나 Python 같은 언어는 선택적 인자에 이름을 붙일 수 있도록 허용하는데, 그것과 비슷한 코드를 작성할 수 있기 떄문이다.

  • 생성자와 마찬가지로, 빌더 ㅍ턴을 사용하면 인자에 불변식 ( invariant )을 적용할 수 있다.
  • Build 메서드 안에서 해당 불변식이 위반되었는지 검사할 수 있는 것이다.
  • 빌더 객체에서 실제 객체로 인자가 복사된 다음에 불변식들을 검사할 수 있다는 것.
  • 그 불변식을 빌더 객체의 필드가 아니라 실제 객체의 필드를 두고 검사할 수 있다는 것은 중요하다.
  • 불변식을 위반한 경우 build 메서드는 IllegalStateException을 던져야 한다.
  • 이 예외 객체를 살펴보면 어떤 불변식을 위반했는지 알아낼 수도 있어야 한다.
  • 여러 인장 관련된 불변식을 강제하는 또 한 가지 방법은 불변식이 적용될 값 전부를 인자로 받는 설정자 메서드(setter method)를 정의하는 것이다.
  • 생성자와 비교했을떄 빌더 패턴이 갖는 또 한 가지 작은 장점은 빌더 객체는 여러 개의 varargs 인자를 받을 수 있다.
  • 빌더 패턴은 유연하다.
  • 하나의 빌더 객체로 여러 객체를 만들 수 있다.
  • 다른 객체를 생성해야 할 때마다 빌더 객체의 설정 메서드를 호출하면 다음에 생성될 객체를 바꿀 수 있다.
  • 빌더 객체는 어떤 필드의 값은 자동으로 채울수 있다. 객체가 만들어질떄 마다 자동적으로 증가되는 일련번호 같은 것은 좋은 예다.

인자가 설정된 빌더는 훌륭한 추상 팩터리다. 다시 말해서 클라이언트는 그런 빌더를 어떤 메서드에 넘겨서 해당 메서드가 클라이언트에게 하나 이상의 객체를 만들어 주도록 할 수 있다.

  • 제네릭 자료형 하나면 어떤 자료형의 객체를 만드는 빌더냐에 관계없이 모든 빌더에 적용할 수 있다.
// 자료형이 T인 객체에 대한 빌더
Public interface Builder<T> {
	public T build();
}
이러한 인터페이스가 있다면 BuilderPattern.Builder 클래스는 Builder<BuilderPattern>를 implements 하도록 선언할 수 있다.
빌더 객체를 인자르 받는 메서드는 보통 한정적 와일드카드 자료형 ( bounded wildcard Type )을 통해 인자의 자료형을 제한한다.

// 클라이언트가 전달한 Builder 객체를 사용하여 트리의 노드를 만든다. Tree builderTree(Builder<? Extends Node> nodeBuilder() { … }

Class.newInstance는 컴파일 시점에 예외 검사가 가능해야 한다는 규칙을 깨뜨린다.

빌더 패턴에도 단점은 있다. 객체를 생성하려면 우선 빌더 객체를 생성해야 한다. 실무에서 빌더 객체를 만드는 오버헤드가 문제가 될 소지는 없어 보이지만, 성능이 중요한 상황에선 그렇지 않을수도 있다. 또한, 빌더 패턴은 점층적 생성자 패턴보다 많은 코드를 요구하기 때문에 인자가 충분히 많은 상황( 네개 이상 )에서 이용해야 한다. 지금은 인자가 적더라도 나중에 새로운 인자를 추가해야 할 상황이 올 수도 있따.

우선은 생성자와 정적 팩터리로 시작 -> 인자 개수가 통제할 수 없을 정도로 많아지면 빌더 패턴을 적용하자. 하지만, 처음부터 빌더 패턴을 적용하는 것이 나을떄도 있다.

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

출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙2 인용.