도서

[Effective Java] 1. 생성자 대신 팩터리 메서드를 고려하라

딤섬뮨 2022. 11. 12. 20:16
728x90

클래스는 생성자와 별도로 정적 팩터리 메서드를 가질 수 있다.

예를 들어 다음과 같이 public생성자 대신 정적 팩토리 메서드를 제공할 수 있다.

    //정적 팩토리 메서드
    public static Question newQuestion(Long id, String title, String contents) {
        return new Question(id, title, contents);
    }
    
    //생성자
    private Question(long id, String title, String contents) {
        super(id);
        this.title = title;
        this.contents = contents;
    }

장점 


  • 이름을 가질 수 있다.

딱 보기에도 생성자로 인스턴스를 생성하려면 new Qustion()으로만 호출을 하여서, 객체의 특성을 잘 설명하지 못한다.

또한 엉뚱한 것을 호출하는 실수도 할 수 있다.

반면 정적 팩토리 메서드만 잘 지으면 객체의 특성을 쉽게 묘사할 수 있다.위의 예시에서도 newQuestion()으로 새로운 객체를 생성해줌이 잘 드러난다.

 

  • 호출 할 때마다 인스턴스를 새로 생성하지는 않아도 된다.

불변 클래스는 미리 인스턴스를 만들어 놓거나 생성한 인스턴스를 캐싱하여 재활용 할 수 있다.

어차피 불변 클래스는 말 자체로 상태가 바뀌지 않으니깐!

따라서 생성 비용이 큰 객체가 자주 요청되는 상황이라면 성능을 끌어 올려준다.

또한 인스턴스를 통제함으로써 싱글턴으로 만들 수도, 인스턴스화 불가 로 만들 수도, 인스턴스가 단 하나 뿐임을 비교할 수도 있다(a==b 사용가능)

ex ) [Java] 인스턴스를 캐싱하여 성능 개선하기 (feat. 로또) (tistory.com)

 

lotto 미션에서 lotto 번호 45개를 생성하는 pool 을만드는 상황.

 

요청될 때마다 45개씩 생성하면 기하급수적으로 생성비용이 커짐.

private static final int FIRST_NUM = 1;
private static final int MAX_NUM = 45;
private static final int LAST_NUM = 6;
private static final List<Integer> LOTTO_NUMBERS_POOL = new ArrayList<>();

static {
   for (int i = FIRST_NUM; i <= MAX_NUM; i++) {
      LOTTO_NUMBERS_POOL.add(i);
   }
}

 1 ~ 45 number를 가진 LOTTO_NUMBERS_POOL 리스트에 캐싱

public class RandomLottoFactory {

	private static final int FIRST_NUM = 1;
	private static final int MAX_NUM = 45;
	private static final int LAST_NUM = 6;
	private static final List<Integer> LOTTO_NUMBERS_POOL = new ArrayList<>();

	static {
		for (int i = FIRST_NUM; i <= MAX_NUM; i++) {
			LOTTO_NUMBERS_POOL.add(i);
		}
	}

	public List<Integer> randomLotto() {
		List<Integer> lottoSet = new ArrayList<>();
		Collections.shuffle(LOTTO_NUMBERS_POOL);

		for (int i = FIRST_NUM; i <= LAST_NUM; i++) {
			lottoSet.add(LOTTO_NUMBERS_POOL.get(i));
		}

		Collections.sort(lottoSet);
		return lottoSet;
	}

}

그리고 정적 팩토리 메서드를 통해 존재하는 인스턴스를 그대로 반환해줍니다.

 

+ 자료를 보니 valueOf라는 이름을 명명함으로써
매개변수에 따라 인스턴스를 반환받을 수 있음을 명시적으로 표현해줄 수 있다고도 합니다.

이렇게 코드가 개선되면 최초 1 ~ 45 까지의 LottoNumber 인스턴스 생성 비용 외에 사용하는 비용이 없다.

 

  • 반환 타입의 하위타입 객체를 반환할 수 있는 능력이 있다

정적 팩토리 메서드 (velog.io)

이 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 ‘엄청난 유연성' 을 선물한다고 한다.

public class Account {
	public Account() {}
}

public class AccountFactory {
	public static Account of(int money) {
		if(money > 100) {
			return new VipAccount();
		}
		else {
			return new NormalAccount();
		}
	}
}

public class VipAccount extends Account{
	public Vip() {}
}

public class NormalAccount extends Account{
	public Normal() {}
}

다음 코드처럼 보유한 돈에 따라, 하위 타입인 Vip, Normal 계좌들을 반환할 수 있다.

  • 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
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);
}

EnumSet 클래스의 경우 public 생성자 없이 default 생성자만 존재한다.

대신에 정적 팩토리 메서드를 적용하였는데, universe의 크기를 계산하여 64 이하일 경우, RegularEnumSet 을, 65 이상일 경우 JumboEnumSet 을 리턴해준다.

클라이언트는 이 두 클래스의 존재를 모른다. 팩토리가 건내주는 객체가 RegularEnumSet인지 아니면 JumboEnumSet 인지 알지 못하며 알 필요도 없다.

 

  • 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스를 작성할 필요가 없다.

서비스 제공자 프레임 워크를 만드는 근간이 된다. 근데 이 장점을 이해 못해서 간단한 예시만 가져왔다,

public class CourseFactory {
	public static List<Course> getCourses() {
		return new ArrayList<>();
	}
}

Course가 인터페이스이고 , 변경이 필요할 때, 이 팩토리 메서드를 수정하는게 아니라, 인터페이스만을 수정하면 된다.

 

단점


  • 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스를 만들 수 없다,

상속을 하려면 public이나 protected 생성자가 필요한데, 정적 팩터리 메서드를 사용하는 경우 기존 생성자를 private 기본생성자를 통해 외부 생성을 막아두기 때문이다.(처음 예시 참조)

따라서 정적 팩터리 메서드를 이용하는 대표적인 클래스인 컬렉션 프레임워크의 유틸리티 구현 클래스들은 상속할 수 없 다.

이 제약은 상속보다 컴포지션을 사용(Item18) 하도록 유도하고 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점이 될 수 있다.

 

  • 정적 팩토리 메서드는 프로그래머가 찾기 어렵다.

생성자처럼 API 설명에 명확히 드러나지 않으므로 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 API 문서를 통해 알아내야 한다. 

아래는 정적 팩터리 메서드에서 통용되는 명명 방식이다.

 

그런데 넥스트 스텝 클린코드 강의에서 사실 정적 팩토리 메서드도 Builder의 역할이기에 다음과 같이 of보다 명사로 선언하는 것이 좋다고 한다. 이미 통상적인 명명방식이기에 참고만 해야겠다.

 

  • from: 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
    • Date d = Date.from(instant);
  • of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
    • Set<Rank> cards = EnumSet.of(JACK, QUEEN, KING);
  • valueOf: from과 of의 더 자세한 버전
    • Boolean true = Boolean.valueOf(true);
  • instance (getlnstance): (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
    • Calendar calendar = Calendar.getlnstance(zone);
  • create (newlnstance): instance 혹은 getlnstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
    • Object newArray = Array.newInstance(classObject, arrayLen);
  • getType: getlnstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용한다. Type은 팩터리 메서드가 반환할 객체의 타입 이다.
    • Filestore fs = Flies.getFileStore(path)
  • newType: newlnstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. Type은 팩터 리 메서드가 반환할 객체의 타입 이다.
    • BufferedReader br = Files.newBufferedReader(path);
  • type. getType과 newType의 간결한 버전
    • List<Complaint> litany = Collections.list(legacyLitany);
 

 

728x90