Java/Effective Java

[이펙티브 자바] 아이템 01.생성자 대신 정적 팩터리 메서드를 고려하라(Re)

메성 2020. 7. 17. 00:16
반응형

지난 포스팅 때, 책을 그저 적기만 한 것 같아 다시 한번 포스팅을 시작했다..


생성자와 정적 팩터리 메서드

보통 클래스의 인스턴스는 public 생성자를 활용하여 생성한다. 그런데 클래스 자체는 생성자와 별도로 아래와 같은 정적 팩토리 메소드를 제공할 수 있다.

public static int testMethod() {
  return 0;
}

위에서 설명한 바와 같이 클래스에서는 클라이언트에게 public 생성자를 제공해주지만 이 뿐만 아니라 정적 팩토리 메소드도 제공해줄 수 있다.

//public 생성자
public class MyBook {
  public MyBook() {}
}

//static factory method
public class MyBook {

  private MyBook() {}

  public static MyBook getInstance() {
    return new MyBook();
  }
}

그럼 public 생성자와 static factory method를 사용했을 때의 장점과 단점을 살펴보자.


정적 팩토리 메소드(static factory method)가 생성자보다 좋은 이유

장점 1. 이름을 가질 수 있다.

  • public 생성자의 경우 생성자가 가지는 파라미터와 반환될 생성자 객체의 특성을 제대로 알 수가 없다. 즉, 반대로 말하면 생성자가 가지는 파라미터와 반환될 생성자의 특성을 제대로 알아야만 해당 생성자를 사용할 수 있다는 것이다.

  • 반면, 정적 팩토리 메소드의 경우 메소드의 네이밍만 잘 짓는다면 어떤 객체가 반환되고 해당 메소드가 어떤 행동을 가지고 있는지 쉽게 파악이 가능할 것이다.

    • 예를 들어, 생성자인 Result(int, int)과 정적 팩토리 메소드 Result.sum 중 어느 쪽이 '덧셈을 위한 값을 반환한다'라는 의미를 더 잘 갖고 있는 지 알 수 있을 것이다.

      public Result(int a, int b) {}
      
      public static Result sum(int a, int b) {
        return new Result(a, b);
      }
    • 또한, 한 클래스의 시그니처(메소드 명, 메소드 파라미터가 같은 것)가 같은 생성자가 여러 개 필요할 시에는 생성자를 오버로딩하여 구현하는 것이 아니라, 정적 팩토리 메소드를 활용하여 구현하는 것이 구분하기 더욱 편한 것이다.

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

  • 불변 클래스(객체가 가지는 값마다 새로운 인스턴스가 필요) 같은 경우 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하고 있다.

    //1) 미리 만들어 놓은 인스턴스 활용
    Boolean flg = new Boolean(true); //(x)
    Boolean flg = Boolean.valueOf(true);    //(o) 미리 만들어 놓은 인스턴스를 반환
    
    //2) 생성한 인스턴스 재활용
    String str = "test";    //1. "test" 인스턴스를 생성
    String str2 = "test";    //2. 생성된 "test" 인스턴스를 재활용
    • 이로 인해서 생성 비용이 크고 반복적으로 호출되는 객체의 경우 메모리 성능을 끌어올려줄 수 있다.

    이와 비슷한 기법으로 플라이웨이트 패턴이라는 것이 있다.

    플라이웨이트 패턴에 대해서 간략히 설명하면, String이 String Constant Pool을 사용하는 것처럼 클라이언트의 요청에 의해 객체를 생성할 때 생성하려는 객체가 Pool에 존재하면 반환만 해주고, 존재하지 않으면 생성 후 반환해주는 패턴 방식이다.

  • 이런 방식들을 다시 말해 인스턴스 통제 방식이라고 할 수 있다. 인스턴스 통제를 하는 이유는 클래스를 싱글톤 으로 만들 수 있고, 불변 클래스에서 동치인 인스턴스가 단 하나 뿐임을 보장(a == b일 때만 a.equals(b)가 성립) 할 수 있으며, 인스턴스화 불가로 만들 수 있다.

    열거 타입(Enum)의 경우 인스턴스가 하나만 만들어짐을 보장한다.

 

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

  • 이 능력에서 가장 중요한 것은 반환할 객체의 클래스를 자유롭게 선택할 수 있다는 것이다.

    public class MyBook{
      public MyBook(){}
      public static MyBook getChildInstance() {
        return MyBookChild.getInstance();
      }
    }
    
    public class MyBookChild extends MyBook{
      private MyBookChild(){}
      public static MyBookChild getInstance() {
        return new MyBookChild();
      }
    }
    • 소스에서 보이는 것처럼 반환할 객체의 타입을 자식의 타입으로 반환하듯 자유롭게 선택할 수 있는 것이다.
    • 이런 방법으로 인해, API를 만들 때 이런 유연성을 응용하면 구현 클래스(MyBookChild)를 공개하지 않고도 MyBook을 통해 구현 클래스를 반환할 수 있어 API와 소통이 가능하다. (클라이언트는 상위 클래스의 정적 팩토리만 접근하여 구현 클래스의 메서드를 호출할 수 있는 것이다.)

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

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

  • 예를 들어, EnumSet 클래스는 public 생성자 없이 오직 정적 팩토리 메소드만 제공하는데, 원소의 수에 따라 두 가지 하위 클래스 중 하나의 인스턴스를 반환한다.

    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);
    }
    • 소스에서 보는 바와 같이 원소의 개수가 64개 이하면 RegularEnumSet의 인스턴스를 반환하고, 원소의 개수가 65개 이상이면 JumboEnumSet 인스턴스를 반환하고 있다.
  • 클라이언트는 하위 클래스의 존재를 알 필요 없이 그저 반환해주는 클래스에게 메시지를 전송해 클래스의 반환 값만 받아 클라이언트가 설계한 로직으로 구성만 하면 되는 것이다.

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

  • 인터페이스나 클래스의 정적 팩터리 메서드가 만들어지는 시점에서 반환 타입의 클래스가 존재하지 않아도 된다는 것이다.

    public class MyBook{
      public static List<MyBookInterface> getInstance() {
        new ArrayList<>();
      }
    }
    
    public interface MyBookInterface{
      //이놈의 구현체는 아직 구현되지 않았다.
    }
    
    //1. 추 후 구현 클래스 생성
    public class MyBookImpl implements MyBookInterface {}
    //2. 클라이언트에서 활용
    public class client{
      public static void main(String [] args) {
        List<MyBookInterface> myBookImpls = MyBook.getInstance();
    
        //추 후에 구현한 클래스를 생성 후 List에 추가
        MyBookInterface myBookImpl = new MyBookImpl();
        myBookImpls.add(myBookImpl);
      }
    }
    • MyBook의 정적 팩터리 메서드의 반환할 객체의 클래스는 MyBookInterface의 구현 클래스인데 아직 구현되지 않은 것을 볼 수 있다. 즉, 추후의 정적 팩터리 메서드의 변경 없이 List에 구현된 클래스를 add하여 사용하면 되는 것이다.

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

  • 상속을 하기 위해서는 생성자가 필요없더라도 필수적으로 필요하다.
  • 이 제약으로 인해 상속보다 컴포지션(합성) 사용을 유도할 수 있고 불변 클래스(선택 인스턴스화)로 만들기 위해 해당 제약을 지켜야 한다는 점에서는 장점으로 받아들일 수 있다.

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

  • 생성자 처럼 API 설명에 명확히 드러나있지 않으므로 프로그래머는 정적 팩터리 메서드를 활용하여 클래스를 인스턴스화 할 방법을 알아내야 한다.

정적 팩터리 메서드의 명명

  • From : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
    • Date d = Date.from(instant);
  • of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
    • Set 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와 같으나 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다.
    • FileStore fs = Files.getFileStore(path);
  • newType : newInstance와 같으나 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다.
    • BufferedReader br = Files.newBufferedReader(path);
  • type : getType과 newType의 간결한 버전
    • List litany = Collections.list(legacyLitany);

이펙티브 자바 Effective Java 3/E
국내도서
저자 : 조슈아 블로크(Joshua Bloch) / 이복연(개앞맵시)역
출판 : 인사이트 2018.11.01
상세보기
반응형