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

2022년 02월 22일

이 글은 Effective Java 3/E의 내용을 요약한 글입니다. 자세한 내용은 책을 참고하시기 바랍니다.

일반적으로 클래스의 인스턴스를 만들 떄 public 생성자를 통해 만들 것입니다. 하지만 생성자를 통한 인스턴스 생성하는 방법이 존재합니다. 그것이 바로 오늘 다룰 주제인 정적 팩터리 메서드(static factory method) 입니다.

정적 팩터리 메서드란?

정적 팩터리 메서드는 도대체 무엇을 뜻하는 것일까요?? 정적 팩터리 메서드를 간단히 설명하자면 새로운 인스턴스를 생성하고 반환하는 클래스 메서드를 뜻합니다.

아래의 코드는 LocalDateTime.java파일에 있는 정적 팩터리 메서드를 가져온 것입니다. 다음 코드를 보면 LocalDateTime의 of라는 클래스 메서드의 반환값은 new LocalDateTime(date, time);으로 새로운 인스턴스를 생성하여 반환하고 있습니다. 이를 이용해 우리는 of 메서드를 사용하여 LocalDateTime의 인스턴스를 생성할 수 있습니다. 이러한 메서드를 우리는 정적 팩터리 메서드라고 부릅니다.

// LocalDateTime.java
...
public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute) {
    LocalDate date = LocalDate.of(year, month, dayOfMonth);
    LocalTime time = LocalTime.of(hour, minute);
    return new LocalDateTime(date, time);
}
...

LocalDateTime time = LocalDateTime.of(2022, 2, 22, 20, 47);

정적 팩터리 메서드의 장점

1. 이름을 가질 수 있다.

// BigInteger.java
// 두 메서드 모두 양의 소수를 반환하는 메서드
public BigInteger(int bitLength, int certainty, Random rnd) {
    BigInteger prime;

    if (bitLength < 2)
    throw new ArithmeticException("bitLength < 2");
    prime = (bitLength < SMALL_PRIME_THRESHOLD
            ? smallPrime(bitLength, certainty, rnd)
            : largePrime(bitLength, certainty, rnd));
    signum = 1;
    mag = prime.mag;
}

public static BigInteger probablePrime(int bitLength, Random rnd) {
    if (bitLength < 2)
        throw new ArithmeticException("bitLength < 2");

    return (bitLength < SMALL_PRIME_THRESHOLD ?
            smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) :
            largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd));
}

위의 코드는 BigInteger.java에 있는 소수를 반환하는 생성자와 메서드입니다. 코드에서 생성자를 보면 어떤 생각이 드나요?? 해당 생성자의 매개변수를 통해 이 생성자는 소수를 반환하는거야~라는 사실을 설명할 수 있을까요?? 아마 해당 생성자를 자주 사용한 사람을 제외한 대부분의 사람들은 No❗️ 라고 답을 할 겁니다. 그러면 생성자와 비교하였을 때 probablePrime메서드는 어떤가요?? 확실히 생성자보다는 소수인 BigInteger를 반환한다는 것을 알기 쉽습니다.

이와같이 매개변수가 여러개인 클래스에 시그니처가 같은 생성자가 여러개 필요할 것 같으면, 매개변수의 순서를 다르게 하여 생성자를 추가하기보다는 생성자를 정적 팩터리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름을 지어주는 것이 좋습니다.

만약 하나의 시그니처로 매개변수의 순서를 다르게 생성자를 추가하고 사용할 경우 개발자들이 API를 잘못 호출하는 실수를 유발할 수도 있다.🤯

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

두번쨰 장점 덕분에 불변 클래스(Immutable class)는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 객체의 생성을 피할 수 있습니다.

public final class Boolean implements java.io.Serializable,Comparable<Boolean> {

    public static final Boolean TRUE = new Boolean(true);

    public static final Boolean FALSE = new Boolean(false);

    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }
}

대표적인 예로 위의 Boolean.valueOf(boolean)메서드는 TRUE, FALSE를 미리 상수로 정의해두어 메서드에서 객체를 아예 생성하지 않는 것을 확인할 수 있습니다. 따라서 팩터리 메서드는 같은 객체가 자주 요청되는 상황이라면 성능을 향상시켜주기도 합니다.

