생성자 대신 정적 팩토리 메서드를 고려하라

정적 팩토리 메서드란?

정적 팩토리 메서드란 객체 생성의 역할을 하는 클래스 메서드와 같은 의미로 요약할 수 있다.

아래의 코드를 보자. 다음 코드는 java.time패키지에 포함된 LocalTime클래스의 정적 팩토리 메서드이다.

// LocalTime.class
...
public static LocalTime of(int hour, int minute) {
  ChronoField.HOUR_OF_DAY.checkValidValue((long)hour);
  if (minute == 0) {
    return HOURS[hour];
  } else {
    ChronoField.MINUTE_OF_HOUR.checkValidValue((long)minute);
    return new LocalTime(hour, minute, 0, 0);
  }
}
...

// hour, minutes을 인자로 받아서 9시 30분을 의미하는 LocalTime 객체를 반환한다.
LocalTime openTime = LocalTime.of(9, 30);

위 코드는 of메소드를 통해 집적접으로 생성자를 통해 객체를 반환하는 것이 아닌, 자체 메서드를 통해 객체를 생성하고 반환한다.

이처럼 생성자 외에, 개발자가 직접 명시하여 객체를 생성 및 리턴하는 메소드를 정적 팩토리 메서드라고 한다.

그렇다면 왜 정적 팩토리 메소드를 권장할까?

클래스를 정의할 때 public생성자 대신 정적 팩터리 메서드를 제공하면 다음과 같은 이점이 있다.

  1. 이름을 가질 수 있다.

    1. 생성자에 넘기는 매개변수와 생성자 자체로는 반환될 객체의 특성을 제대로 설명하지 못한다. 예컨대 생성자인 BigInteger와 BigInteger.probablePrime 중 어느쪽이 ‘소수인 BigInteger를 반환한다’를 잘 표현할까?
    public class LottoFactory() {
      private static final int LOTTO_SIZE = 6;
      
      private static List<LottoNumber> allLottoNumbers = ...; // 1~45까지의 로또 넘버
        
      public static Lotto createAutoLotto() {
        Collections.shuffle(allLottoNumbers);
        return new Lotto(allLottoNumbers.stream()
                .limit(LOTTO_SIZE)
                .collect(Collectors.toList()));
      }
    
      public static Lotto createManualLotto(List<LottoNumber> lottoNumbers) {
        return new Lotto(lottoNumbers);
      }
      ...
    }
    
  2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.

    1. 불변클래스는 인스턴스를 미리 만들어 놓거나, 새로 생성한 인스턴스를 캐싱하여 재활용할 수 있다.
    2. 대표적 예로 Boolean.valueOf(boolean) 메서드는 객체를 아예 생성하지 않는다.
    public class LottoNumber {
      private static final int MIN_LOTTO_NUMBER = 1;
      private static final int MAX_LOTTO_NUMBER = 45;
      
      private static Map<Integer, LottoNumber> lottoNumberCache = new HashMap<>();
      
      static {
        IntStream.range(MIN_LOTTO_NUMBER, MAX_LOTTO_NUMBER)
                    .forEach(i -> lottoNumberCache.put(i, new LottoNumber(i)));
      }
      
      private int number;
      
      private LottoNumber(int number) {  
        this.number = number;
      }
      
      public LottoNumber of(int number) {  // LottoNumber를 반환하는 정적 팩토리 메서드
        return lottoNumberCache.get(number);
      }
      
      ...
    }
    
  3. 반환 타입의 하위 타입 객체를 반환할 수 있다.

    1. 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 유연성이 있다.
    2. 이같은 방법은 인터페이스기반 프레임워크를 만드는 핵심 기술이기도 하다.
    3. java.util.Collections에서 정적 팩터리 메소드를 통해 구현체를 제공하는 것을 생각해보라
    public class Level {
      ... 
      public static Level of(int score) {
        if (score < 50) {
          return new Basic();
        } else if (score < 80) {
          return new Intermediate();
        } else {
          return new Advanced();
        }
      }
      ...
    }
    
  4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

    1. 반환타입의 하위타입이기만 하면 어떤 클래스의 객체를 반환하든 상관이없다.
  5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다

    1. 즉 유연성이 매우 높다는 것이다. 대표적인 예로 JDBC프레임웤이 있는데, 실제 서비스 구현체는 JDBC이나, 이를 사용하는 인터페이스는 마이바티스, JPA, JPQL등 다양한 인터페이스가 있다. 즉 정적팩토리메소드를 작성만 해두면, 언제든지 인터페이스를 통한 기능확장이 가능하다.
    2. 이를 클라이언트가 구현체로부터 분리되었다고 한다.

서브스를 제공하는 프레임워크는 다음과 같은 핵심 컴포넌트로 이루어진다.

  1. 서비스 인터페이스 [JDBC의 Connection]
  2. 제공자 등록 API [DriverManager.registerDriver()]