SSAFY-Book-Study / modern-java-in-action

모던 자바 인 액션 북스터디입니다.
1 stars 10 forks source link

3 Weeks - [Static Factory Method 사용 이유] #55

Open Seongeuniii opened 1 year ago

Seongeuniii commented 1 year ago

문제

정적 팩토리 메서드는 왜 사용하는지, 언제 사용하면 좋은지 알고싶어요.

contents - 세부 내용

  1. Collectors 클래스가 정적 팩토리 메서드 패턴으로 구현된 이유에 대한 궁금증이 생겼습니다.
  2. "생성자 대신 정적 팩토리 메서드를 고려하라"는 말이 있습니다. 결국 둘 다 객체를 생성하는 역할을 하는데 어떤 점에서 우위에 있는지 궁금합니다.

참고

DeveloperYard commented 1 year ago

팩토리 패턴

팩토리 패턴은 자바에서 정말 많이 사용되는 디자인 패턴입니다. 이 패턴은 객체를 생성하는 패턴으로서 좋은 방법을 제시해줍니다.

객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스를 만들지는 서브 클래스에서 정하게 합니다.

먼저 구조를 살펴봅시다.

image

Shape 인터페이스를 정의하고, 서브 클래스들이 슈퍼 클래스인 Shape를 구현하게 됩니다. ShapeFactory라는 클래스를 생성하고, main class에서는 객체의 생성을 팩토리에게 맡깁니다.

각각 모양들의 코드입니다.

// Shape interface
public interface Shape {
   void draw();
}
// Rectangle class
public class Rectangle implements Shape {

   @Override
   public void draw() {
      System.out.println("Inside Rectangle::draw() method.");
   }
}
// Square class
public class Square implements Shape {

   @Override
   public void draw() {
      System.out.println("Inside Square::draw() method.");
   }
}
// Circle class
public class Circle implements Shape {

   @Override
   public void draw() {
      System.out.println("Inside Circle::draw() method.");
   }
}

각각의 모양들은 Shape에서 구현해야 할 draw라는 메서드를 상속받고, 각각의 모양들이 각자의 모양대로 draw 메서드를 오버라이딩하여 구현하게 됩니다.

다음으로는 ShapeFactory 클래스를 살펴봅시다.

// class ShapeFactory
public class ShapeFactory {

   //use getShape method to get object of type shape 
   public Shape getShape(String shapeType){
      if(shapeType == null){
         return null;
      }     
      if(shapeType.equalsIgnoreCase("CIRCLE")){
         return new Circle();

      } else if(shapeType.equalsIgnoreCase("RECTANGLE")){
         return new Rectangle();

      } else if(shapeType.equalsIgnoreCase("SQUARE")){
         return new Square();
      }

      return null;
   }
}

ShapeFactory 내에서는 getShape()라는 메서드를 가지는데, 이것이 팩토리로서 객체를 요청시 반환하게 됩니다. 내용을 살펴보면 분기처리를 통해 각 문자열이 들어왔을 때 검사하여 문자열에 해당하는 객체를 반환한다는 것을 알 수 있습니다.

public class FactoryPatternDemo {

   public static void main(String[] args) {
      ShapeFactory shapeFactory = new ShapeFactory();

      //get an object of Circle and call its draw method.
      Shape shape1 = shapeFactory.getShape("CIRCLE");

      //call draw method of Circle
      shape1.draw();

      //get an object of Rectangle and call its draw method.
      Shape shape2 = shapeFactory.getShape("RECTANGLE");

      //call draw method of Rectangle
      shape2.draw();

      //get an object of Square and call its draw method.
      Shape shape3 = shapeFactory.getShape("SQUARE");

      //call draw method of square
      shape3.draw();
   }
}

각 Shape 참조변수에 문자열을 넣고, getShape를 호출하게 되면 이에 해당하는 객체를 받게 됩니다. 이 때, main 클래스에서 shapeFactory 클래스의 getShape 메서드를 호출하지만, 그 내부에서는 어떤 일이 일어나는지 모르게 됩니다.

팩토리 패턴은 간단히 요약하자면 여러 개의 서브 클래스를 가진 슈퍼 클래스가 있을 때, input에 따라 하나의 자식 클래스의 인스턴스를 리턴해주는 방식이라고 볼 수 있겠습니다.

그래서 왜 팩토리 패턴이 왜 좋은 걸까요?

장점

정적 팩토리 메서드 vs. 생성자

