팩토리 메소드 (Factory method) 패턴

weekbelt·2022년 11월 27일
1

1. 패턴 소개

팩토리 메소드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만듭니다. 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 위임합니다.

자동차를 만드는 공장으로 예를 들어보겠습니다. 세단만 생산하는 공장이 있었는데 엄청난 인기로 잘팔리면서 공장에서 이제는 suv까지 생산해서 팔기로 했습니다. 기존에 세단만 생상하는 팩토리 메서드는 예를들어 orderCar라는 메서드로 주문을 받아서 세단을 생산하고 있었는데 suv까지 생산을 하게 된다면 orderCar에서 if문으로 세단인지 suv인지 분기를 통하여 생산을 해야합니다. 이렇게되면 나중에 사업을 더 확장하여 웨건도 생산하기로 했을때 if-else문이 늘어나면서 점차 코드가 복잡해지기 시작합니다.

Car car;

if (sedan) {
	car = new Sedan();
} else if (suv) {
	car = new Suv();
} else if (wagon) {
	car = new Wagon();
}

이 코드를 보면 주어진 조건에 따라 인스턴스가 결정됩니다. 이렇게 구현이 되면 뭔가 변경하거나 확장해야 할 때 코드를 다시 확인하고 추가 또는 제거해야하는 번거로움이 생깁니다. 따라서 코드를 관리 및 갱신하기가 어려워지고, 오류가 생길 가능성도 높아지게 됩니다. 그래서 추상화 되어있는 팩토리를 만들예정입니다. 먼저 위의 그림을 보면 Creator인터페이스를 생성합니다. 이 인터페이스에서 정의되어 있는 메서드 중에 구현체에서 바뀌어야 하는 부분을 추상메서드로 선언해서 하위클래스인 ConcreteCreator에서 구체적인 인스턴스를 생성하도록 구현할 수 있게끔 해줍니다. 그리고 팩토리 메소드에서 다양한 Product를 만들 수 있게끔 Product를 인터페이스로 선언합니다.

코드로 살펴보겠습니다.

Car 클래스

public class Car {

    private String name;

    private String color;

    private String logo;

	// getter, setter, toString ....
}

Car 생성 요청

public class Client {

    public static void main(String[] args) {
        Car sedan = CarFactory.orderCar("sedan", "weekbelt@mail.com");
        System.out.println(sedan);

        Car suv = CarFactory.orderCar("suv", "weekbelt@mail.com");
        System.out.println(suv);
    }

}

Client클래스에서 CarFactory에 각각 sedan과 suv를 생성하도록 주문을 요청하는 orderCar를 실행하고 있습니다.

CarFactory 클래스

public class CarFactory {

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

        prepareFor(name);

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

        // Customizing for specific name
        if (name.equalsIgnoreCase("sedan")) {
            car.setLogo("🚗");
        } else if (name.equalsIgnoreCase("suv")) {
            car.setLogo("🚙");
        }

        // coloring
        if (name.equalsIgnoreCase("sedan")) {
            car.setColor("red");
        } else if (name.equalsIgnoreCase("suv")) {
            car.setColor("blue");
        }

        // notify
        sendEmailTo(email, car);

        return car;
    }

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

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

sedan인지 suv인지에 따라서 이모지의 모양과 색상이 달라지도록 if-else문으로 구분하도록 구현하였습니다. 이렇게 sedan과 suv를 생산하는 코드가 한곳에 있습니다. 예를들어 웨건을 추가하기로 했다면 이코드에 if-else문을 추가해야합니다. 또한 Car클래스가 구체적인 클래스이기 때문에 만들어낼 제품의 특성을 저장할 필드나 메서드가 추가될 가능성이 있습니다. 결국 새로운 제품을 생상할 때마다 기존의 코드들이 계속 바뀌게 되고 바뀌면서 실수를 할 가능성이 높아지기 때문에 코드의 안정성과 유지보수 측면에서 굉장히 변경에 취약한 코드가 됩니다. 이런 코드를 변경에 닫혀있지 않다라고 합니다. 객체지향의 원칙중 하나인 확장에는 열려있고 변경에는 닫혀있어야하는 원칙을 어기게 됩니다.

그렇다면 이 구조를 어떻게 고쳐서 확장에는 열려있고 변경에는 닫혀있게 할 수 있을지 알아보겠습니다.

2. 패턴 적용하기

