디자인 패턴(구)/생성 패턴

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

공대키메라 2022. 4. 3. 21:15

1. 아이디어 캐치

공대 키메라가 공장을 운영하고 있다고 하자.(역시 상상이 최고야... )

공장에서는 자동차 모델 1이라는 것을 생산하고 있다.

차체 Frame Stamping, 조립, 그리고 도색까지 모든 차의 제작 과정을 아우르고 있다.

그래서 공대 키메라의 공장의 매출이 상승해서 새로운 모델을 제작하려고 한다. 

이 자동차 모델 2는 기존 모델에서 핵심 기능만 다를 뿐 다른 것은 전부 동일하다.

그러면 우리는 기존 자동차 모델 1을 제작하기 위해 사용한 공정을 동일하게 사용하고, 일정 부분에서 자동차 모델 2를 위해 기능을 추가혹은 수정하면 된다.

결국, 잘 추상화 되어있는 factory 를 구현한다면 문제는 해결된다.

의도

객체를 생성하기 위해 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 서브클래스가 내리도록 한다. 

 

즉, 몇몇 파생된 클래스들의 인스턴스 생성에 관련된 패턴이다. 

사용 시기

  • 어떤 클래스가 자신이 생성해야 하는 객체의 클래스를 예측할 수 없을 때 
  • 생성할 객체를 기술하는 책임을 자신의 서브클래스가 지정했으면 할 때
  • 객체 생성의 책임을 몇 개의 보조 서브클래스 가운데 하나에게 위임하고, 어떤 서브클래스가 위임자인지에 대한 정보를 국소화시키고 싶을 

효과

응용 프로그램에 국한된 클래스가 우리 코드에 종속되지 않도록 한다. 

구조

  • Product : 팩토리 메서드가 생성하는 객체의 인터페이스를 정의
  • ConcreteProduct : Product클래스에 정의된 인터페이스를 실제로 구현
  • Creator : Product타입의 객체를 반환하는 팩토리 메서드를 선언. 기본적으로 팩토리 메서드를 구현해서 ConcreteProduct 객체를 반환. 또한, Product객체 생성을 위해 팩토리 메서드를 호출
  • ConcreteCreator : 팩토리 메서드를 재정의하여 ConcretProduct의 인스턴스를 반환

 

위 구조를 잘 보자. 최종적으로 위와 같이 구현을 하는지 말이다!

2. 구현하기

공장에서 모델 1을 생산한다고 하자. 

그런데 너무 장사가 잘되서 모델 2도 같은 공정을 이용해 생산하려고 한다. 

 

다만 달라지는건 차량의 색깔과 모델 이름이다.

 

이것을 만들기 위한 작업을 작성했다.

Car.java

public class Car {

    private String name;
    private String color;
    private Integer price;

    private Frame frame;

    private Airbag airbag;

	// getter & setter
    // toString 생략
}

CarFactory.java

public class CarFactory {

    public static Car makeCar(String name, String address){
        if(name == null || name.isBlank()) {
            throw new IllegalArgumentException("자동차 이름을 입력하세요");
        }
        if(address == null || address.isBlank()) {
            throw new IllegalArgumentException("주소를 입력하세요");
        }

        getOrder(name);

        Car car = new Car();
        car.setName(name);

        if (name.equalsIgnoreCase("model1")){
            car.setColor("white");
            car.setPrice(10000);
            car.setCompany("Bangal");
        } else if(name.equalsIgnoreCase("model2")){
            car.setColor("black");
            car.setPrice(15000);
            car.setCompany("ShanePlanet");
        }

        sendTextMessageTo(address, car);

        return car;

    }

    private static void getOrder(String name){
        System.out.println(name + " 주문이 들어왔습니다. 차를 만들자!");
    }

    private static void sendTextMessageTo(String address, Car car){
        System.out.println(car.getName() + " 제작이 완료됬습니다. 주소 : " + address + "로 배송 예정");
    }
}

Customer.java

public class Customer {

    public static void main(String[] args) {
        Car model1 = CarFactory.makeCar("model1", "seoul");
        System.out.println("model1 = " + model1);

        System.out.println("=======================");

        Car model2 = CarFactory.makeCar("model2", "daejeon");
        System.out.println("model2 = " + model2);

	/*
            출력 결과
            model1 주문이 들어왔습니다. 차를 만들자!
            model1 제작이 완료됬습니다. 주소 : seoul로 배송 예정
            model1 = Car{name='model1', color='white', price=10000, company='Bangal'}
            =======================
            model2 주문이 들어왔습니다. 차를 만들자!
            model2 제작이 완료됬습니다. 주소 : daejeon로 배송 예정
            model2 = Car{name='model2', color='black', price=15000, company='ShanePlanet'}

            Process finished with exit code 0
	*/
    }

}

 

