생성자 보단 정적 팩토리 메소드

디우·2022년 3월 3일
0

스터디 진행..그리고 이전 자동차 경주 게임에 비해서 난이도가 올라간 로또 미션으로..정적 팩토리 메소드에 대한 정리를 이제서야 하게된다.
사실 우테코를 시작하고 가장 많이 게으른 한 주를 보낸 것이 사실이다.
늦었지만 자동차 경주 게임 미션을 진행하며 피드백을 통해 알게 되었던 정적 팩토리 메소드에 대해서 정리해보고자 한다.


정적 팩토리 메소드란?

이름에서 알 수 있듯이 정적(static) 이고, 팩토리(factory) 즉 무엇인가를 만들어내는 메소드라는 의미이다. (디자인 패턴의 팩토리 패턴에서 유래한 factory는 객체를 생성하는 역할을 분리하겠다는 취지가 담겨있다.)
정리하면 정적 팩토리 메소드객체의 생성을 담당하는 static 메소드 라는 의미이다.

개발자는 이런 글보다는 코드를 통해서 보는 것이 보다 이해가 빠르다고 생각한다. 다음은 자동차 경주 게임 미션을 진행하면서 구현한 GameTotalCount 클래스로 게임 진행 횟수에 대한 책임을 가지는 클래스이다.

package racingcar.domain;

public class GameTotalCount {

    private static final int GAME_END_CONDITION = 0;

    private int totalAttemptCount;

    private GameTotalCount(int totalAttemptCount) {
        validatePositiveNumber(totalAttemptCount);
        this.totalAttemptCount = totalAttemptCount;
    }

    public static GameTotalCount createGameTotalCount(String attempt) {
        int number = translateInteger(attempt);

        return new GameTotalCount(number);
    }

    public int getTotalAttemptCount() {
        return totalAttemptCount;
    }

    public boolean isContinue() {
        if (totalAttemptCount == GAME_END_CONDITION) {
            return false;
        }

        return true;
    }

    public void reduceAttemptCount() {
        totalAttemptCount = totalAttemptCount - 1;
    }

    private static int translateInteger(String attempt) {
        try {
            return Integer.parseInt(attempt);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("[ERROR] 시도 횟수는 숫자여야 합니다.");
        }
    }

    private void validatePositiveNumber(int number) {
        if (number <= 0) {
            throw new IllegalArgumentException("[ERROR] 양수를 입력해주세요.");
        }
    }
}

여기서 정적 팩토리 메소드 가 무엇인지 찾아낼 수 있겠는가? 아마 대부분의 이 글을 읽는 사람들은 바로 찾아낼 수 있을 것이라고 생각한다. 바로 생성자 바로 아래에 있는 createGameTotalCount 이다.
하지만 위 코드를 보며 다음과 같이 반문하는 사람이 있을 것이라고 생각한다.

왜 굳이 static한 메소드인 createGameTotalCount() 를 별도로 두고 있을까? 아래와 같은 생성자를 두면 되지 않을까?
public GameTotalCount(String attempt) {
	int totalAttemptCount = translateInteger(attempt);
    validatePositiveNumber(totalAttemptCount);
    this.totalAttemptCount = totalAttemptCount;
}

하지만 위의 피드백 메시지에서도 알 수 있다시피 GameTotalCount에서 중요한 생성규칙은 0보다 큰 수를 기준으로 만든다는 것이다. 즉 코드를 통해 표현하면 다음과 같다.

	public GameTotalCount(int totalAttemptCount) {
        validatePositiveNumber(totalAttemptCount);
        this.totalAttemptCount = totalAttemptCount;
    }

이것이 GameTotalCount의 핵심인 것이다. 이 클래스의 생성에 있어서 문자열을 받는지 혹은 Double, Long 등을 받는지는 관심이 없다. 따라서 문자열을 받는 별도의 생성자인 정적 팩토리 메소드 를 두고 0보다 큰 수 인 것을 체크하는 책임과 문자열을 변환할 때 정상 숫자인지에 대한 역할을 나누게 되는 것이다.

객체의 본질이 숫자라는 것에 집중하면 가장 근본적인 생성은 숫자를 받아 생성하는 것이고 0보다 큰 수 여야한다는 조건이다.


장점과 단점

그렇다면 이렇게 정적 팩토리 메소드를 활용함으로써 얻을 수 있는 장점에는 무엇이 있을까? 또 단점에는 어떤 것이 있을까?

이름을 가질 수 있다.