팩토리 메서드를 사용하는 Client

public class Client {

    public static void main(String[] args) {
        Car sedan = CarFactory.orderCar("sedan", "weekbelt@mail.com");
        System.out.println(sedan);

        Car suv = CarFactory.orderCar("suv", "weekbelt@mail.com");
        System.out.println(suv);
    }

}

실행 결과

sedan 만들 준비 중
sedan 다 만들었습니다.
Car{name='sedan', color='red', logo='🚗'}
suv 만들 준비 중
suv 다 만들었습니다.
Car{name='suv', color='blue', logo='🚙'}

팩토리 메서드 패턴을 적용해서 기존의 코드를 리팩토링 해보겠습니다. 팩토리 메서드 패턴을 적용해도 위와 같은 실행 결과가 나와야합니다.

기존 CarFactory 클래스

public class CarFactory {

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

        prepareFor(name);

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

        // Customizing for specific name
        if (name.equalsIgnoreCase("sedan")) {
            car.setLogo("🚗");
        } else if (name.equalsIgnoreCase("suv")) {
            car.setLogo("🚙");
        }

        // coloring
        if (name.equalsIgnoreCase("sedan")) {
            car.setColor("red");
        } else if (name.equalsIgnoreCase("suv")) {
            car.setColor("blue");
        }

        // notify
        sendEmailTo(email, car);

        return car;
    }

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

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

sedan과 suv를 생성하는 코드가 모두 CarFactory클래스에 있습니다. 여기서 공통적으로 처리하는 부분은
orderCar메서드안에 name과 email을 validation하는 부분과 prepareFor메서드가 있습니다. 그래서 인터페이스에 default메서드로 orderCar()선언하고 name과 email검증 로직과 준비중을 알려주는 로직을 추가하겠습니다.

공통 로직을 orderCar default 메서드에 선언

public interface ICarFactory {

    default Car orderCar(String name, String email) {
        validate(name, email);
        prepareFor(name);
        
        // Car 생성로직
        
        sendEmailTo(email, car);
		return car;
    }

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

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

orderCar메서드에 email, name유효성을 검사하는 validate메서드, 진행상태를 출력하는 prepareFor메서드, 차 생성후 이메일을 발송하는 sendEmailTo메서드를 추가합니다.

변경되는 로직

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

        // Customizing for specific name
        if (name.equalsIgnoreCase("sedan")) {
            car.setLogo("🚗");
        } else if (name.equalsIgnoreCase("suv")) {
            car.setLogo("🚙");
        }

        // coloring
        if (name.equalsIgnoreCase("sedan")) {
            car.setColor("red");
        } else if (name.equalsIgnoreCase("suv")) {
            car.setColor("blue");
        }

기존 CarFactory에서 차종에 따라 바뀌는 이 생성로직을 팩토리 별로 분리시켜야 합니다. 먼저 ICarFactory의 orderCar메서드에 차를생성하는 추상메서드를 추가하여 하위타입에서 무조건 구현을 하도록 합니다.

변경 되는 로직을 추상 메서드로 선언

public interface ICarFactory {

    default Car orderCar(String name, String email) {
        validate(name, email);
        prepareFor(name);
        
        Car car = createCar();		// 변경되는 로직
        
        sendEmailTo(email, car);
        return car;
    }

    Car createCar();		// 추상 메서드로 선언
    
    // validate, prepareFor, sendEmail ......
}

기존 CarFactory클래스에 있는 공통처리 부분을 가져와 ICarFactory인터페이스에 orderShip메서드에 정의했습니다. 위의 orderCar메서드를 보면 코드의 가독성이 좋아지는 것을 볼 수 있습니다. 그대로 읽어서 name, email유효성 검사를 하고 차를 준비하고 차를 생성한 후에 이메일전송을 하는구나 하고 이해할 수 있습니다. 그렇다면 이제 ICarFactory의 구현체를 생성해 보겠습니다.

ICarFactor의 구현체를 구현하기 전에 Product도 Sedan과 Suv를 구분하기위해 Car를 상속받는 Sedan클래스를 먼저 선언하겠습니다.

Car를 상속받는 Sedan클래스

public class Sedan extends Car {

    public Sedan() {
        setName("sedan");
        setLogo("\uD83D\uDE97");
        setColor("red");
    }
}

기존에 CarFactory에서 if-else문으로 분기해서 name, logo, color를 설정했지만 지금은 Sedan클래스에서 생성자를 통해서 미리 설정할 수 있습니다.

sedan을 생성하는 SedanFactory 클래스

public class SedanFactory implements ICarFactory{

