ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 1. Consider static factory methods instead of constructors
    독서/Effective Java 2021. 12. 4. 18:21

    인스턴스를 생성하는 다양한 방법들

    • 생성자 사용
    • 빌더 사용(추후 아이템에 나온다.)
    • 정적 팩토리 메서드 사용

     

    정적 팩토리 메서드란?

    해당 클래스의 인스턴스를 반환하는 메소드라고 보면 된다.

    Ex) boolean(primitive) 타입 -> Boolean(Object) 타입으로 변환

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

    정적 팩토리 메소드 != 팩토리 메소드 패턴

    정적 팩토리 메서드는 디자인 패턴에 직접적으로 대응되는 것이 없다.

     

    정적 팩토리 메소드를 사용했을 때 생성자보다 나은 장점들

     

    (1) 이름을 가질 수 있다.

    생성자에 대한 parameter가 그 자체로 반환되는 객체를 설명하지 않는 경우 이름이 잘 선택된 정적 팩토리를 사용하는 것이 코드 이해에 더 유리하다.

    Ex) User에 새로운 자식이 태어나는 경우와 귀화하는 경우를 분리해서 생성하게 하는 예. 생성자로만 해당 사용자를 추가할 경우 의미가 온전히 전달되지 않을 수 있다.

    public class User {
    
      private static final int FIRST_AGE = 1;
    
      private final String firstName;
    
      private final String lastName;
    
      private int age;
    
      private User(String firstName, String lastName, int age){
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
      }
    
      public static User newBaby(String firstName, User father){
        return new User(firstName, father.lastName, FIRST_AGE);
      }
    
      public static User immigration(String firstName, String lastName, int age){
        return new User(firstName, lastName, age);
      }
    
      public String getFirstName() {
        return firstName;
      }
    
      public String getLastName() {
        return lastName;
      }
    }
    
    // Test
    class UserTest {
    
      private User father;
    
      @BeforeEach
      void setup(){
        father = User.immigration("boris", "Chak", 32);
      }
    
      @Test
      @DisplayName("귀화하는 경우")
      void immigrationTest(){
        Assertions.assertThat(father.getLastName()).isEqualTo("Chak");
      }
    
      @Test
      @DisplayName("자손이 태어나는 경우")
      void birthTest(){
        User son = User.newBaby("sonas", father);
        Assertions.assertThat(son.getLastName()).isEqualTo("Chak");
      }
    }

     

    (2) 호출 때마다 새 객체를 생성할 필요가 없다.

    정적 팩토리 메서드에서 해당 인스턴스의 생성 여부를 결정한다면, 불변 클래스(Item17)로 미리 구성된 인스턴스를 사용하거나 인스턴스가 생성될 때 인스턴스를 캐시하고 반복적으로 분배하여 불필요한 중복 객체를 생성하지 않도록 할 수 있다. 

    대표적인 예로는 위의 boolean -> Boolean의 인스턴스 생성이 있다.

    또 다른 예는 싱글톤 패턴에서 확인할 수 있다.

    public class ImmigrationBorder {
    
      private static ImmigrationBorder immigrationBorder;
    
      private ImmigrationBorder() {
      }
    
      public static ImmigrationBorder getInstance(){
        if(immigrationBorder == null){
          immigrationBorder = new ImmigrationBorder();
        }
    
        return immigrationBorder;
      }
    }

    해당 클래스에서 이민국은 getInstance를 첫 호출할 경우에만 생성되고, 이후에는 인스턴스를 계속 static으로 가지고 있다가 반환해주는 역할만 하게 된다. 이민국은 단 하나만 존재한다는 규칙이 있다면, 해당 방법을 통해 인스턴스를 유지할 수 있는 것이다.

    추가적으로, 해당 방식을 통해 인스턴스의 생성/사용에 대한 권한을 해당 클래스가 가지고 있을 경우(하나의 클래스에서 해당 종류의 인스턴스들을 관리), 해당 Object를 공유해 사용하는(생성, 공유를 하나의 클래스에서만 수행) 플라이 웨이트 패턴의 기본이 된다.

     

    (3) 반환 형식의 모든 하위 형식 개체를 반환할 수 있다.

    예제로 확인해보자.

    // 과일에 따른 세부 과일을 반환해주는 정적 팩토리 메서드
    public static Fruit buy(String fruitName){
      if(fruitName.equals(FruitType.APPLE.fruitName())){
     	 return new Apple();
      } else if(fruitName.equals(FruitType.BANANA.fruitName())){
     	 return new Banana();
      } else {
     	 return new DefaultFruit();
      }
    }
    
    public class Apple extends Fruit {}
    public class Banana extends Fruit{}
    public class DefaultFruit extends Fruit{}
    
    enum FruitType {
      APPLE("apple"), BANANA("banana");
    
      private final String name;
    
      FruitType(String name) {
        this.name = name;
      }
    
      public String fruitName(){
        return name;
      }
    }

    Fruit에서 buy라는 정적 팩토리 메서드를 사용할 경우, 사용자는 Fruit 종류가 어떤 게 있는지, 어떤 Fruit 인스턴스를 사용할 지 고민할 필요 없이 그냥 호출만 하면 사용이 가능하다. 이 말인 즉, 정적 팩토리 메서드를 사용할 경우 반환 객체의 클래스를 자유롭게 선택할 수 있다.

    추가로, 인터페이스로 정적 팩터리 메소드를 구현하면 위임받아 생성하는 각 클래스에서 다형성을 가진 객체를 제공해줄 수 있습니다.

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

     

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

    위의 예시를 다시 보자

    // 과일에 따른 세부 과일을 반환해주는 정적 팩토리 메서드
    public static Fruit buy(String fruitName){
      if(fruitName.equals(FruitType.APPLE.fruitName())){
     	 return new Apple();
      } else if(fruitName.equals(FruitType.BANANA.fruitName())){
     	 return new Banana();
      } else {
     	 return new DefaultFruit();
      }
    }
    
    public class Apple extends Fruit {}
    public class Banana extends Fruit{}
    public class DefaultFruit extends Fruit{}
    
    enum FruitType {
      APPLE("apple"), BANANA("banana");
    
      private final String name;
    
      FruitType(String name) {
        this.name = name;
      }
    
      public String fruitName(){
        return name;
      }
    }

    정적 팩토리 메서드를 활용했을 때의 4번째 장점은 입력 매개변수에 따라 return object가 달라지도록 구현할 수 있다는 점이다.

    위에서 buy 메서드의 fruitName은 그 자체로 fruit의 return type을 결정하는 변수가 되어 활용된다.

     

    (5) 메서드를 포함하는 클래스가 작성될 때 반환된 객체의 클래스가 존재할 필요가 없다

    정적 팩토리의 다섯 번째 장점은 메서드를 포함하는 클래스가 작성될 때 반환된 객체의 클래스가 존재할 필요가 없다는 것입니다.

    또 다시 위의 예시를 들어보자... 만약 포도라는 과일이 추가된다면?

    그저 FruitType에 GRAPE라는 타입을 추가(굳이 할 필요 없음)하고, buy란 메서드에서 grape를 생성해서 반환해주는 부분만 추가해주면 될 뿐이다. 그 말인 즉, 어차피 상위의 클래스/인터페이스를 반환 타입으로 지정했기 때문에 하위의 클래스는 굳이 생성 시점에 고려하지 않아도 된다는 점이다. 

    public static Fruit buy(String fruitName){
      if(fruitName.equals(FruitType.APPLE.fruitName())){
     	 return new Apple();
      } else if(fruitName.equals(FruitType.BANANA.fruitName())){
     	 return new Banana();
         // buy 메서드가 만들어질 때에는 존재하지 않았던 포도 return 부분
      } else if(fruitName.equals(FruitType.GRAPE.fruitName())){
      	 return new Grape();
      }else {
     	 return new DefaultFruit();
      }
    }
    
    // 나중에 추가된 포도 클래스
    public class Grape extends Fruit {}
    
    enum FruitType {
      APPLE("apple"), BANANA("banana"), GRAPE("grape");
    
      private final String name;
    
      FruitType(String name) {
        this.name = name;
      }
    
      public String fruitName(){
        return name;
      }
    }

     

     

    정적 팩토리 메소드를 사용했을 때의 제한되는 점/단점

    그렇다면 정적 팩토리 메소드를 사용하면 무조건 장점만 있을까?

     

    (1) 정적 팩토리 메서드만 제공할 경우 public 또는 protected 생성자가 없는 클래스는 하위 클래스화할 수 없다.

    예를 들어 Collections Framework에서 편의 구현 클래스를 하위 클래스로 만드는 것은 불가능하다.

    Collections의 생성자가 "private"로 선언되어 있기 때문이다(상속 불가능)

    대신에, Inheritance 대신 Composition을 사용하도록 장려하고(Item 18) 불변 타입으로 생성하려면 이 조건이 필요하기 때문에(Item 17) 오히려 장점이 될 수 있다.

     

    (2) 프로그래머가 찾기 어렵다.

    어떤 라이브러리를 사용하기 위해 API Docs를 보다보면 정적 팩터리 메서드에 대해서 확인하기가 쉽지 않다(일반 메서드로 보이기 때문).

     

    정적 팩터리 메서드 명명 방식

    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와 유사하지만 팩토리 메소드가 다른 클래스에 있는 경우에 사용됨.

    FileStore fs = Files.getFileStore(path);

    • newType - newInstance와 비슷하지만 팩토리 메서드가 다른 클래스에 있는 경우에 사용됨.

    BufferedReader br = Files.newBufferedReader(path);

    • type - getType 및 newType에 대한 간결한 버전

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

     

    정리

     

    요약하면, 정적 팩토리 메소드와 공용 생성자는 모두 용도가 존재하니 상황에 맞춰 사용하자.

    종종 정적 팩토리가 선호되므로 먼저 정적 팩토리를 고려하지 않고 공용 생성자를 제공하려고 하지 말자.

    댓글

Designed by Tistory.