개발자는 기록이 답이다

ITEM 1 : 생성자 대신 정적 팩터리 메서드를 고려하라 본문

기술 서적/Effective Java

ITEM 1 : 생성자 대신 정적 팩터리 메서드를 고려하라

slow-walker 2023. 12. 26. 11:47

Static Factory Method(정적 메소드)

 

클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단 : public 생성자

클래스는 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있다

// boolean의 기본 타입의 값을 받아 Boolean 객체 참조로 변환
public static Boolean valueOf(boolean b){
    return b ? Boolean.TRUE : Boolean.FALSE;
}

 

※ 정적 팩터리 메서드는 디자인 패턴에서의 팩터리 메서드(Factory Method)와다르다.

디자인 패턴 중에 이와 일치하는 패턴은 없다.

 

장점

1. 이름을 가질 수 있다. (동일한 시그니처의 생성자를 두개 가질 수 없다.)

 

생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다.

하지만 정적 팩터리는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.

 

하나의 시그니처는 생성자를 하나만 만들 수 있다.

예를 들어, 아래처럼 시그니처(반환타입, 메소드명, 매개 파라미터)가 동일하고 변수명만 다를 경우, 컴파일 에러가 난다.

  • 생성자는 클래스 이름과 동일하게 만들어야하고, 해당 클래스 타입만 리턴해야 한다.

하나의 시그니처는 생성자 하나만 만들 수 있다.

 

매개변수 순서를 다르게 한 생성자

 

위의 그림처럼 입력 매개변수들의 순서를 다르게 한 생성자를 새로 추가하는 식으로 이 제한을 피할 수 도 있지만, 좋지 않은 발상이다.

  • 가독성과 오류 방지 : 입력 매개변수의 순서가 다르면 사용자가 어떤 생성자를 호출해야할지 혼동 가능성이 높아진다
  • 유지보수의 어려움 : 생성자의 시그니철르 변경하거나, 새로운 생성자를 추가할때 기존 코드에서 호출되는 부분을 찾아 모두 수정해야 한다. 다수의 클래스가 해당 생성자를 사용하고 있다면, 일일이 찾아 수정하는 작업이 번거로울 수 있다.
  • 명시성 부족 : 생성자 시그니처는 매개변수의 개수와 타입만으로 명시되기 때문에, 이름 자체로 어떤 역할을 하는지 구분하기 어렵다.

한 클래스에 생성자의 시그니처가 중복되는 경우, 팩터리 메서드로 각각의 차이를 잘 드러내는 이름을 지어주자.

package org.example.item1;

public class Person {
    private String lastName;
    private String firstName;

    // 생성자
    public Person(String lastName, String firstName) {
        this.lastName = lastName;
        this.firstName = firstName;
    }

    public Person() {}

    // 정적 팩터리 메소드 1
    public static Person createWithLastName(String lastName) {
        Person person = new Person();
        person.lastName = lastName;
        return person;
    }

    // 정적 팩터리 메소드 2
    public static Person createWithFirstName(String firstName) {
        Person person = new Person();
        person.firstName = firstName;
        return person;
    }
}

 

 

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

 

자바의 생성자는 매번 호출될때마다 새로운 인스턴스를 만든다.

  • 아무런 생성자를 만들지 않아도, 기본 생성자가 있기 때문에 매번 새로운 인스턴스가 만들어진다.
  • 생성자가 있다면 인스턴스 통제가 불가능하다 : 어디서든 생성자를 호출해서 매번 새로운 인스턴스를 만들 수 있게 된다.
  • 만약 Settngs 인스턴스가 딱 하나만, 유일한 인스턴스로 있어야 한다면, 생성자가 아니라 정적 팩토리를 통해 통제할 수 있다.
package org.example.item1;

public class Settings {
    private boolean useAutoSteering;
    private boolean useABS;
    private Person person;

    private Settings() {}

    private static final Settings SETTINGS = new Settings();

