[effective-java] item1 생성자 대신 정적 팩토리 메소드를 고려하라

차_현·2023년 7월 31일
1

생성자 대신 정적 팩토리 메소드를 고려하라.

들어가기전

생성자는 알겠는데 정적 팩토리 메소드 대신 쓰라고???

그럼 대신 쓰라 했으니 뭐 장점과 단점이 있지 않을까?


정적 팩토리 메소드란?

클래스의 인스턴스를 반환하는 단순한 정적 메소드

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

이 코드는 원시 타입 boolean의 박싱 클래스(Boolean)의 메소드를 나타낸 것인데,

원시 타입 boolean을 인자로 받아서 Boolean 객체 참조로 변환해준다.


정적 팩토리 메소드의 장점

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

오로지 생성자 자체만 으로는 반환될 객체의 특성을 직관적으로 바로 알기는 힘들다.(생성자의 이름이 클래스의 이름과 동일하기 때문 아닐까...?)


하지만 정적 팩토리 메소드를 고려해본다면? 이름만 잘 지으면 반환될 객체가 무엇 인지 대략적으로 알 수 있다.
1) BigInteger(int,int,Random)
2) BigInteger.probablePrime

솔직하게 이 둘중에 어느 것이 '값이 소수인 BigInteger를 반환한다' 라는 의미를 더 가지고 있다고 생각하냐는 물어볼 필요?가 없을지도 모른다.
당연히 2번이다. probablePrime의 영어 뜻만 알고 있다면, 직관적으로 그 뜻을 바로 알 수 있다.

추가로, 하나의 시그니처(parameter의 타입,갯수,순서를 포함)로는 생성자를 하나만 만들 수 있다.

public class Person {
    private String name;

    // 생성자 1
    public Person(String name) {
        this.name = name;
    }

    // 컴파일 오류! 생성자 1과 시그니처가 동일하기 때문에
    public Person(String nameRepeated) {
        this.name = nameRepeated;
    }
}

위의 코드는 동일한 시그니처가 두개 있기 때문에, 불가능하다.
하지만, 앞서 말했듯 정적 팩토리 메소드는 이런 제약이 없다.

왜냐하면 이렇게 뭔가 시그니처가 동일한 생성자가 필요할 것 같으면
생성자를 정적 팩토리 메소드로 바꾼뒤, 그 메소드의 이름만 보고 직관적으로 특성을 알 수 있게 이름을 지어주면 되기 때문이다.

이렇게->

public class Person {
    private String name;
    private String country;
    
    private Person() { }

    private Person(String name) {
        this.name;
    }
    public static Person withName(String name) {
        Person person = new Person(name);
        return person;
    }
    public static Person withCountry(String country) {
        Person person = new Person(country);
        return person;
    }
    public static Person withNameAndCountry(String name, String country) {
        Person person = new Person(name);
        person.country = country;
        return person;
    }
}

정적 팩토리 메소드를 사용하지 않고 한 클래스 내에 동일한 시그니처를 가지고 있는 생성자를 만들게 된다면
오류가 발생한다.

그래서 한 클래스에 시그니처가 같은 생성자가 여러개가 있어야 할 것 같으면, 생성자를 정적 팩토리 메소드로 바꾸고
각각의 특성이 들어나게 메소드 이름을 정하면 된다.


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

이 장점으로는 Immutable class(불변 클래스)는 인스턴스를 미리 만들어 놓거나
자주 사용하는 인스턴스를 캐싱하여 불필요한 객체 생성을 피해 성능을 개선할 수 있다는데....

Boolean.valueOf(boolean) 메소드는 객체를 아예 생성하지 않는다고 한다.
그래서 Boolean.java와 그 안의 valueOf메소드를 한번 보겠다.

public final class Boolean implements java.io.Serializable, Comparable<Boolean>, Constable {
    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' 객체 참조로 변환해주기 때문에

그리고 Boolean.valueOf(boolean b) 메소드는 객체를 아예 생성하지 않는다.

boolean 타입의 매개변수를 받아 Boolean 객체를 반환하는데,

인자가 true이면 이미 생성 되어 있는 Boolean.TRUE를 재사용하고

인자가 false이면 이미 생성 되어 있는 Boolean.FALSE를 재사용한다.

이렇게 되면 아까 말한대로 객체를 매번 생성하지 않아 불필요한 객체 생성을 피하고,
메모리 사용도 최적화 할 수 있다.

또 다른 예시를 찾아봤다.

import java.util.stream.IntStream;
import java.util.HashMap;

public class Number {
    private static final int MAX_NUMBER = 100;
    private static final int MIN_NUMBER = 1;
    
