자바/기본

디자인 패턴 - 추상 팩토리 패턴

backend dev 2024. 3. 15.

추상 팩토리 패턴

추상 팩토리 패턴은 연관성이 있는 객체 군이 여러개 있을 경우 이들을 묶어 추상화하고, 어떤 구체적인 상황이 주어지면 팩토리 객체에서 집합으로 묶은 객체 군을 구현화 하는 생성 패턴

 

팩토리 메서드 패턴 vs 추상 팩토리 패턴

객체 생성 과정을 추상화한 인터페이스를 제공객체 생성을 캡슐화함으로써 

구체적인 타입을 감추고 느슨한 결합 구조를 표방

new로 직접 생성하는것을 막음 (private 생성자)  그래서 구체적인 객체의 타입을 감추고

인터페이스화 된 팩토리에 접근해서 객체를 생성하는것은 두 패턴의 공통점이다.

 

차이점

팩토리 메서드 패턴은 한 팩토리당 한 종류의 객체 생성을 지원 한다

예를 들어 버튼 팩토리가 있으면 A버튼 , B버튼 ,C버튼과 같이 

버튼이라는 추상클래스를 상속받은 버튼 서브클래스(자식클래스)만 만든다.

 

 

추상 팩토리 패턴은 한 팩토리안에 연관있는 객체생성만 들어있는 팩토리이다.

예를 들어 패딩 팩토리가 있으면 그 안에는 패딩버튼, 패딩자크, 패딩옷감과 같이

패딩과 관련되어 있는 객체 생성을 모아놓는 형식이다.

 

즉 팩토리 메서드 패턴은 같은 타입의 객체를 생성을 모아놓는 팩토리를 사용하고

 

추상 팩토리 패턴은 서로 연관된 여러 종류의 객체 생성을 지원한다. 

 

 

추상 팩토리 패턴 사용 시기

 

 -  각기 다른 여러 타입의 객체를 생성을 해야할때

[팩토리 메서드 패턴은 각 타입마다 팩토리가 하나씩 존재하게 된다. 

필요한 객체의 팩토리객체를 생성해야하는 번거로움이 발생한다.

추상 팩토리 패턴은 사용해야하는 연관된 객체의 생성을 담당하는 팩토리가 있기에

하나의 팩토리 객체만 생성해서 원하는 각기 다른 타입의 객체를 생성할 수 있다.]

 

 

- 구체적인 클래스에 의존하고 싶지않을때

[사용자는 팩토리 객체를 이용해서 결과값을 인터페이스 참조변수로 받게되면

팩토리 객체와 인터페이스만 의존하고

다양한 구현체를 받을 수 있다. 즉 각 구현체의 구체적인 클래스에 의존하지 않아도 된다.]

 

- 여러 제품군 중 하나를 선택해서 시스템을 설정해야하고 한 번 구성한 제품을 다른 것으로 대체할 수도 있을 때
[여기서 제품군이란 여러 타입의 객체가 필요한 것을 말함]

 

 

장점

객체를 생성하는 코드를 분리하여 클라이언트 코드와 결합도를 낮출 수 있다.
제품 군을 쉽게 대체 할 수 있다.

 

단점

- 각 구현체마다 팩토리 객체들을 모두 구현해주어야 하기 때문에

객체가 늘어날때 마다 클래스가 증가하여 코드의 복잡성이 증가한다.

(팩토리 패턴의 공통적인 문제점)

(추상 팩토리패턴에서는 구현체마다가 아닌, 집합군 마다 인거 같다.)

 

- 기존 추상 팩토리(abstract)의 세부사항이 변경되면 모든 팩토리에 대한 수정이 필요해진다.

이는 추상 팩토리(abstract)와 모든 서브클래스(추상팩토리의 자식클래스)의 수정을 가져온다. 

 

 

-새로운 종류의 제품을 지원하는 것이 어렵다. 새로운 제품이 추가되면 팩토리 구현 로직 자체를 변경해야한다.
[추상 팩토리(abstract)의 추상화를 다시해야한다는 뜻 이다. 
하나의 제품군을 만들고있었는데 ,다른 종류의 제품군을 만드려고하면 수정이 어렵기때문,

그때는 다른 추상 팩토리를 만들어서 쓰는게 더 나을듯]


팩토리 메서드 패턴으로 구현 예시

 

위의 구성을 팩토리 메서드 패턴으로 구현해보자.

 

팩토리 메서드 패턴의 공장 객체는 한가지 종류의 컴포넌트만 생성하는 구조이다. 

[버튼 팩토리에는 윈도우 버튼, 맥버튼과 같이 버튼의 구현체만 생성해낸다]

 

팩토리 메서드의 초점은 