    @Override
    public Car createCar() {
        return new Sedan();
    }
}

SednaFactory에서는 Sedan을 생성해서 리턴해주기만 하면 됩니다.

이제 생성한 로직을 Client에서 사용할때 인터페이스를 통해서 차를 생성하도록 해야합니다.

인터페이스타입으로 orderCar를 호출해서 Car를 생성

public class Client {

    public static void main(String[] args) {
        ICarFactory carFactory = new SedanFactory();
        
        Car sedan = carFactory.orderCar("sedan", "weekbelt@mail.com");
        System.out.println(sedan);
    }

}

실행결과 기존의 실행결과와 같다는 것을 알 수 있습니다.

실행결과

sedan 만들 준비 중
sedan 다 만들었습니다.
Car{name='sedan', color='red', logo='🚗'}

중요한건 이제 Suv를 추가할때 ICarFactory, SedanFactory클래스가 바뀌는지 안바뀌는지가 중요합니다. 객체지향 원칙중 하나인 확장에 열려있고 변경에 닫혀 있는 구조라면 지금 Suv를 만드는 공장을 추가할 때 기존코드가 변경되는지 살펴보겠습니다.
우선 Car클래스를 상속받는 Suv클래스를 생성합니다.

Suv클래스

public class Suv extends Car {

    public Suv() {
        setName("suv");
        setLogo("\uD83D\uDE99");
        setColor("blue");
    }
}

다른 코드를 건드릴 필요없이 Suv클래스만 추가하면 됩니다. 그다음 Suv를 생산하는 팩토리를 생성하겠습니다.

SuvFactory 클래스

public class SuvFactory implements ICarFactory {

    @Override
    public Car createCar() {
        return new Suv();
    }
}

SuvFactory도 마찬가지 기존 코드를 건드리지않고 Suv를 리턴하도록 새로 추가만 했습니다. 결국 확장에는 열려있고 변경에는 닫혀있는 코드가 됐습니다. 그렇다면 Client코드에서 팩토리만 ICarFactory에 갈아끼워서 SedanFactory를 사용하면 Sedan이 나오고 SuvFactory를 사용하면 Suv가 나오도록 실행해보겠습니다.

인터페이스의 default메서드를 통해서 실행

public class Client {

    public static void main(String[] args) {
        ICarFactory carFactory = new SedanFactory();
        Car sedan = carFactory.orderCar("sedan", "weekbelt@mail.com");
        System.out.println(sedan);

        carFactory = new SuvFactory();
        Car suv = carFactory.orderCar("suv", "weekbelt@mail.com");
        System.out.println(suv);
    }

}

실행결과

sedan 만들 준비 중
sedan 다 만들었습니다.
Car{name='sedan', color='red', logo='🚗'}
suv 만들 준비 중
suv 다 만들었습니다.
Car{name='suv', color='blue', logo='🚙'}

실행결과를 보면 패턴을 적용하기 전 코드의 결과와 같음을 확인할 수 있습니다. 하지만 어떤 분들은 다른 코드는 변경이 안되어도 Client코드가 변경됐고 그렇다면 변경에 닫혀있는게 아니냐고 생각하실 수 있습니다. 그래서 이런 Factory를 갈아끼우는 그런 부분은 스프링에서 쓰고있는 의존성주입을 이용해서 설정과 구현을 분리시켜서 클라이언트에서 코드변경을 최소화 할 수 있습니다.

3. 장점

팩토리 메소드 패턴을 적용하면서 얻는 장점은 객체지향의 원칙중 하나인 확장에 열려있고 변경에 닫혀있는 원칙을 지킬 수 있습니다. 인스턴스를 만드는 과정이 담겨있는 로직을 건드리지 않고 새로운 인스턴스를 다른 방법으로 얼마든지 확장이 가능합니다. 이런 확장이 가능한 이유는 Creator와 Product간의 관계를 인터페이스를 통해 느슨하게 맺어지기 때문입니다.

4. 단점

기존에 차를 생성하는 CarFactory하나가 있었지만 차의 종류마다 Factory를 생성해야하기 때문에 클래스가 많이 늘어납니다.

참고

profile
백엔드 개발자 입니다

0개의 댓글