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

두별·2023년 4월 2일
1

TIL

목록 보기
38/46
post-thumbnail

Effective Java 3/E 북스터디 기록
아이템1. 생성자 대신 정적 팩터리 메서드를 고려하라

정적 팩토리 메소드

클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자이지만, 이 방법은 매개변수 만을 보고 어떤 객체가 반환될지 예측이 잘 되지 않는 단점이 있다.
그래서 이러한 방법 말고 모든 프로그래머가 알아둬야 할 기법이 하나 더 있다. 바로 클래스는 생성자와 별도로 정적 팩토리 메소드(static factory method)를 제공할 수 있다는 것.

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);
    }
}

valueOf()메소드는 기본 타입인 boolean 값을 받아 Boolean 객체 참조로 변환해주는 메소드
이렇게 클래스는 public 생성자 대신 정적 팩토리 메소드를 제공할 수 있다.

정적 팩토리 메소드 장점

1. 이름을 가질 수 있다.
일반적인 생성자로 객체를 생성한다면 매개변수가 무슨 의미를 뜻하는지 알기 어려우나, 정적 팩터리 메서드를 고려한다면 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.

Book book1 = new Book(“이펙티브 자바”);
Book book2 = Book.createByTitle(“이펙티브 자바”);

2. 인스턴스 통제 클래스
호출될 때 마다 인스턴스를 새로 생성하지 않아도 된다.
new 키워드를 사용하면, 객체는 무조건 새로 생성된다. 만약, 자주 생성될 것 같은 인스턴스는 클래스 내부에 미리 생성해 놓은 다음 반환한다면 코드를 최적화할 수 있을 것 이다.
불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.

예시로 Boolean.valueOf(boolean) 이 메소드는 객체를 아예 생성하지 않는다.
TRUE, FALSE를 상수로 정의해놓고 메소드에서는 이것을 반환하고 앴다.
따라서 객체 생성 비용이 큰 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어올릴 수 있다.

정적 팩토리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있다. 이런 클래스를 인스턴스 통제 클래스 라고 한다.

인스턴스를 통제하는 이유?

  • 클래스를 싱글턴(Singleton) 또는 인스턴스화 불가 (Noninstantiable) 클래스로 만들 수 있다
  • 불변 값 클래스에서 동일한 값을 가지고 있는 인스턴스를 단 하나 뿐임을 보장할 수 있다. (a == b 일 때만, a.equals(b). 즉, a 와 b 가 같은 메모리 주소를 갖을 때만 둘의 '값'도 같을 수 있다.)
  • 인스턴스 통제는 플라이웨이트 패턴의 근간이 되며, 열거 타입은 인스턴스가 하나만 만들어짐을 보장

플라이웨이트 패턴이란?
플라이웨이트 패턴 (Flyweight pattern) : 데이터를 공유하여 메모리를 절약하는 패턴, 공통으로 사용되는 객체는 한번만 사용되고 Pool에 의해서 관리, 사용된다.
(JVM의 String Pool에서 같은 String이 잇는지 먼저 찾는다. [불변객체 String])

3. 반환 타입의 하위 타입 객체를 반환할 수 있다.
생성자를 사용하면 생성되는 객체의 클래스가 하나로 고정된다. 정적 팩터리 메서드를 사용하면 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 엄청난 유연성을 가지게 된다.

public interface Type {
    static Type getAType() {
        return new AType();
    }

    static Type getBType() {
        return new BType();
    }
}

class AType implements Type {
}

class BType implements Type {
}

getAType() 메소드와 getBType()의 메소드의 반환 타입은 인터페이스인 Type 이지만, 반환하고 있는 것은 인터페이스의 하위 클래스인 것을 볼 수 있다.

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);
    }
}

noneOf() 메소드를 보면 원소가 64개 이하면 원소들을 long 변수 하나로 관리하는 RegularEnumSet의 인스턴스를, 65개 이상이면 long 배열로 관리하는 JumboEnumSet의 인스턴스를 반환하는 것을 볼 수 있다.
이 두 객체 타입은 노출되지 않고 감춰져 있기 때문에 사용자는 이에 대해 알 필요가 없으며 추후에 새로운 타입을 만들거나 기존 타입을 없애도 문제없이 사용할 수 있다. (EnumSet의 하위타입이기만 하면 되는 것이다.)