그렇다면 팩토리 메서드 패턴과 생성자, 둘 다 객체를 반환해주는 것인데 어떤게 좋고 어떤게 나쁜 걸까요?

결론부터 말씀드리자면, 정적 팩토리 메서드의 경우 단순히 생성자를 대신하는 것 뿐만 아니라 우리가 조금 더 가독성 좋은 코드를 작성하고, 객체지향적으로 프로그래밍할 수 있도록 도와줍니다.

정적 팩토리 메서드의 장점을 살펴보겠습니다.

1. 이름 부여 가능

public class LottoFactory() {
  private static final int LOTTO_SIZE = 6;

  private static List<LottoNumber> allLottoNumbers = ...; // 1~45까지의 로또 넘버

  public static Lotto createAutoLotto() {
    Collections.shuffle(allLottoNumbers);
    return new Lotto(allLottoNumbers.stream() // 새로운 로또 번호 생성
            .limit(LOTTO_SIZE) // 로또 번호 개수만큼 사이즈를 제한
            .collect(Collectors.toList())); // toList() 메서드를 활용해 스트림을 리스트로 변환
  }

  public static Lotto createManualLotto(List<LottoNumber> lottoNumbers) {
    return new Lotto(lottoNumbers); // 기본 생성자의 경우 어떠한 방법으로 로또 번호가 생성되는지 알 수 없다.
  }
  ...
}

다음의 코드는 자동 로또와 수동 로또를 생성하는 팩토리 클래스의 일부 코드입니다.

정적 팩토리인 createAutoLotto()의 경우 이름을 보면 명확하게 자동으로 섞인 로또를 반환해주는 것을 알 수 있습니다. 반면, 기본 생성자로 로또 번호를 받는다면 이게 수동인지, 자동인지 알 수 있는 방법이 없습니다.

정적 팩토리 메서드는 어떤 목적으로 객체를 생성하는지, 객체의 역할이나 의도를 명확히 알 수 있습니다. 또한 이를 통해 가독성도 확보할 수 있습니다.

2. 호출 시마다 새로운 객체를 생성할 필요가 없습니다.

public class LottoNumber {
  private static final int MIN_LOTTO_NUMBER = 1;
  private static final int MAX_LOTTO_NUMBER = 45;

  private static Map<Integer, LottoNumber> lottoNumberCache = new HashMap<>();

  static {
    IntStream.range(MIN_LOTTO_NUMBER, MAX_LOTTO_NUMBER)
                .forEach(i -> lottoNumberCache.put(i, new LottoNumber(i)));
  }

  private int number;

  private LottoNumber(int number) {
    this.number = number;
  }

  public LottoNumber of(int number) {  // LottoNumber를 반환하는 정적 팩토리 메서드
    return lottoNumberCache.get(number);
  }

  ...
}

Enum과 같이 자주 사용되는 요소의 개수가 정해져있다면 해당 개수만큼 미리 생성해놓고 캐싱할 수 있는 구조로 만들 수 있습니다. 위의 코드의 경우 static 블록에서 로또 번호의 수만큼 로또 숫자의 캐시에 넣어놓고, of라는 스태틱 메서드로 숫자들을 빼내올 수 있습니다. 이 때 객체의 캐싱을 통해서 객체를 생성하지 않을 뿐 아니라, 객체 생성자를 private으로 설정함으로써 객체 생성을 정적 팩토리 메서드로만 가능케 할 수 있습니다.

3. 하위 자료형 반환

public class Level {
  ...
  public static Level of(int score) {
    if (score < 50) {
      return new Basic();
    } else if (score < 80) {
      return new Intermediate();
    } else {
      return new Advanced();
    }
  }
  ...
}

Level 클래스의 정적 팩토리 메서드인 of는 점수에 따라 분기처리를 이용해 서로 다른 하위 자료형 객체의 반환을 이끌어내고 있습니다. 하위 자료형 객체의 반환은 어떤 점이 좋을까요?

다음과 같습니다.

정적 팩토리 메서드의 네이밍 컨벤션

from : 하나의 매개 변수를 받아서 객체를 생성.
of : 여러 개의 매개 변수를 받아서 객체를 생성.
getInstance | instance : 인스턴스를 생성, 이전에 반환했던 것과 같을 수 있다.(싱글톤 패턴의 경우)
newInstance | create : 새로운 인스턴스를 생성.
get[OtherType] : 다른 타입의 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있다.
new[OtherType] : 다른 타입의 새로운 인스턴스를 생성.

Reference