정적 팩토리 메서드와 생성자는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다. 빌더 패턴이 나오기까지의 과정을 살펴보자.

생성자에 매개변수가 많을 때 사용할 수 있는 방법

1. 점층적 생성자 패턴

점층적 생성자 패턴을 사용할 때 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 의미를 파악하기 어렵다.

점층적 생성자 패턴은 필수 매개변수만 받는 생성자를 기반으로 선택 매개변수를 하나씩 늘린 생성자를 만드는 패턴이다.

Circles 클래스에서 필수 매개변수인 radius와 count를 받는 생성자를 기본으로 선택 매개변수인 colorsolidLineshadow를 하나씩 추가한 생성자를 만들었다. 이 클래스를 사용하기 위해서는 원하는 선택 매개변수를 모두 포함한 생성자 중 가장 짧은 생성자를 호출하게 되는데, 이 방법은 사용하지 않는 매개변수도 결국 채워진다는 점에서 비효율적이다. 자료형이 같은 값이 연속되면 각 값의 의미도 헷갈릴 것이다.

public class Circles {
  private final int radius;          // 반지름       (필수)
  private final int count;           // 원 개수      (필수)
  private final String color;        // 원 색깔      (선택)
  private final boolean solidLine;   // 실선 여부    (선택)
  private final boolean shadow;       // 그림자 여부 (선택)
  
  public Circles (int radius, int count) {
    this(radius, count, "");
  }

  public Circles (int radius, int count, Stirng color) {
    this(radius, count, color, 0);
  }

  public Circles (int radius, int count, String color, boolean solidLine) {
    this(radius, count, color, solidLine 0);
  }

  public Circles (int radius, int count, String color, boolean solidLine, boolean shadow){
    this.radius = radius;
    this.count = count;
    this.color = color;
    this.solidLine = solidLine;
    this.shadow = shadow;
  }
}

 

2.  자바빈즈 패턴

자바빈즈 패턴을 사용하면 객체 하나를 만들기 위해 여러 메소드를 호출해야 하고 객체가 완전히 생성되기 전에 일관성이 무너진다.

자바빈즈 패턴은 매개변수가 없는 생성자로 객체를 만든 후, 세터 메서드를 호출해 원하는 매개변수 값을 설정하는 패턴이다. 반지름이 3이고 그림자가 있는 노란색 원을 7개 만들려면 아래 예시와 같이 생성하면 된다.

이 방법은 인스턴스를 만들기 쉽고 각 매개변수를 따로 설정하기 때문에 읽기에도 쉽다. 그러나 circles 객체를 하나 만들기 위해 세터 메서드를 5개나 호출해야 하고 모든 메서드 호출이 완료되기 전까지는 객체 일관성이 깨진다는 단점이 있다. 객체 일관성이 깨지면 스레드 안정성을 얻기 위해 수동으로 객체를 얼리고 녹여야 하는데 이 방법은 런타임 오류에 취약하므로 사용하기 어렵다.

Circles circles = new Circles();
circles.setRadius(3);
circles.setCount(7);
circles.setColor("yellow");
circles.setShadow(true);
public class Circles {
  // 기본값으로 초기화
  private int radius = 1;
  private int count = 1;
  private String color = "white";
  private boolean solidLine = true;
  private boolean shadow = false;
  
  public Circles() { }

  // 세터 메서드
  public void setRadius(int val)          { radius = val; }
  public void setCount(int val)           { count = val; }
  public void setColor(String val)        { color = val; }
  public void setSolidLine(boolean val)   { solidLine = val; }
  public void setShadow(boolean val)      { shadow = val; }
}

 

3. 빌더 패턴

빌더 패턴은 점층적 생성자 패턴의 안정성과 자바빈즈 패턴의 가독성을 둘 다 가진다. 생성자나 정적 팩터리 메서드가 처리할 매개변수가 많다면 빌더 패턴을 사용하는 것이 좋다.

빌더 패턴에서 클라이언트는 필요한 객체를 직접 만들지 않고, 필수 매개변수를 사용해 생성자(혹은 정적 팩토리 메서드)를 호출해 빌더 객체를 얻는다. 빌더 객체는 세터 메서드로 선택 매개변수를 설정하고 build()를 호출해 객체를 만든다.

아래 예제를 보면 빌더의 세터 메서드는 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다. 이 코드는 점층적 생성자 패턴보다 읽고 쓰기 쉽다.

잘못된 매개변수가 넘어오는 것을 발견하려면 빌더의 생성자와 메서드에서 매개변수 유효성을 검사하고, 여러 매개변수에 걸친 오류는 build()에서 불변식 여부를 검사해야 한다. 잘못된 점이 있다면 IllegalArgumentException을 던지면 된다.
Circles circle = new Circles.Builder(3, 7).color("yellow").shadow(true);
public class Circles {
  private final int radius;
  private final int count;
  private final String color;
  private final boolean solidLine;
  private final boolean shadow;

  public static class Builder {
    // 필수 매개변수
    private final int radius;
    private final int count;

    // 선택 매개변수 (기본값으로 초기화)
    private String color = "white";
    private boolean solidLine = true;
    private boolean shadow = false;