    public static Settings newInstance() {
        return SETTINGS;
    }
}

 

정적 팩토리를 사용하면 결코 여러개의 인스턴스를 만들 수 없게 된다.

  • 아래 그림을 확인해보면, 객체를 여러 번 생성해도 해시 코드 값이 동일한 것을 확인할 수 있다.

 

불변 클래스(immutable class)는 인스턴스를 미리 만들어놓거나, 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.

 

Boolean.valueof()는 이에 대한 대표적인 예로, 객체를 생성하지 않는다.

public final class Boolean implements java.io.Serializable,
                                      Comparable<Boolean>
{
    /**
     * The {@code Boolean} object corresponding to the primitive
     * value {@code true}.
     */
    public static final Boolean TRUE = new Boolean(true);

    /**
     * The {@code Boolean} object corresponding to the primitive
     * value {@code false}.
     */
    public static final Boolean FALSE = new Boolean(false);
         // ...
}
// boolean의 기본 타입의 값을 받아 Boolean 객체 참조로 변환
public static Boolean valueOf(boolean b){
    return b ? Boolean.TRUE : Boolean.FALSE;
}

 

생성 비용이 큰 같은 객체가 자주 요청되는 상황(데이터베이스 연결)이라면 성능을 상당히 끌어올려준다.

  • 플라이웨이트(Flyweight pattern) : 데이터를 공유해 메모리를 절약하는 패턴
    • 공통으로 사용되는 객체는 한번만 사용되고, pool에 의해서 관리, 사용된다.
    • 자주 사용하는 값을 미리 캐싱해서 넣어두고 꺼내서 사용하는 디자인 패턴(인스턴스를 통제하는 방법을 통해)
    • 자주 변경되는 속성(또는 외적인 속성, extrinsit)과 변하지 않는 속성(또는 내적인 속성, instrinsit)을 분리하고 재사용하여 메모리 사용을 줄인다.
    • 자주 변경되지 않은 값들을 따로 분리해서 인스턴스 메서드가 아니라 static 팩토리 메서드를 사용해서 플라이웨이트를 사용하면, 매번 새로운 인스턴스를 새로 만드는게 아니라, 하나의 인스턴스를 공유해서 쓰니까 객체를 가볍게 사용하고 메모리 사용량을 줄일 수 있다. 해당 내용을 따로 정리한 코드는 여기를 보면 된다.
  • 인스턴스 통제(instance-controlled) : 반복되는 요청에 같은 객체를 반환하면 언제 어느 인스턴스를 살아있게 할지를 철저히 통제할 수 있다.
  • 인스턴스를 통제하는 이유?
    • 인스턴스를 통제하면 클래스는 싱글턴(singleton; item3)으로 만들 수 있다.
    • 인스턴스화 불가(noninstantiable; item4)로 만들 수 있다.
    • 불변 값 클래스를(item 17)에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다.
      • a == b 일때만 a.equals(b) 성립
    • 플라이웨이트 패턴의 근간이 되며, 열거타입(item34)은 인스턴스가 하나만 만들어짐을 보장한다.

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

 

  • 반환할 객체의 클래스를 자유롭게 선택할 수 있다.(유연성)
  • 인터페이스 기반 프레임워크(item20) : 인터페이스를 정적 팩터리 메서드의 반환 타입으로 사용 (자바 8부터 허용)
    • 선언의 리턴타입에는 인터페이스를 넣어놨지만, 막상 리턴해주는 인스턴스는 인터페이스의 구현체로 리턴해도 된다.

 

  • 클라이언트로부터 구체적인 클래스 타입(KoreanHelloService, EnglishHelloService)을 숨길 수 있다.
    • 또한, 클래스를 선언해놓고, 해당하는 클래스의 하위 클래스를 리턴할 수도 있다.
package org.example.item01;