추상화된 팩토리 메서드를 각 서브 공장 클래스가 재정의하여 걸맞는 제품 객체를 생성하는 것이기 때문이다. 

 

그렇기 때문에 버튼을 생성한다고 하더라도 

어느 OS 실행환경인지는 메서드 내에서 분기문을 통해 구분해 주어야 한다.

interface ComponentFactoryMethod {
    Component createOperation(String type); // 템플릿
    Component createComponent(String type); // 팩토리 메서드
}

class ButtonFactory implements ComponentFactoryMethod {

    public Button createOperation(String type) {
        Button button = createComponent(type);
        button.추가설정();
        return button;
    }

    public Button createComponent(String type) {

        Button button = null;

        switch (type.toLowerCase()) {
            case "window":
                button = new WindowButton();
                break;

            case "mac":
                button = new MacButton();
                break;
        }

        return button;
    }
}

class CheckBoxFactory implements ComponentFactoryMethod {
    public CheckBox createOperation(String type) {
        CheckBox checkbox = createComponent(type);
        checkbox.추가설정();
        return checkbox;
    }

    public CheckBox createComponent(String type) {

        CheckBox checkbox = null;

        switch (type.toLowerCase()) {
            case "window":
                checkbox = new WindowCheckBox();
                break;

            case "mac":
                checkbox = new MacCheckBox();
                break;
        }

        return checkbox;
    }
}

class TextEditFactory implements ComponentFactoryMethod {
    public TextEdit createOperation(String type) {
        TextEdit txtedit = createComponent(type);
        txtedit.추가설정();
        return txtedit;
    }

    public TextEdit createComponent(String type) {

        TextEdit txtedit = null;

        switch (type.toLowerCase()) {
            case "window":
                txtedit = new WindowTextEdit();
                break;

            case "mac":
                txtedit = new MacTextEdit();
                break;
        }

        return txtedit;
    }
}
public static void main(String[] args) {
    ComponentFactoryMethod factory = null;
    Button btn = null;
    CheckBox chkBox = null;

    // 윈도우 버튼 생성
    factory = new ButtonFactory();
    btn = (Button) factory.createOperation("Window");
    btn.render();

    // 맥 버튼 생성
    btn = (Button) factory.createOperation("Mac");
    btn.render();

    // 윈도우 체크 박스 생성
    factory = new CheckBoxFactory();
    chkBox = (CheckBox) factory.createOperation("Window");
    chkBox.render();

    // 맥 체크 박스 생성
    chkBox = (CheckBox) factory.createOperation("Mac");
    chkBox.render();
}

 

팩토리 메서드의 문제점

팩토리 메서로 구현해본 코드는 실행 자체는 문제가 없어 보이지만, 만일 기능을 확장할 필요가 있을 때 문제가 생기게 된다. 예를 들어 OS 종류에 Linux를 새로 추가한다고 생각해보자. 그러면 각 메서드마다 있는 분기문 로직을 일일히 수정하여야 하는데, 그러면 OCP 원칙에 위배되는 꼴이 된다.

[ocp -> 개방 폐쇠원칙 open closed principle == 기존의 코드를 변경하지않으면서 기능을 추가하도록 설계해야한다.]

[linux 환경이 추가되면 모든 팩토리에서 linux타입이 들어왔을경우의 코드를 추가해줘야한다.]

 

 

추상 팩토리 패턴으로 구현

그럼 추상 팩토리 패턴으로 구현하면 어떨까?
팩토리 메서드의 공장 객체는 한 종류의 컴포넌트만 생성하지만, 

추상 팩토리의 공장 객체는 하나의 객체에서 여러 종류의 컴포넌트들을 골라 생산할 수 있도록 구성한다.

[ 하나의 제품군(혹은 제품)을 만들기 위해 필요한 다양한 타입의 객체 생성을 한 팩토리 안에 모은다]

interface ComponentAbstractFactory {
    Button createButton();
    CheckBox createCheckBox();
    TextEdit createTextEdit();
}

class WindowFactory implements ComponentAbstractFactory {

    @Override
    public Button createButton() {
        return new WindowButton();
    }

    @Override
    public CheckBox createCheckBox() {
        return new WindowCheckBox();
    }

    @Override
    public TextEdit createTextEdit() {
        return new WindowTextEdit();
    }
}

class MacFactory implements ComponentAbstractFactory {

    @Override
    public Button createButton() {
        return new MacButton();
    }

    @Override
    public CheckBox createCheckBox() {
        return new MacCheckBox();
    }

