Design Pattern

[Design Pattern] Factory Method Pattern (팩토리 메서드 패턴)

ilot 2022. 11. 13. 22:34

팩토리 메서드 패턴 (Factory Method Pattern)

  • 구체적으로 어떤 인스턴스를 만들지는 서브클래스가 정한다.
  • 인스턴스를 생성하는 책임을 구체 클래스가 아닌 인터페이스의 메서드로 감싸게 된다.
  • 다양한 구현체(Product)가 있고, 그 중에서 특정한 구현체를 만들 수 있는 다양한 팩토리(Creator)를 제공할 수 있다.

팩토리 메서드 패턴

장점

  • 기존에 인스턴스 생성 코드를 건드리지 않고 비슷한 류의 인스턴스 생성 코드를 만들 수 있어 OCP를 보장한다. (Creator와 Product의 느슨한 결합)

단점

  • 클래스가 많아진다.

예시 (as-is)

public class Ship {

  private String name;

  private String color;

  private String logo;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getColor() {
    return color;
  }

  public void setColor(String color) {
    this.color = color;
  }

  public String getLogo() {
    return logo;
  }

  public void setLogo(String logo) {
    this.logo = logo;
  }

  @Override
  public String toString() {
    return "Ship{" +
        "name='" + name + '\'' +
        ", color='" + color + '\'' +
        ", logo='" + logo + '\'' +
        '}';
  }
}

public class ShipFactory {

  public static Ship orderShip(String name, String email) {
    // validate
    if (name == null || name.isBlank()) {
      throw new IllegalArgumentException("배 이름을 지어주세요.");
    }
    if (email == null || email.isBlank()) {
      throw new IllegalArgumentException("연락처를 남겨주세요.");
    }

    prepareFor(name);

    Ship ship = new Ship();
    ship.setName(name);

    // Customizing for specific name
    if (name.equalsIgnoreCase("whiteship")) {
      ship.setLogo("\uD83D\uDEE5️");
    } else if (name.equalsIgnoreCase("blackship")) {
      ship.setLogo("⚓");
    }

    // coloring
    if (name.equalsIgnoreCase("whiteship")) {
      ship.setColor("whiteship");
    } else if (name.equalsIgnoreCase("blackship")) {
      ship.setColor("black");
    }

    // notify
    sendEmailTo(email, ship);

    return ship;
  }

  private static void prepareFor(String name) {
    System.out.println(name + " 만들 준비 중");
  }

  private static void sendEmailTo(String email, Ship ship) {
    System.out.println(ship.getName() + " 다 만들었습니다.");
  }

}

public class Client {

  public static void main(String[] args) {
    Ship whiteship = ShipFactory.orderShip("Whiteship", "keesun@mail.com");
    System.out.println(whiteship);

    Ship blackship = ShipFactory.orderShip("Blackship", "keesun@mail.com");
    System.out.println(blackship);
  }

}

orderShip은 name과 color에 따라 로직이 변경된다.

Ship에 신규 특성이 추가되면, (Yellowship, ...) orderShip 로직이 계속 변경되어 OCP에 어긋나게 된다. 변경에 닫혀있지 않게 된다.

예시 (to-be)

public class Ship {

  private String name;

  private String color;

  private String logo;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getColor() {
    return color;
  }

  public void setColor(String color) {
    this.color = color;
  }

  public String getLogo() {
    return logo;
  }

  public void setLogo(String logo) {
    this.logo = logo;
  }

  @Override
  public String toString() {
    return "Ship{" +
        "name='" + name + '\'' +
        ", color='" + color + '\'' +
        ", logo='" + logo + '\'' +
        '}';
  }

}

public Whiteship extends Ship {

  public Whiteship() {
    setName("whiteship");
    setLogo("\uD83D\uDEE5️");
    setColor("white");
  }

}

public Blackship extends Ship {

  public Blackship() {
    setName("blackship");
    setLogo("⚓");
    setColor("black");
  }

}

public interface ShipFactory {

  default Ship orderShip(String name, String email) {
    validate(name, email);
    prepareFor(name);
    Ship ship = createShip();
    sentEmailTo(email, ship);
    return ship;
  }

  Ship createShip();

  private void validate(String name, String email) {
    // validate
    if (name == null || name.isBlank()) {
      throw new IllegalArgumentException("배 이름을 지어주세요.");
    }
    if (email == null || email.isBlank()) {
      throw new IllegalArgumentException("연락처를 남겨주세요.");
    }
  }

  private void prepareFor(String name) {
    System.out.println(name + " 만들 준비 중");
  }
  
  private void sendEmailTo(String email, Ship ship) {
    System.out.println(ship.getName() + " 다 만들었습니다.");
  }

}

public class WhiteshipFactory implements ShipFactory {

  @Override
  public Ship createShip() {
    return new Whiteship();
  }

}

public class BlackshipFactory implements ShipFactory {

  @Override
  public Ship createShip() {
    return new Blackship();
  }

}

public class Client {

  public static void main(String[] args) {
    Ship whiteship = new WhiteshipFactory.orderShip("Whiteship", "keesun@mail.com");
    System.out.println(whiteship);

    Ship blackship = new BlackshipFactory.orderShip("Blackship", "keesun@mail.com");
    System.out.println(blackship);
  }

}

WhiteShip이 있는 와중에, BlackShip을 추가하려고 해도 BlackshipFactory와 Blackship만 생성하면 되기 때문에 WhiteshipFactory와 Whiteship에 코드 변경을 요하지 않는다. OCP를 충족시킨 코드가 되었다.

 

예시 (to-be) : Client에 DI 적용하기

// ...

public class Client {

  public static void main(String[] args) {
    Client client = new Client();
    client.print(new WhiteshipFactory(), "whiteship", "keesun@mail.com");
    client.print(new BlackshipFactory(), "blackship", "keesun@mail.com");
  }

  // shipFactory를 메서드 파라미터로 의존성 주입 (DI)
  private void print(ShipFactory shipFactory, String name, String email) {
    System.out.println(shipFactory.ordership(name, email));
  }

}

활용

  • 단순한 팩토리 패턴
    • 매개변수의 값에 따라 또는 메서드에 따라 각기 다른 인스턴스를 리턴하는 단순한 버전의 팩토리 패턴
    • java.lang.Calendar 또는 java.lang.NumberFormat
  • 스프링의 BeanFactory
    • Object 타입의 Product를 만드는 BeanFactory라는 Creator이고, 그것에 대한 구현체가 ClassPathXmlApplicationContext, AnnotationConfigApplicationContext이다.
    • Product는 Object이고, getBean시 가져오는 값이 ConcreteProduct가 된다.

 

출처
https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4