public class HelloServiceFactory {
    public static HelloService of(String lang) {
        if (lang.equals("ko")) {
            return new KoreanHelloService();
        } else {
            return new EnglishHelloService();
        }
    }
    // 외부에 있는 메인 메소드라고 가정하자
    public static void main(String[] args) {
        // 클라이언트 코드로부터 인터페이스 기반의 프레임웍을 사용하도록 강제할 수 있음
        // 구체적인 타입을 클라이언트에게 숨길 수 있다.
        HelloService ko = HelloServiceFactory.of("ko");
    }
}

 

  • 인터페이스에서 static 메서드를 선언할 수 있으므로, factory 클래스에 별도의 정적 팩토리 메서드를 만들지 않아도 된다.
    • 관련해서 포스팅한 내용은 여기를 참고하면 된다.

 

  • Java 7버전의 Collections.emptyList() 메소드를 살펴보자.
    • Java 7에서는 인터페이스의 정적 팩터리 메소드를 지원하지 않았기 때문에 Collections 클래스 내에 동반 클래스(Companion Class)인 EmptyList를 사용해서 빈 리스트를 생성한다.
    • 물론 이 방법도 클라이언트가 빈 리스트를 얻기 위해 어떤 구체적인 클래스(EmptyList)가 사용되는지 알 필요가 없으며, 불필요한 클래스 세부사항에 의존하지 않는다.
// java7 Collections.emptyList()
public Collections(){
      ///...

    public static final List EMPTY_LIST = new EmptyList<>();

    public static final <T> List<T> emptyList() {
        return (List<T>) EMPTY_LIST;
    }

    private static class EmptyList<E>
        extends AbstractList<E>
        implements RandomAccess, Serializable {
        @java.io.Serial
        private static final long serialVersionUID = 8842843931221139166L;

        public Iterator<E> iterator() {
            return emptyIterator();
        }
        public ListIterator<E> listIterator() {
            return emptyListIterator();
        }

        public int size() {return 0;}
      //...
}

 

  • Java 9버전의 List of()메소드를 살펴보자
    • Java 9에서는 인터페이스 내에서 정적 팩터리 메소드를 지원하게 되어, List 인터페이스에 직접 정적 팩터리 메소드를 추가하여 불변한 리스트를 생성한다.
    • 라이브러리를 뜯어보면 위의 코드보다 더 간결한 것을 확인할 수 있다.
    • 이 메소드는 ImmutableCollections내부에 있는 ListN.EMPTY_LIST를 반환한다.
    • 클라이언트 코드는 반환되는 리스트가 어떻게 구현되었는지에 대한 세부사항을 알 필요가 없고, 반환 타입은 List로만 다룬다.
// java9 List of()

public interface List<E> extends Collection<E> {

    int size();
    ...
    static <E> List<E> of() {
        return (List<E>) ImmutableCollections.EMPTY_LIST;
    }
}

 

 

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

3번의 개념과 비슷한 내용으로 KoreanHelloService와 EnglishHelloService예제 그림을 보면 된다.

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

  • 다음 릴리스에서는 또 다른 클래스의 객체를 반환해도 된다.
  • 대표적인 예로 EnumSet 클래스(Item 36)는 public 생성자 없이 오직 정적 팩터리만으로 제공한다.
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable
{
    //...
    /**
     * Creates an empty enum set with the specified element type.
     *
     * @param <E> The class of the elements in the set
     * @param elementType the class object of the element type for this enum
     *     set
     * @return An empty enum set of the specified type.
     * @throws NullPointerException if <tt>elementType</tt> is null
     */
    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);
    }

  //...
}

 