정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있으며 이를 인스턴스 통제(instance-controlled)클래스라고 합니다.

인스턴스를 통제하려는 이유는 다음과 같습니다.

  • 인스턴스를 통제하면 클래스를 싱글턴(Singleton)으로 만들 수도, 인스턴스화 불가(noninstantiable)로 만들 수도 있습니다.
  • 불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있습니다. (a==b일때 a.equals(b)가 성립)
  • 플라이웨이트 패턴의 근간이 되며, 열거타입은 인스턴스가 하나만 만들어짐을 보장합니다.

플라이웨이트 패턴이란?

플라이웨이트 패턴(Flyweight pattern)는 동일하거나 유사한 객체들 사이에 가능한 많은 데이터를 서로 공유하여 사용하도록 하여 메모리 사용량을 최소화하는 소프트웨어 디자인 패턴이다.

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

해당 능력은 자바의 다형성의 특징을 이용하여 코드의 유연성이 늘어납니다.

public interface CarType {
    static CarType getSuvType() {
        return new SuvType();
    }

    static CarType getSedanType() {
        return new SedanType();
    }
}

class SuvType implements CarType {}

class SedanType implements CarType {}

자바 8 이후부터는 인터페이스에 정적 메서드를 선언할 수 있게 되어 위와 같이 인터페이스에 선언된 정적 메서드를 통해 해당 인터페이스를 구현한 클래스 인스턴스를 생성할 수 있습니다. 이러한 방법을 사용한다면 구현 클래스를 공개하지 않고도 해당 객체를 반환할 수 있어 API를 작게 유지할 수 있습니다.

이는 인터페이스를 정적 팩터리 메서드의 반환타입으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심 기술이기도 합니다.

자바의 컬렉션 프레임워크도 정적 팩터리 메서드 방법을 사용해 45개의 클래스를 공개하지 않으며, API크기를 작게 만들 수 있었습니다.

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

반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하던지 상관없습니다.

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable {

    ...
    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.java의 noneOf메서드를 보면 원소 수에 따라 하위 클래스 중 하나의 인스턴스를 생성하여 반환하고 있습니다. 원소들이 64개 이하면 RegularEnumSet인스턴스를, 이상이면 JumboEnumSet 인스턴스를 반환하고 있습니다.

해당 메서드를 사용하는 클라이언트는 RegularEnumSet, JumboEnumSet의 존재를 모르고 사용하며, Java언어 개발 쪽에서는 특정 클래스를 없애거나 추가하더라도 EnumSet의 하위 타입이기만하면 문제없이 사용할 수 있습니다.

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

말 그대로 때때로 컴파일 시점에 알려지지 않은 클래스 객체를 생성해야할 때도 존재하는데, 이런 경우 정적 팩터리 메서드를 사용하면 쉽게 구현할 수 있습니다.

정적 팩터리 메서드의 단점

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

상속을 하려면 public이나 protected생성자가 필요한데 이러한 생성자를 private로 만들고 정적 팩터리 메서드만을 public으로 만든다면 상속을 한 하위 클래스를 만들 수가 없다.

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

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

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

API문서를 잘 쓰고 메서드 이름도 널리 알려진 규약을 따라 짓는 식으로 문제를 완화해줘야합니다.

정적 팩터리 메서드에 흔히 사용되는 명명 방식들

  • from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형 변환 메서드
  • of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
  • valueOf : from과 of의 더 자세한 버전
  • instance or getInstance : 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않는다.
  • create or new Instance : instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
  • getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. 'Type'은 팩터리 메서드가 반환할 객체의 타입이다.
  • newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다. 'Type'은 팩토리 메소드가 반환할 객체의 타입이다.
  • type : getType과 newType의 간결한 버전

핵심정리

정적 팩터리 메서드와 public 생성자는 각각의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇다고 하더라도 정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있으면 고치자.

Buy me a coffeeBuy me a coffee
Written by

@Seongwon

기술공유를 통해 새로운 가치 창조을 추구하는 백엔드 개발자 오성원입니다.
©SeongwonOh