생성자는 메소드가 이름을 가지지 않는다. Class 명과 동일한 메소드명을 사용해야한다. 매개변수만으로 생성자 오버로딩을 하게 되는데 이를 통해서는 반환될 객체의 특성을 제대로 설명해주지 못한다. 예를 들어 이전 인스턴스와 동일 인스턴스인지 혹은 호출시 마다 항상 새로운 인스턴스를 반환해주는지 알 수 없다. 직접 생성자 내의 코드의 흐름을 읽어야만이 파악가능하다.
하지만 정적 팩토리 메소드를 활용한다면 이름을 통해서 반활될 객체의 특성을 쉽게 표현할 수 있다.

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

두번째 장점은 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다는 것이다.

로또 미션 에서 구현한 VO인 LottoNumber 클래스이다.

public class LottoNumber implements Comparable<LottoNumber> {

    private static final int MINIMUM_LOTTO_NUMBER = 1;
    private static final int MAXIMUM_LOTTO_NUMBER = 45;
    private static final Map<Integer, LottoNumber> LOTTO_TOTAL_NUMBERS = IntStream.rangeClosed(MINIMUM_LOTTO_NUMBER, MAXIMUM_LOTTO_NUMBER)
            .boxed()
            .collect(toMap(identity(), LottoNumber::new));

    private final int number;

    private LottoNumber(int number) {
        this.number = number;
    }

    public static LottoNumber from(int number) {
        validateNumberBoundary(number);

        LottoNumber lottoNumber = LOTTO_TOTAL_NUMBERS.get(number);

        return Objects.requireNonNullElseGet(lottoNumber, () -> new LottoNumber(number));
    }

    private static void validateNumberBoundary(int number) {
        if (number < MINIMUM_LOTTO_NUMBER || number > MAXIMUM_LOTTO_NUMBER) {
            throw new IllegalArgumentException("1~45의 숫자이어야 합니다.");
        }
    }

	...
}

여기서 static final 로 1~45까지의 정수를 통해서 생성한 LottoNumber를 가지는 List를 가지고 있고, 정적 팩토리 메소드를 통해서 만약 이미 인스턴스가 존재한다면 새로운 인스턴스가 아닌 기존 List 에서 꺼내서 반환해주고, 다음번에 동일한 요청이 왔을 때 기존에 저장해놓은 인스턴스를 반환해줄 수 있다.

반환 타입의 하위 타입 객체를 반환할 수 있다.

생성자에는 반환 타입을 명시하지 않는다. 즉 해당 생성자를 선언하는 클래스의 인스턴스를 생성해서 반환한다. 하지만 정적 팩토리 메소드를 사용하면 반환할 객체의 클래스를 자유롭게 선택할 수 있는 유연성을 제공한다.

이를 통해서 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다. 라고는 언급하고 있지만 해당 장점에 대해 EFFECTIVE JAVA 에서 설명하는 내용은 아직 공감 혹은 이해가 100% 되지 않아 향후에 해당 장점을 느끼게 되면 다시 업데이트 하도록 하겠습니다ㅠㅠ

입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

반환하는 타입의 하위 타입이기만 한다면 어떤 클래스의 객체를 반환하든 상관이 없게 된다. 이는 클라이언트 입장에서 정적 팩토리 메소드를 통해서 반환해주는 객체가 어느 클래스의 인스턴스인지 알 수도 없고 알 필요도 없다는 점에서 큰 장점을 가진다.

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

이해하지 못한 내용으로..마찬가지로 향후 업데이트

확실한 점은 정적 팩토리 메소드는 단순 생성자의 기능을 대신할
뿐 아니라 보다 좋은 가독성을 제공하며(로직을 다 보지 않아도메소드의 이름을 통해서 우리는 어떤 인스턴스가 반환될지 짐작할 수 있다.) 보다 객체지향적으로 개발할 수 있도록 인도한다.

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

어찌보면 위 단점은 상속보다는 구성(composition) 을 사용하도록 유도하고 불변 타입으로 만들려면 해당 제약을 지켜야한다는 점에서 오히려 장점으로 받아들일 수도 있다.

정적 팩토리 메소드는 찾기 어렵다.

단순 static 메소드라고 보고 넘어가기 쉽상이다. 그래서 본인은 생성자 바로 아래에 정적 팩토리 메소드를 두기는 하지만 이것 또한 본인의 생각이고 본인이 본인의 코드를 작성할 때의 일이다. 그래서 많은 개발자들이 다음과 같이 네이밍 컨벤션을 만들어 두었다.

  • form: 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메소드

  • of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메소드

  • valueOf: from 과 of 의 더 자세한 버전

  • instance 혹은 getInstance: (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.

  • create 혹은 newInstance: instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.

  • getType: getInstance 와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다.

  • newType: newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다.

  • type: getType과 newType의 간결한 버전

profile
꾸준함에서 의미를 찾자!

0개의 댓글