코드를 잘 보면 공대 키메라의 공장에서 만들어 내는 자동차의 경우 model1이나, model2나 공통된 작업을 거치고 핵심적인 곳에서 다른 작업을 해주면 될 것 같다. 

 

현재 구조에서는 일일이 기존의 코드를 수정해야 하니 OCP가 지켜지지도 않았다. 

 

그러면 이것을 Factory Method Pattern을 적용해서 해결해보자.

 

공통적인 업무는 통일하되, 중요한 차 생성시 이름 부여와 address입력의 경우는 다르게 할 수 있도록 한다. 

 

, 체를 생성하기 위해 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 서브클래스가 내리도록 한다. 어떤걸 생성할지는 서브클래스가 결정하게 한다!!!!!!!!!@!@!@

3. 구현하기 - Factory Method Pattern 적용

CarFactoryInterface.java

public interface CarFactoryInterface {

	//default 로 선언 가능. java 8부터 
    default Car orderCar(String name, String address){
        validate(name, address);
        getOrder(name);
        Car car = makeCar();
        sendCarTo(address ,car);
        return car;
    }

    Car makeCar();

    //java 9 이상부터 private선언시에는 interface에도 메소드 구현이 가능함.
    private void validate(String name, String address){
        if(name == null || name.isBlank()) {
            throw new IllegalArgumentException("자동차 이름을 입력하세요");
        }
        if(address == null || address.isBlank()) {
            throw new IllegalArgumentException("주소를 입력하세요");
        }
    }

    private void getOrder(String name){
        System.out.println(name + " 주문이 들어왔습니다. 차를 만들자!");
    }

    private static void sendCarTo(String address, Car car){
        System.out.println(car.getName() + " 제작이 완료됬습니다. 원산지 : " + address);
    }
}

 

내부에서 중요한 부분은 Car를 어떻게 만드는 것인데

 

앞뒤 공정은 동일하다 가정하고, 차에 어떤 설정을 할지는 subclass에서 구현하도록 여지를 남겼다.

ModelOneWhieCar.java

public class ModelOneWhiteCar extends Car {

    public ModelOneWhiteCar() {
        setName("model first");
        setColor("white");
        setPrice(10000);
    }

}

ModelOneWhiteCarFactory.java

public class ModelOneWhiteCarFactory implements CarFactoryInterface{

    @Override
    public Car makeCar() {
        return new ModelOneWhiteCar();
    }
}

 

ModelOneWhiteCarFactory에서 CarFactoryInterface를 구현한다. 

 

makeCar만 override해주면 되는데 ModelOneWhiteCar를 부르는 순간 기본적인 setting이 자동적으로 되도록 했다.

Customer.java

public class Customer {

    public static void main(String[] args) {
    
        Car car1 = new ModelOneWhiteCarFactory().orderCar("model1", "seoul");
        System.out.println(car1);
        
        /*
        출력 결과
        model1 주문이 들어왔습니다. 차를 만들자!
        model first 제작이 완료됬습니다. 주소 : seoul로 배송 예정
        Car{name='model first', color='white', price=10000}

        */
    }

}

잠시나마의 고민

일단 우리가 생각한 의도, 구조대로 코드들이 잘 작성된건지 고민을 해보려고 한다.

 

다시 팩토리 메소드의 의도를 가져오면 다음과 같다. 

 

객체를 생성하기 위해 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 서브클래스가 내리도록 한다. 

 

현재 객체를 생성하기 위한 인터페이스는 CarFactoryInterface이다. 이를 상속하는 서브클래스는 ModelOneWhiteCarFactry와 ModelTwoBlackCarFactory이다. 각기 다른 두개의 클래스에서 어떤 인스턴스를 생성할지, 반환을 통해서 기능을 제공하고 있다. 그러므로 의도가 잘 맞게끔 구현을 했다고 볼 수 있다. 

 

그러면 구조는 어떻게 대응이 되는걸까?

 

구조에 대해 설명을 한것을 다시 가져와봤다.

  • Product : 팩토리 메서드가 생성하는 객체의 인터페이스를 정의
  • ConcreteProduct : Product클래스에 정의된 인터페이스를 실제로 구현
  • Creator : Product타입의 객체를 반환하는 팩토리 메서드를 선언. 기본적으로 팩토리 메서드를 구현해서 ConcreteProduct 객체를 반환. 또한, Product객체 생성을 위해 팩토리 메서드를 호출
  • ConcreteCreator : 팩토리 메서드를 재정의하여 ConcretProduct의 인스턴스를 반환

 

