본문 바로가기
CS/Design Pattern

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

by I move forward every day. 2023. 8. 9.

해당 글은 Java 8에 default 메서드가 추가된 이후 interface를 사용해 팩토리 메서드를 구현하는 것을 기준으로 작성된 글입니다.

 

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

팩토리 패턴은 구체적인 인스턴스에 대한 구현을 서브 클래스가 정하는 패턴이다. 즉, 부모클래스에서 객체들을 생성할 수 있는 뼈대(interface)를 제공하고, 자식 클래스들이 생성될 객체의 유형을 결정할 수 있도록 하는 생성 패턴이다. 

  • Creator: 뼈대 즉, interface를 제공하는 최상위 팩토리 인터페이스다. 서브 클래스들에게 구현을 위임한다.
    • templateMethod(): 객체 생성에 관한 전처리, 후처리를 템플릿화 한 메서드(공통 작업 처리)
    • createProduct(): 서브 클래스에서 구현해야 할 객체 생성 추상 메서드
  • ConcreteCreator: 서브 클래스로써 구현해야 할 제품에 맞는 제품 객체를 반환하도록 추상 메서드를 구현한다.
    • Product: 제품 구현체를 추상화
    • ConcreteProduct: 제품 구현체

팩토리 메서드 패턴은 객체(제품)를 만들어내는 공장을 만드는 패턴이라고 볼 수 있다. 어떤 제품이 생성될지에 관한 자세한 내용은 서브 클래스에서 결정한다.

 

만드는 방법이 번거로움에도 불구하고 사용하는 이유는 상위 클래스는 객체 생성방식에 대 해 알 필요가 없기에 유연성을 갖게 되고, 객체의 구체적인 구현은 하위 클래스에서 구현하기 때문에 유지보수성이 증가하기 때문이다. 아래의 예제를 통해 사용하는 이유에 대해 조금 더 자세히 알아보자.


Factory Method Pattern 사용 전 문제점

@Data
public class Ship {
  private String name;
  private String email;
  private String logo;
  private String color;
}

public class ShipFactory {

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

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

    if (name.equalsIgnoreCase("whiteship")) {
    	ship.setLogo("\uD83D\uDEE5");
    } else if(name.equalsIgnoreCase("blackship")) {
    	ship.setLogo("⚓");
    }
    
    .............
  }
}

위의 코드는 배를 찍어내는 공장이다. 공장에서 흰색 배와 검정색 배 두가지 종류만을 제조한다면 무척이나 간단할 것이다. 그러나 공장이 잘되서 배의 종류가 늘어나고 하늘을 나는 배, 잠수함 등... 많은 것들을 제조하기 시작 한다면 조건이 많아져 구현하는 방법이 매우 복잡해질 것이다. 또한 배에 추가적인 특성이 생길 때 마다 기존 Ship 클래스의 내용이 계속 바뀌어야 한다.

 

이는 객체지향 원칙중 open closed principle(개방, 폐쇄) 원칙을 어기게 된다. 요구 사항이 생길 때마다 기존 코드 즉, Ship 클래스가 변해야 되기 때문이다. 이런 때에 팩토리 패턴을 사용하여, 확장에는 열려있고 변경에는 닫혀있는 코드를 작성할 수 있다.


Factory Method Pattern 구현 예제

public interface ShipFactory {

  // java8에 추가된 디폴트 메서드
  // 객체 생성 전처리/ 후처리 메서드
  default Ship orderShip(String name, String email){
    validate(email);

    prepareFor(name);

    // 선박 객체 생성
    Ship ship = createShip();

    sendEmailTo(email, ship);
    return ship;
  }
  
  // 팩토리 추상 메서드
  Ship createShip();

  // Java9에 추가된 private 메서드
  private void sendEmailTo(String email, Ship ship){
    System.out.println(ship.getName() + " 다 만들었습니다.");
  };

  private void validate(String email){
    if (email == null || email.isBlank()) {
      throw new IllegalArgumentException("연락처를 남겨주세요.");
    }
  }

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

public class WhiteShipFactory implements ShipFactory{
  @Override
  public Ship createShip() {
    return new Whipteship();
  }
}

public class BlackShipFactory implements ShipFactory{
  @Override
  public Ship createShip() {
    return new BlackShip();
  }
}
@Data
public class Ship {
  private String name;
  private String email;
  private String logo;
  private String color;
}

public class Whipteship extends Ship{
  public Whipteship() {
    setName("whiteship");
    setLogo("\uD83D\uDEE5");
    setColor("white");
  }
}

public class BlackShip extends Ship{
  public BlackShip(){
    setName("blackship");
    setLogo("⚓");
    setColor("black");
  }
}
public class Client {

  public static void main(String[] args) {
    Client client = new Client();
    client.print(new WhiteShipFactory(), "whiteship", "white@naver.com");
    client.print(new BlackShipFactory(), "blackship", "black@naver.com");
  }