    private final int number;

    private Number(int number) {
        this.number = number;
    }
    
    //Number인스턴스를 반환하는 정적 팩토리 메소드
    public static Number valueOf(final int number) {
        Number getNumber = mapCache.get(number);
        return getNumber;
    }
    
    private static Map<Integer, Number> mapCache = new HashMap<>();

    //99개의 Number인스턴스를 미리 만들어둠.
    static {
        IntStream.range(MIN_NUMBER, MAX_NUMBER)
                .forEach(n -> mapCache.put(n, new Number(n)));
    }
    ...
}

반복되는 요청에 같은 객체를 반환하는 식으로, 정적 팩토리 방식의 클래스는 언제 어느 인스턴스를
살아 있게 할지를 철저히 통제할 수 있다.이를 '인스턴스 통제 클래스' 라고 한다.

  • 반복되는 요청에 같은 객체를 반환하는 식으로~ : 동일한 요청에 대해 매번 새로운 객체를 생성하지 않고 이미 생성한 객체를
    재사용 하겠다라는 뜻이다. 성능을 향상시키고 메모리 사용량을 줄일 수 있다. 이러한 패턴은 자주 사용되는
    객체를 미리 생성해둘때 효율적일 것같다.

  • 정적 팩토리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있다 :
    클래스가 인스턴스의 생성과 소멸을 철저하게 관리하고, 필요할 때만 인스턴스를 생성하고 필요하지
    않게 되면 소멸시키겠다는 것이다. 그로써 인스턴스의 수명 주기를 효율적으로 관리하고,불필요한 메모리 사용을
    방지할 수 있다.


그러면 인스턴스 통제를 하면 뭐가 좋아?
1. 싱글톤이 가능하다(item 4)

  • 생성자를 private으로 막아버리고, 메소드를 통해서 인스턴스를 제공할 수 있다.
public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() { 
        
    }
    public static Singleton getInstance() {
        return INSTANCE;
    }
}
  1. 인스턴스화 불가로 만들 수 있다(item 4)
  • 상속 금지 클래스나 정적 필드 만 담은 클래스의 인스턴스화를 막아버리기 위해서 private으로 생성자를 막아버리면 된다.
  1. 불변 값 클래스(item 17)에서 동치인 인스턴스가 단 하나뿐 임을 보장 할 수 있다.
  • 동치 인스턴스란 a==b 일때만 a.equals(b)가 성립하는 것을 말한다.-> 같은 객체를 참조한다.
  • 위에서 봤던 Boolean.valueOf메소드 예제도 이에 성립한다
    • Boolean.valueOf(true) 메소드를 호출할면 항상 Boolean.TRUE 객체를 반환한다.

      이 메소드를 여러번 호출해도,항상 같은 Boolean.TRUE 객체를 반환한다.

      따라서, Boolean.TRUE==Boolean.valueOf(true)는 항상 true이다.
  1. 열거 타입은 인스턴스가 하나만 만들어 짐을 보장한다
  • 열거 타입에서 선언한 상수가 해다 열거 타입의 오직 하나의 인스턴스이다. public static final이기 때문에 어디서든
    이 상수에 접근할 수 있지만, 상수의 값을 변경할 수 없다. 추가로 열거 타입의 인스턴스를 직접 new를 이용하여
    만들 수 없는 이유는 각 상수가 이미 열거 타입의 인스턴스로 생성되어 존재하기 때문이다.
public enum Day {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    ...
}

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

이는 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하여 엄청나게 유연한 코딩을 지향하게 해준다.
특히 API를 만들때 이를 활용하여 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 무겁지 않게 API를 작게 유지할 수 있다.

인터페이스를 정적 팩토리 메소드의 반환 타입으로 사용
원랜 자바8 이전에는 아래와 같이 인터페이스에 정적 메소드를 선언할 수 없었다. 그렇기 때문에,예를 들어 Sport라는 인터페이스를
반환하는 정적 메소드가 필요하면, Sports라는(인스턴스화 불가인) 동반 클래스를 만들어 이 클래스 안에 정적 메소드를 정의하였다.

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이지만,