    public Builder(int radius, int count) {
      this.radius = radius;
      this.count = count;
    }
    public Builder(String val) { color = val; return this; }
    public Builder(boolean val) { solidLine = val; return this; }
    public Builder(boolean val) { shadow = val; return this; }

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

  private Circles(Builder builder) {
    radius = builder.redius;
    count = builder.count;
    color = builder.color;
    solidLine = builder.solidLine;
    shadow  =builder.shadow;
  }
}

단, 빌더 객체를 만들려면 다른 방법에 비해 장황한 빌더부터 만들어야 한다. 따라서 매개변수가 4개 이상인 경우에 빌더 사용을 고려하는 것이 낫다.

빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다. 빌더 클래스에는 재귀적 타입 한정을 이용하는 제네릭 타입을 반환 타입으로 사용할 수 있고, 여기에 추상 메서드인 self를 더해 하위 클래스 형변환 없이 메서드 연쇄를 지원할 수 있다.

정적 팩토리 메서드로 인스턴스 생성 시 장점

1. 이름 사용

: 메서드 명에 클라이언트에 반환할 객체 특성을 나타낼 수 있어서 이름이 없는 생성자에 비해 개발자들이 각 팩토리 메서드가 어떤 역할을 하는지 이해하기 쉽다.

 

2. 인스턴스 통제

: 호출될 때마다 동일한 객체가 반환되도록 해서 인스턴스를 통제할 수 있고 싱글톤 혹은 인스턴스화 불가하게 만들어서 성능을 끌어올릴 수 있다.

 

3. 하위 객체 반환

: 정적 팩토리 메서드를 사용하면 반환 타입의 하위 객체를 반환할 수 있다. 이 특징은 API를 더 유연하고 가볍게 만든다.

 

[예시] EnumSet

클라이언트는 팩터리가 전달하는 객체가 어떤 클래스의 인스턴스인지 알 필요 없다. 단지 EnumSet의 하위 클래스이기만 하면 된다.

EnumSet은 public 생성자 대신 정적 팩토리 메서드를 사용한다. 아래 noneOf 메서드를 보면 universe.length가 64보다 큰지에 따라 EnumSet의 하위 클래스인 RegularEnumSet 또는 JumboEnumSet을 반환할 수 있다.

public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
  Enum<?>[] universe = getUniverse(elementType);
  if (universe == null)
      throw new ClassCastException(elementType + " not an enum");

  if (universe.length <= 64)
      return new RegularEnumSet<>(elementType, universe);
  else
      return new JumboEnumSet<>(elementType, universe);
}

 

[예시] Collections

정적 팩토리 메서드를 응용해 구현 클래스를 숨기면 API를 논리, 물리적으로 가볍게 만들 수 있다. 인터페이스 기반 프레임워크의 핵심 기술이다.

java.util.Collections는 java.util.Collection 인터페이스의 구현체이며 동반 클래스(companion class)라고도 한다. unmodifiableSet 반환 타입은 인터페이스이고, 실제로 반환하는 것은 인터페이스 하위 객체인 UnmodifiableSet의 인스턴스이다. 여기서 클라이언트는 인스턴스가 어떻게 구현되었는지 알 필요가 없으므로 API를 사용하기 위해 인터페이스 정보만 알면 된다.

public class Collections {

  // 정적 팩토리 메서드
  public static <T> Set<T> unmodifiableSet(Set<? extends T> s) {
        return new UnmodifiableSet<>(s);
    }

  // 구현 클래스
  static class UnmodifiableSet<E> extends UnmodifiableCollection<E> implements Set<E>, Serializable{/* 중략 */}
}

단, Java8 이전에는 인터페이스에 static 메서드를 사용할 수 없었으므로 동반 클래스를 사용했다는 점에 유의한다.

 

[예시] Collections

정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

서비스 제공자 프레임워크는 작성 시점에 반환할 객체의 클래스가 존재하지 않아도 된다는 정적 팩토리 메서드의 유연성을 기반으로 만들어진다.

예를 들어, 대표적인 서비스 제공자 프레임워크인 JDBC는 구현체를 클라이언트에 제공해 클라이언트를 구현체로부터 분리하는 역할을 한다. 서비스 제공자 프레임워크를 구성하는 서비스 접근 API 컴포넌트는 클라이언트가 서비스의 인스턴스를 얻을 때 사용된다. 클라이언트는 서비스 접근 API를 사용할 때 원하는 구현체의 조건을 명시할 수 있고, 조건을 명시하지 않으면 기본 구현체를 반환하거나 지원하는 구현체를 돌아가며 반환할 수 있다.

 

정적 팩토리 메서드로 인스턴스 생성 시 단점

1. 하위 클래스 생성 불가

: 생성자 대신 정적 팩토리 메서드만 사용한다면 하위 클래스를 만들 수 없다. 상속하기 위해서는 상위 클래스에 public이나 protected 생성자가 필요하기 때문이다. (상속보다 컴포지션 사용을 권장한다는 점에서 장점일 수 있다.)

 

2. 객체를 생성하는 메서드인지 알기 어려움

: 생성자와 비교해보면 정적 팩토리 메서드는 API 설명에서 객체를 생성하는 역할을 하는지 알기 어렵다.

+ Recent posts