  private void print(ShipFactory shipFactory, String name, String email) {
    System.out.println(shipFactory.orderShip(name, email));
  }
}

먼저 Creator 즉, 뼈대를 담당할 최상위 팩토리 인터페이스 ShipFactory 를 생성한다.

Java 8 버전 이후 추가된 인터페이스의 디폴트 메서드와 Java 9 버전 이후 추가된 private 메서드를 통해 인터페이스로 구성할 수 있게 되었다고 한다.

ShipFactory 인터페이스에서는 서브 클래스에서 구현해야 할 객체 생성 추상 메서드 createShip()을 추상화 한다. 이후 각 선박의 종류에 맞게 ShipFactory 인터페이스를 구현하는 서브 팩토리 클래스를 만들어 createShip() 추상 메서드를 각 객체의 특성에 맞게 구현한다.

 

ShipFactory 인터페이스의 orderShip() 기본 메서드를 보면 팩토리에서 진행되야 할 공통 작업 코드들을 수행하면서 특징에 따라 구현할 Ship 인스턴스를 만드는 작업만을 서브 클래스가 구현하도록 하는 모습을 볼 수 있다.

 

팩토리 메서드 패턴을 적용하기 전 코드는 whiteShip, blackShip에 이어 merchantShip을 추가한다면 기존 코드에 분기문을 추가하여 코드가 복잡해지고 기존 코드를 계속 수정해야 하는 문제점이 발생했을 것이다. 그러나 팩토리 메서드 패턴을 적용한다면 간단하게 서브 클래스를 구현하고 제품 객체를 정의하여 확장이 가능하다. 예제를 통해 merchantShip을 추가해보자.

public class MerchantShipFactory implements ShipFactory{
  @Override
  public Ship createShip() {
    return new MerchantShip();
  }
}

public class MerchantShip extends Ship{
  public MerchantShip(){
    setName("merchantShip");
    setLogo("🚢");
    setColor("blue");
  }
}
public class Client {

  public static void main(String[] args) {
    Client client = new Client();
    client.print(new WhiteShipFactory(), "whiteship", "grande@naver.com");
    client.print(new BlackShipFactory(), "blackship", "ariana@naver.com");
    client.print(new MerchantShipFactory(), "merchantship", "merchant@naver.com");
  }

  private void print(ShipFactory shipFactory, String name, String email) {
    System.out.println(shipFactory.orderShip(name, email));
  }
}

위 코드와 같이 기존 소스코드에 아무런 변경없이 확장하였다. 그저 Client 에서 주문하기 위한 코드가 한 줄 추가되었을 뿐이다.

 

확장에는 열려있으며, 변경에는 닫혀있는 구조를 팩토리 메서드 패턴을 이용하여 구현할 수 있다.


사용시기

결합도를 낮추고자 할 때

팩토리 메서드 사용 전에는 인스턴스를 생성하고 제품에 따라 분기처리하는 로직이 한곳에 몰려있었다. 그러나 팩토리 메서드 이후에는 각각의 제품에 따라 인스턴스를 생성하는 공장이 나뉘고 공장에서 생성하는 제품에 로직에만 신경을 쓰게된다. 이러한 방식으로 인스턴스 생성부와 제품 처리 로직의 결합도를 낮추고 제품을 추가할때에 기존의 코드를 건드리지 않고 손쉽게 새로운 제품을 추가하고 싶을때 사용할 수 있다.

라이브러리 또는 프레임워크 사용자에게 컴포넌트를 확장하는 방법을 제공 할 때

ShipFactory라는 선박을 만들수 있는 라이브러리를 사용자에게 제공했다고 가정해보자. 해당 라이브러리는 화물용 선박, 전투용 선박 만을 제공한다. 그러나 사용자는 물고기를 잡는 어선이 필요하다. 이때 사용자에게 해당 ShipFactory를 구현할 수 있도록 제공한다면 사용자는 서브 클래스를 만들어 쉽게 어선을 구현 할 수 있다.

기존 객체를 재구성 하기보다 재사용 하려는 경우

팩토리 메서드 사용전 문제점 코드를 볼 때 생성로직과 분기처리 로직이 한곳에 모여있다. 대부분의 처리 로직이 중복되지만 제품이 새로 추가되거나, 생성해야 하는 제품에 따라 많은 부분이 변경되어야 하는 경우가 발생할 수 있다. 이때 팩토리 메서드 패턴을 사용한다면 새로 추가되는 제품에 들어가는 특수한 로직은 서브 클래스에서 구현할때 즉, 해당 제품을 만들때에 따로 처리할 수 있게된다.

 

기존 객체를 재구성하지 않고 재사용할 부분들은 재사용하며, 새로 추가되는 제품 처리에 필요한 로직은 해당 제품을 생산하는 서브 클래스에서 별도로 처리할 수 있다.


마무리

팩토리 메서드 패턴을 사용시 개방, 폐쇄 원칙을 준수할 수 있으며, 서브 클래스를 통해 객체를 구현하기에 수정사항 발생시 해당 서브 클래스만을 수정해도 된다는 유지보수의 편리성이 있다. 또한 제품을 따로따로 구현해도 되기에 여러 개발자가 협업을 통해 개발할 수 있다.

 

다만 각 제품마다 서브 클래스와 제품 구현체를 구현해주어야 하기 때문에, 작성해주어야 하는 구현체가 늘어날수록 클래스의 수가 많이 증가한다.


참조

CS 지식의 정석 | 디자인패턴 네트워크 운영체제 데이터베이스 자료구조

백기선 - 코딩으로 학습하는 GoF의 디자인 패턴

인파 - 팩토리 패턴

https://refactoring.guru/ko/design-patterns/factory-method