반환하고 있는 것은 Type 인터페이스를 구현하고 있는 구현 클래스(AType,BType)이다.

아직 뭐가 장점인지는 잘 모르겠지만, 또 다른 예제를 보면서 이해해보자...

public interface Pet {
  void makeSound();
}

public class Dog implements Pet {
  @Override
  public void makeSound() {
    System.out.println("왈왈");
  }
}
public class Cat implements Pet {
  public void makeSound() {
    System.out.println("야옹");
  }
}

여기서 만약에 Pet 인터페이스에 정적 팩토리 메소드를 추가하면, 클라이언트가 어떤 동물을 만들고 싶어하는지를 정확하게 파악할 수 있다.

public interface Pet {
  void makeSound();

  static Pet createDog() {
    return new Dog();
  }
  static Pet createCat() {
    return new Cat();
  }
}

public class Main{
  public static void main(String[] args) {
    Pet dog = Pet.createDog();
    dog.makeSound(); // 왈왈
    
    Pet cat = Pet.createCat();
    cat.makeSound(); // 야옹
  }
}

추가로, 자바 컬렉션 프레임워크는 핵심 인터페이스들에 수정 불가나 동기화 등의 기능을 덧붙인 총 45개의 유틸리티 구현체를 제공하는데,
이 구현체 대부분을 단 하나의 인스턴스화 불가 클래스의 java.util.Collections에 정적 팩토리 메소드를 통해 얻도록 했다.

먼저, Collection.java를 보면,

public class Collections {
    // Suppresses default constructor, ensuring non-instantiability.
    private Collections() {
    }
}
...

이렇게 생성자가 private으로 선언되어 인스턴화를 막고 있다. 대신에, 정적 팩토리 메소드를 통해서 컬렉션에 대한
다양한 연산을 수행할 수 있다. 이런 메소드들은 기존 컬렉션을 입력으로 받아서 '수정을 불가능'하게 만들거나 '동기화' 하는 등의
작업을 수행한 후에 완전히 새로운 컬렉션을 반환한다.
수정 불가능의 예시 코드와 동기화에 대한 예시 코드를 한번 봐보자.

import java.util.ArrayList;
import java.util.Collection;
<수정 불가능 예시 코드>
public static<T> Collection<T> unmodifiableCollection(Collection<?extends T> c){}
...
public class AnotherClass {
  public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("A");
    list.add("B");
    list.add("C");

    //리스트를 수정 불가능하게 만드는 메소드 호출
    List<String> unmodifiedListOfCollections = Collection.unmodifiableList(list);
  }
}

unmodifiableList는 수정이 불가능해졌다.

즉,이 리스트에 아이템을 추가하거나 삭제하는 작업은 UnsupportedOperationException을 발생시키게 된다.\

import java.util.ArrayList;
import java.util.Collections;
<Thread-Safe 예시 코드>
public static <T> List<T> synchronizedList(List<T> list) { }
...
public class AnotherClass2 {
  public static void main(String[] args) {
    List<String> list2 = new ArrayList<>();
    list2.add("가");
    list2.add("나");
    list2.add("다");

    //리스트를 thread-safe 하게 만듬
    List<String> list2 = Collections.synchronizedList(list2);
  }
}

synchronizedList는 여러 Thread에서 동시에 접근하더라도 데이터 일관성을 유지할 수 있게 해준다.

결론은, java.util.Collections는 이러한 기능들을 제공하는 Utility클래스로, 컬렉션을 다루는 다양한 메소드를 제공한다.
이러한 메소드들은 정적 메소드들이므로 Collections 클래스르 인스턴스화 하지 않고도 사용할 수 있다는게 핵심이다.


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

약간 장점 3번과 통하는 내용인 것 같다.

반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다. 심지어 다음 릴리스에서 또 다른 클래스의 객체를 반환해도 된다.

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) 정적 팩토리 메소드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

이 말은 정적 팩토리 메소드가 유연성을 제공한다는 소리이다.
인터페이스나 클래스가 만들어지는 시점에서 하위 타입의 클래스가 존재하지 않아도 나중에 만들 클래스가 기존의
인터페이스나 클래스르 상속 받으면 언제든지 주입 받아서 사용할 수 있다. 반환값이 인터페이스가 되며 정적 팩토리 메소드 변경 없이
구현체를 갈아 끼울 수 있다.