다양한 구현체 (Product)가 있고, 그중에서 특정한 구현체를 만들 수 있는 다양한 팩토리(Creator)를 제공할 수 있다.

 

1. Product에 해당되는 것은 CarFactoryInterface이다. 

즉, 우리가 만들 차에 대한 정보의 인터페이스를 정의한 Product가 CarFactoryInterface이다. 

 

2. ConcreteProduct에 해당되는 것은 ModelOneWhiteCarFactory, ModelTwoBlackCarFactory이다. 

우리가 생성한 Product인 CarFactoryInterface를 위 두개의 클래스가 상속해서 인터페이스를 구현하고 있다.

 

3. Creator에 해당되는 것은 ModelOneWhiteCar, ModelTwoBlackCar이다.

두개의 클래스는 우리가 구현해야 하는 메소드의 반환 객체의 타입으로 사용된다. 물론 필자는 여기서 Car를 상속받아서 각각의 클래스를 조금 다르게 손본것 뿐이다. 

Creator는 자신의 서브 클래스를 통해 실제 필요한 팩토리 메서드를 정의하여 적절한 ConcreteProduct의 인스턴스를 반환할 수 있게 한다. 

 

4. ConcreteCreator에 해당되는 것이 ModelOneWhiteCarFactory, ModelTwoBlackCarFactory인 것 같다. 

 

사실 이게 필자도 공부하면서 비교하는것이라 이게 맞는지 아닌지는 정확히 대입시켜서 뭘 못하겠다.

 

무언가 잘못된 부분이 있다면 피드백을 주시면 감사하겠습니다 꾸버벅


 

근데 만약 우리가 새로운 모델의 차를 생산해야 한다고 하자. 모델 2 생산은 어떻게 해야 할까?

ModelTwoBlackCar.java

public class ModelTwoBlackCar extends Car {

    public ModelTwoBlackCar() {
        setName("model second");
        setColor("black");
        setPrice(15000);
    }

}

ModelTwoBlackCarFactory.java

public class ModelTwoBlackCarFactory implements CarFactoryInterface{

    @Override
    public Car makeCar() {
        return new ModelTwoBlackCar();
    }
}

Customer.java

public class Customer {

    public static void main(String[] args) {
        /*
        Car model1 = CarFactory.makeCar("model1", "seoul");
        System.out.println("model1 = " + model1);

        System.out.println("=======================");

        Car model2 = CarFactory.makeCar("model2", "daejeon");
        System.out.println("model2 = " + model2);
        */

        Car car1 = new ModelOneWhiteCarFactory().orderCar("model1", "seoul");
        System.out.println(car1);

        Car car2 = new ModelTwoBlackCarFactory().orderCar("model2", "daejeon");
        System.out.println(car2);
        
        /*
            결과
            
            model1 주문이 들어왔습니다. 차를 만들자!
            model first 제작이 완료됬습니다. 원산지 : seoul
            Car{name='model first', color='white', price=10000}
            model2 주문이 들어왔습니다. 차를 만들자!
            model second 제작이 완료됬습니다. 원산지 : daejeon
            Car{name='model second', color='black', price=15000}
        */
    }

}

 

자! 이제 새로운 차를 양산할 때 마다 기존의 공정은 그대로 유지한 채로 어떻게 만들것인지만 설정해주면 된다.

 

 

엄연히 따지만 결국 어떤 모델을 사용할지 직접 수정을 하기 때문에 변경이 일어난거라고 할 수 있다.

 

이것을 Spring에서 DI를 사용하면 깔끔히 해결 가능하다.

 

이 FactoryMethod의 문제점은 새로운 subClass를 필요시에 계속 만들어야 한다는 점이다. 

 

필자도 똑같은 아이들을 다 만들어주었다. 


팩토리 메소드 디자인 패턴의 장점

  • Factory Method Pattern allows the sub-classes to choose the type of objects to create.
  • It promotes the loose-coupling by eliminating the need to bind application-specific classes into the code. That means the code interacts solely with the resultant interface or abstract class, so that it will work with any classes that implement that interface or that extends that abstract class.

 

팩토리 메소드 패턴은 앞에서부터 계~속 강조했듯이, 생성할 객체의 타입을 서브 클래스가 선택하도록 한다. 

또한, 코드 안에 애플리케이션 구체 클래스들의 binding을 제거해서 느슨한 결합을 증진시킨다. 

 

팩토리 메소드 패턴은 디자인 패턴을 크게 3개의 카테고리로 생성, 구조, 행위패턴으로 분류할 때,

생성 패턴에 해당한다. 

 

생성 패턴은 클래스 인스턴스화 혹은 객체 생성에 대한 디자인 패턴인데, 앞에서 설명한 의도대로 설계가 된 것 같다.