    @Override
    public TextEdit createTextEdit() {
        return new MacTextEdit();
    }
}
public static void main(String[] args) {
    ComponentAbstractFactory factory = null;

    // 윈도우 버튼 생성
    factory = new WindowFactory();
    Button WindowBtn = createBtn(factory);
    WindowBtn.render();

    // 맥 버튼 생성
    factory = new MacFactory();
    Button MacBtn = createBtn(factory);
    MacBtn.render();
}

// 추상 팩토리에서 객체를 생성하는 부분 코드는 같기 때문에 따로 메서드로 묶음 분리
public static Button createBtn(ComponentAbstractFactory fac) {
    return fac.createButton();
}

기존 팩토리 메서드에서는 다른 OS의 컴포넌트를 생성하기 위해선 문자열을 인자로 주어 메서드 내에서 

분기문으로 객체 생성을 처리하였지만, 추상 팩토리에선 어떠한 팩토리 객체를 생성하느냐에 따라

 똑같은 메서드를 호출해도 반환되는 결과가 다르게 된다.


하지만 착각하지 말아야 할 것이 추상 팩토리가 팩토리 메서드보다 무조건 좋다는 말이 아니다. 

이 예제 처럼 어떠한 제품들에 대한 '군'을 묶어 생성해야 할때 추상 팩토리로 구성하는 것이 

유지보수와 확장에 있어 더 유리하다는 것을 보여주는 것이다.

 

추상 팩토리의 유연한 확장

예를들어 Linux OS 환경이 추가된다 하더라도, 

기존의 코드 수정 없이 리눅스 컴포넌트 구현체 클래스와 리눅스 팩토리 클래스만 적절하게 

추가만 해주면 확장이 완료되게 된다.
기존 팩토리 메서드로 설계 했을때는 메서드의 분기문을 일일히 뜯어 고치는 것에 비하면 

확실히 OCP 원칙의 수정에는 닫혀있고 확장에는 열려있다는 말이 무슨 의미인지 체감이 온다

추상 팩토리의 문제점

그러나 모든 확장에 대해 유연하게 대처할수 있는 것은 아니다. 

이번에는 새로운 OS 환경이 아닌 새로운 컴포넌트인 툴팁 을 추가한다고 생각해보자. ( 팩토리에서 생성하는 구현체 추가)

그러면 모든 서브 팩토리 클래스( 팩토리 구현 클래스)마다 툴팁 객체를 생성하는 createToolTip() 메서드를 추가 해야 되니 

이때는 오히려 문제점으로 작용하게 된다.

 

추상 팩토리 객체 싱글톤화

기본적으로 팩토리 클래스는 호출되면 객체를 생성하기만 하면 되기 때문에

 메모리 최적화를 위해 각 팩토리 클래스마다 싱글톤 적용 하는 것이 옳다.

 

물론 위와 같이 메서드 호출용으로 일회용으로 쓰인 인스턴스는

JVM의 가비지 컬렉션(GC) 에 의해 자동으로 지워지지만,

이런 가비지 값이 늘어나게되면 나중에 객체 제거 과정에서 Stop-the-world 가 일어나게 된다. (프로그램이 렉걸림)

class WindowFactory implements ComponentAbstractFactory {

    // 싱글톤 객체화
    private WindowFactory() {
    }
    private static class SingleInstanceHolder {
        private static final WindowFactory INSTANCE = new WindowFactory();
    }
    public static WindowFactory getInstance() {
        return SingleInstanceHolder.INSTANCE;
    }

    @Override
    public Button createButton() {
        return new WindowButton();
    }

    @Override
    public CheckBox createCheckBox() {
        return new WindowCheckBox();
    }

    @Override
    public TextEdit createTextEdit() {
        return new WindowTextEdit();
    }
}

class MacFactory implements ComponentAbstractFactory {

    // 싱글톤 객체화
    private MacFactory() {
    }
    private static class SingleInstanceHolder {
        private static final MacFactory INSTANCE = new MacFactory();
    }
    public static MacFactory getInstance() {
        return MacFactory.SingleInstanceHolder.INSTANCE;
    }

    @Override
    public Button createButton() {
        return new MacButton();
    }

    @Override
    public CheckBox createCheckBox() {
        return new MacCheckBox();
    }

    @Override
    public TextEdit createTextEdit() {
        return new MacTextEdit();
    }
}

 

 

[Java] 싱글톤 패턴(Singleton Pattern) - 개념 및 예제

싱글톤 패턴(Singleton Pattern) 싱글톤 패턴은 객체 지향 프로그래밍에서 특정 클래스가 단 하나만의 인스턴스를 생성하여 사용하기 위한 패턴이다. 생성자를 여러 번 호출하더라도 인스턴스가 하

ittrue.tistory.com

 

추상 팩토리 + 팩토리 메서드 패턴 조합