EnumType의 원소의 개수에 따라 RegularEnumSet, JumboEnumSet 으로 결정되는데 클라이언트는 이 두 객체의 존재를 모르며, 추후에 새로운 타입을 만들거나 기존 타입을 없애는 경우에도 문제되지 않는다.

 

 

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

  • 대표적인 Service Provider Framework(서비스 제공자 프레임워크)로는 JDBC 가 있다.
  • 서비스 인터페이스(JDBC - Connection) : 구현체의 동작 정의
  • 제공자 등록 API(JDBC - DriverManager.registerDriver) : provider가 구현체를 등록할 때 사용
  • 서비스 접근 API(JDBC - DriverManager.getConnection) : 클라이언트는 서비스 접근 API 사용시 원하는 구현체의 조건을 명시할 수 있음
  • 서비스 제공자 인터페이스(JDBC - Driver) : 서비스 인터페이스의 인스턴스를 생성하는 펙토리 객체를 설명해준다.
    서비스 제공자 인터페이스가 없다면 각 구현체를 인스턴스로 만들 때 리플렉션(Item 65)를 사용해야 한다.
     
    클라이언트는 서비스 접근 API 사용시 원하는 구현체의 조건을 명시할 수 있는 점은 Service Provider Framework가 유연한 정적 팩토리라고 할 수 있는 실체이다.
 

단점

 

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

정적 팩토리 만을 사용하게끔 만든 클래스는  생성자를 private으로 만들어야하므로, 상속을 허용하지 않게 된다.

그러므로 상속보다 Composition 사용을 유도하고, immutable타입으로 만들려면 해당 제약을 지켜야 한다는 점에서 장점이 된다.

 

생성자를 허용해서 객체를 만드는 방법과, static factory 메서드를 사용하는 방법 모두 다 사용하는 경우도 있는데,

대표적으로 ArrayList이다.

  •  new 키워드로 리스트 객체 생성
  • List.of() 정적 팩토리 메서드로 리스트 객체 생성

2. 프로그래머가 해당 메서드를 찾기 어렵다.

 

생성자는 API Docs 상단에 모아두었기 때문에 찾기가 쉬우나, 정적 팩터리 메서드는 다른 메서드와 구분 없이 보여주므로 사용자가 인스턴스화할 방법을 알아서 찾아내야한다. 인텔리제이로 java docs만드는 방법은 여기에 잘 나와있다.

 

Person 클래스로 API DOCS

 

생성자를 카테고리화 해준것과 달리 메서드 부분은 어떤게 정적 팰토리인지 찾기가 어렵다.

메서드가 많아질 경우, 인스턴스를 생성해주는 용도의 메서드를 찾기가 어려울 것이다.

생성자 없이 정적 팩터리만 있는 경우, java document를 읽는 사람은 인스턴스를 어떻게 만드는지 의문을 가질 것이다.

주로 사용하는 명명 방식

메서드 설명 예제
from
매개변수를 하나 받아 해당 타입의 인스턴스를 반환(형변환 method)
Date d = Date.from(instant);
 
of 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드 Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf from과 of의 더 자세한 버전 BigInteger.valueOf(Integer.MAX_VALUE);
instance / getInstance 매개변수를 받을 경우 매개변수로 명시한 인스턴스를 반환하지만 같은 인스턴스임을 보장하지는 않음 StackWalker luke = StackWalker.getInstance(options);
create / newInstance instance 혹은 getInstance와 같지만 매번 새로운 인스턴스를 생성해 반환한다. Object newArr = Array.newInstance(classObj,arrayLen);
getType getInstance와 같으나 생성할 클래스가 아닌 다른 클레스의 팩터리 메서드를 정의할 때 사용한다. FileStore fs = Files.getFileStore(path)
newType newInstance와 같으나 생성할 클래스가 아닌 다른 클레스의 팩터리 메서드를 정의할 때 사용한다. BufferedReader br = Files.newBufferedReader(path);
type getType과 newType의 간결한 버전 List<Complaint> litany = Collections.list(legachLitancy);

 

명명 규칙을 사용하여 API DOCS의 static 메서드 탭에서 확인하거나, 문서화를 잘 해둬야 한다.

 

 

결론

 

생성자를 사용하지 말라는 것이 아니다.

평범한 경우에는 생성자를 사용하되, 정적 팩토리 메서드가 적절한 경우인지를 판단해라.