5. 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
유연함은 서비스 제공자 프레임워크를 만드는 근반이 된다. 대표적인 서비스 제공자 프레임워크로는 JDBC가 있다.

단점

1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩토리 메소드만 제공하면 하위 클래스를 만들 수 없다.
인스턴스 통제 클래스를 구현하기 위해서는 사용자가 new 키워드를 사용하여 임의로 객체를 생성함을 막아야한다. 이를 위해 생성자의 접근 제어자를 private 로 설정해야하는데, 생성자가 private 인 클래스는 상속을 할 수 없다. 즉, 부모 클래스가 될 수 없다.
하지만, 이 제약은 상속보다 컴포지션을 사용하도록 유도하고, 불변타입으로 만들기 위해서는 이 제약을 지켜야 한다. 따라서 이 단점은 장점으로도 작용할 수 도 있다.

2. 정적 팩토리 메소드는 프로그래머가 찾기 어렵다.
일반적으로 자바 API Docs를 보면 생성자는 상단에 있기 때문에 찾기가 쉽다. 하지만 정적 팩토리 메소드는 다른 메소드와 구분 없이 함께 보여주고
사용자가 정적 팩토리 메소드 방식 클래스를 인스턴스화할 방법을 알아내야 하는데 찾기 쉽지 않다는 단점이 있다.

정적 팩터리 메서드 네이밍

단점2를 보완하기 위해 널리 알려진 규약을 통해 정적 팩터리 메서드를 명명하는 것이 좋다. 이펙티브 자바에서 소개하는 정적 팩터리 메서드 네이밍 컨벤션은 아래와 같다.

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

Date d = Date.from(instant);

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

Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);

valueOf
from 과 of 의 더 자세한 버전

BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);

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

StackWalker luke = StackWalker.getInstance(options);

create 혹은 newInstance
instance 혹은 getInstance 와 비슷하지만, 매번 새로운 인스턴스를 생성하여 반환함을 보장한다.

Object newArray = Array.newInstance(classObject, arrayLen);

getType
getInstance 와 같으나, 현재 클래스가 아닌 다른 클래스의 인스턴스를 생성할 때 사용한다. Type 은 팩터리 메서드가 반환할 객체의 타입을 적는다.

FileStore fs = Files.getFileStore(path);

newType
createInstance 와 같으나, 현재 클래스가 아닌 다른 클래스의 인스턴스를 생성할 때 사용한다. Type 은 팩터리 메서드가 반환할 객체의 타입을 적는다.

BufferedReader br = Files.newBufferedReader(path);

type
getType 과 newType 의 간결한 버전

List<Complaint> litany = Collections.list(legacyLitany);

리뷰

  1. 언제 어느 인스턴스를 살아 있게 할지를 통제할 수 있다는 말이 잘 이해되지 않아요.
    • 정적 팩토리 메서드 안에서 어떤 객체를 생성한다거나 혹은 다시 생성한다거나 그런 것들을 제어할 수 있다는 의미
    • 인스턴스를 딱 하나만 만들게 할 수 있다는 의미 (싱글톤으로 만들거나 인스턴스화 하지 못하게 통제할 수 있다)
    • 정적 팩토리 메서드 내에서만 객체를 생성할 수 있도록 유도 할 수 있는 것
    • 객체를 매번 생성 하지 않고 미리 생성된 객체를 재활용 하면서 인스턴스의 수를 통제한다는 뜻으로 이해
  2. 플라이웨이트 패턴이 잘 이해되지 않아요.
    • pool을 사용해서 객체를 불필요하게 생성하지 않는 메모리 절약 패턴!
  3. 정적 팩터리 메소드가 실무에서 자주 사용되는 상황
    JPA entity -> dto 혹은 request dto -> entity를 변환할 때 많이 사용했어요.
    (+ QueryDSL을 사용할때 필요한 값만 조회한 후 바로 DTO로 받는 경우는 제외)
  4. static 영역에 이런 정적 팩터리 메소드를 많이 올려도 성능적인 문제가 없는지?
    성능적인 문제는 없지만 다른 개념으로, static으로 선언한 상수의 타입이 list나 Map일때 add나 put으로 데이터를 추가하면 아웃오브메모리 익셉션이 발생할 수 있으므로 주의해야 합니다. (static영역에 메모리가 꽉 차게 되니까)

0개의 댓글