사람들이 많이들 착각하는 부분이 추상 팩토리와 팩토리 메서드는 2지 선다형으로 알고 있다는 점이다.

이 둘은 엄연히 별개의 코드 패턴이다.


팩토리 메서드는 

추상 메서드를 통한 다른 제품 구현과 더불어 객체 생성에 관한 전처리, 후처리를 해주는 로직이 핵심이며,

추상 팩토리는 여러 타입의 객체 군을 생성할 수 있는 것이 핵심이다. 

 

따라서 둘을 적절히 조합하여 사용하여도 무관하다. 

아니 필요하면 둘을 조합하여 사용하여야 한다. 

 

추상 팩토리와 팩토리 메서드 패턴 둘을 조합하게 된다면, 

여러 타입의 객체 군을 생성하면서 

동시에 템플릿 메서드를 통해 전처리, 후처리 작업을 해주는 것이 가능해진다.

아래의 예제 코드는 OS 군 별로 추상 팩토리를 구성하며, 

각 객체 생성 메서드에 대해서 팩토리 메서드로 구성한 예제이다.

 

팩토리 메서드의 템플릿은 한꺼번에 컴포넌트들을 생성하고 

추가 세팅한다는 컨셉으로써 하나의 리스트로 묶어서 반환한다.
만일 버튼 따로, 체크박스 따로 하고 싶다면 

별도의 createButtonOperation() 이나 createCheckBoxOperation() 메서드로 각기 구현해주면 된다.

interface ComponentAbstractFactoryMethod {
	// 서브 클래스에서 구현할 팩토리 메서드들
    Button createButton(); 
    CheckBox createCheckBox();
    TextEdit createTextEdit();
	
    // 팩토리 템플릿
    default List<Component> createOperation() {
        Button btn = createButton();
        CheckBox chkbox = createCheckBox();
        TextEdit txtEdt = createTextEdit();
        btn.추가세팅();
        chkbox.추가세팅();
        txtEdt.추가세팅();

        return new ArrayList<Component>(Arrays.asList(btn, chkbox, txtEdt));
    }
}

// 추상 팩토리
class WindowFactoryMethod implements ComponentAbstractFactoryMethod {

    @Override
    public Button createButton() {
        return new WindowButton();
    }

    @Override
    public CheckBox createCheckBox() {
        return new WindowCheckBox();
    }

    @Override
    public TextEdit createTextEdit() {
        return new WindowTextEdit();
    }
}

// 추상 팩토리
class MacFactoryMethod implements ComponentAbstractFactoryMethod {
	// ...
}

 

public static void main(String[] args) {
    ComponentAbstractFactoryMethod factory = null;

    // 윈도우 컴포넌트 생성
    factory = new WindowFactoryMethod();
    List<Component> list = factory.createOperation();

    System.out.println(list);
    for(Component c: list) {
        c.render();
    }
}

 

팩토리 메소드 패턴은 객체 생성에 필요한 과정을 템플릿 처럼 미리 구성해놓고, 

[반환값으로 원하는 타입이 있을경우 그 반환값이 되게끔 처리해줘야하니까]

객체 생성에 관한 전처리나 후처리를 통해 생성 과정을 다양하게 처리하여 객체를 유연하게 정할 수 있는 특징도 있다.

// 공장 객체 추상화 (추상 클래스)
abstract class AbstractFactory {

    // 객체 생성 전처리 후처리 메소드 (final로 오버라이딩 방지, 템플릿화)
    final IProduct createOperation() {
        IProduct product = createProduct(); // 서브 클래스에서 구체화한 팩토리 메서드 실행
        product.setting(); // .. 이밖의 객체 생성에 가미할 로직 실행
        return product; // 제품 객체를 생성하고 추가 설정하고 완성된 제품을 반환
    }

    // 팩토리 메소드 : 구체적인 객체 생성 종류는 각 서브 클래스에 위임
    // protected 이기 때문에 외부에 노출이 안됨
    abstract protected IProduct createProduct();
}

팩토리 메서드 패턴의 팩토리 추상클래스를 보면 createOperation()을 통해 객체의 전처리를 진행하는 모습을 볼 수 있다.

이렇게 팩토리 메서드 패턴에서 팩토리는 생성만 추상화하는것이아닌 각 객체에 전처리,후처리또한 처리할수있다는 특징이 있다. 그래서 팩토리 메서드 패턴, 추상 팩토리 패턴을 조합해서 사용할 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'자바 > 기본' 카테고리의 다른 글

try-with-resources  (2) 2024.03.19
자바 - 상속  (0) 2024.03.11
자바 - 클래스 (내부클래스-인스턴스 클래스,static 클래스)  (1) 2024.03.06
자바 - 배열 , 제어문  (0) 2024.03.04

댓글