java.util.ServiceLoader 클래스를 보자.

ServiceLoader는 서비스 제공자 인터페이스의 구현체를 로드하는 역할을 한다. 서비스 제공자 인터페이스는 일종의 플러그인 역할을 하는 인터페이스로, 여러 구현체가 존재할 수 있다.

ServiceLoader.load() 메소드는 이러한 인터페이스의 구현체를 로드하는 정적 팩토리 메소드이다.

ServiceLoader<SomeService> services = ServiceLoader.load(SomeService.class);

이 코드에서 'SomeService'는 서비스 제공자 인터페이스이다.

ServiceLoader.load() 메소드는 이 인터페이스를 구현하는 모든 클래스를 로드하여 'ServiceLoader' 객체를 반환한다.

여기서 'ServiceLoader.load()' 메소드를 작성하는 시점에 'SomerService'를 구현하는 모든 클래스가
존재할 필요는 없다.
이 메소드가 실행되는 시점에 적합한 클래스를 로드할 수 있다.

따라서 정적 팩토리 메소드를 사용하면 어떤 클래스를 반환할지에 대한 결정을 런타임 시점으로 미룰 수 있기 때문에 유연하게 대응할 수 있다.


정적 팩토리 메소드의 딘점

장점이 있으면, 단점도 있겠다. 단점을 한번 알아보자.

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

앞에서도 있었지만 상기시킬겸, 다시 작성해보자면,
정적 팩토리 메소드를 사용하는 클래스는 종종 생성자를 private으로 선언하여 외부에서 클래스의 인스턴스를 직접 생성하는 것을 방지한다.
이렇게 하면, 클래스의 인스턴스 생성을 정적 팩토리 메소드를 통해서만 할 수 있게 된다.
즉, new 키워드를 통해서 인스턴스를 생성하는 것이 아니라, 클래스가 제공하는 정적 팩토리 메소드를 통해 인스턴스를 얻게 된다.

public class Singleton {
  private static final Singleton INSTANCE = new Singleton();

  private Singleton() {
  }

  public static Singleton getInstance() {
    return INSTANCE;
  }
}

위에서 저 instance변수가 static에 final로 선언되어 있으니 정적 팩토리 메소드를 통해서만 인스턴스를 얻을 수 있고,

한 번만 생성된 Singleton의 인스턴스를 이후로는 재사용한다고 했었다.

다시 돌아와서, 장점 부분에서 작성했던 컬렉션 프레임워크의 유틸리티 구현 클래스들은 상속할 수 없다는 얘기다.
상속을 하기 위해서는 public,protected 생성자가 필요하다 -> 그래야 하위 클래스에서 super()를 써서 부모 클래스의 생성자를 호출할 수 있으니까.


단점2) 정적 팩토리 메소드는 프로그래머가 찾기 어렵다

정적 팩토리 메소드는 생성자처럼 인스턴스에 API 설명에 명확히 드러나지 않아서 자신이나 다른 프로그래머가 찾기 어렵다.

그래서 혼란을 줄이기 위해 흔히 사용되는 네이밍을 몇 가지 사용한다.

< 정적 팩토리 메소드 명명 규칙>

  • 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 : (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다. But, 싱글톤 패턴에서는 항상 같은 인스턴스를 반환하도록 구현하기도 함.
    • StackWalker luke = StackWalker.getInstance(options);
  • create 혹은 newInstance : instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
    • Object newArray = Array.newInstance(classObject, arrayLen);
  • getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다. "Type"은 팩토리 메소드가 반환할 객체의 타입이다.
    • FileStore fs = FileStore.getFileStore(path);
  • newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메스드를 정의할 때 쓴다. "Type"은 팩토리 메소드가 반환할 객체의 타입이다.
    • FileStore fs = FileStore.getFileStore(path);
  • type : getType과 newType의 간결한 버전
    • List<Complaint> litany = Collections.list(legacyLitany);

정리

정적 팩토리 메소드와 public 생성자(new 키워드 사용)는 각각 장단점이 있기 때문에
잘 이해하고 사용하여야 한다. 앞으로 무분별한 public생성자 사용을 지양해보겠다.

0개의 댓글