본문 바로가기
컴퓨터공학

[디자인 패턴] 추상 팩토리 패턴: 객체 생성의 유연성과 확장성 극대화

by oobw 2023. 12. 10.

'추상 팩토리 패턴(Abstract Factory Pattern)'은 객체 지향 프로그래밍에서 객체 생성의 유연성과 확장성을 증진시키는 중요한 디자인 패턴입니다. 이 글에서는 추상 팩토리 패턴의 개념, 장점, 사용 사례 및 구현 방법에 대해 심도 있게 소개합니다.

1. 추상 팩토리 패턴이란?

정의 및 개념

이 패턴의 주된 목적은 서로 관련 있는 객체의 그룹 또는 서로 의존하는 객체의 그룹을 생성할 때, 구체적인 클래스에 의존하지 않고 인터페이스를 통해 이들을 생성하는 것입니다. 이를 통해 소프트웨어의 확장성과 유연성이 증가하며, 코드의 재사용성과 테스트 용이성도 향상됩니다.

 

추상 팩토리 패턴은 '팩토리 메서드 패턴'을 일반화한 형태라고 볼 수 있습니다. 팩토리 메서드 패턴이 객체 생성을 서브클래스에 위임하는 반면, 추상 팩토리 패턴은 관련된 객체의 집합을 생성하는 책임을 여러 팩토리 클래스에 위임합니다.

패턴의 구조

추상 팩토리 패턴은 주로 다음 네 가지 주요 구성 요소로 이루어집니다.

  • Abstract Factory (추상 팩토리): 모든 구체적인 팩토리 클래스가 구현해야 하는 인터페이스입니다. 이 인터페이스는 다양한 종류의 객체를 생성하는데 필요한 메서드를 정의합니다.

  • Concrete Factory (구체적인 팩토리): 추상 팩토리 인터페이스를 구현하는 클래스로, 실제 객체 생성의 과정을 담당합니다. 각각의 구체적인 팩토리는 특정 객체 그룹을 생성하는 데 특화되어 있습니다.

  • Abstract Product (추상 제품): 팩토리가 생성하는 객체들의 일반적인 인터페이스입니다. 이 인터페이스는 모든 구체적인 제품이 구현해야 하는 기능을 정의합니다.

  • Concrete Product (구체적인 제품): 추상 제품의 인터페이스를 구현하는 클래스로, 추상 팩토리에 의해 생성되는 실제 객체입니다.

이 패턴은 클라이언트 코드가 구체적인 클래스의 인스턴스를 직접 생성하는 대신, 추상 인터페이스를 통해 관련 객체 그룹을 생성하도록 합니다. 이 방식은 시스템의 독립성과 확장성을 향상시키고, 특히 다양한 환경에서 실행되어야 하는 소프트웨어에서 유용합니다. 예를 들어, 다양한 유형의 UI 요소를 생성해야 하는 시스템에서 OS별로 다른 UI 구성 요소를 제공하는 것과 같은 경우에 이 패턴을 적용할 수 있습니다.

2. 추상 팩토리 패턴의 구현 예시

여기서는 GUI 라이브러리를 구현하는 예제를 통해서 추상 팩토리 패턴을 구현하는 방법을 살펴보겠습니다. 우리는 여러 OS를 지원하는 GUI 라이브러리를 만든다고 가정합니다. 추상 팩토리 패턴을 이용해서 새로운 OS를 지원해도 기존의 코드 수정 없이 확장하고 싶습니다. 이 예시에서는 두 가지 유형의 GUI 요소, 즉 버튼과 체크박스를 생성하는 시나리오를 가정합니다.

추상 팩토리의 인터페이스 정의

먼저 GUI Library가 지원하는 각 구성 요소들의 인터페이스를 정의합니다. 우리의 라이브러리는 Button과 Checkbox를 지원하고, paint라는 메소드를 정의합니다. 각 구성 요소들을 정의했으면, 이제 이 구성 요소들을 생성하는 공장, GUIFactory를 정의합니다. 이 GUIFactory는 이름에서 알 수 있듯이, GUI를 구성하는 각 구성 요소들을 생성해 주는 역할을 합니다.

// 추상 제품 인터페이스
interface Button {
    void paint();
}

interface Checkbox {
    void paint();
}

// 추상 팩토리 인터페이스
interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

 

위와 같이 생성 클래스와 구성 요소들에 대한 인터페이스를 정의하면 우리의 GUI 라이브러리의 추상 팩토리의 정의가 끝납니다. OS별로 추가적으로 지원하고 싶을 때 마다 이제 이 인터페이스를 구현하면 됩니다.

Windows OS 지원하는 구체적인 객체 구현하기

먼저 Windows OS를 지원하는 라이브러리를 구현합시다. 아래와 같이 Button과 Checkbox를 상속받아서 구체적인 WindowsButton과 WindowsCheckbox라는 클래스를 구현합니다. 그리고 이 객체들을 생성해 주는 WindowsFactory의 구체적인 팩토리 클래스도 구현합니다.

// Windows 버전의 구체적인 제품
class WindowsButton implements Button {
    public void paint() {
        System.out.println("Rendering a button in a Windows style");
    }
}

class WindowsCheckbox implements Checkbox {
    public void paint() {
        System.out.println("Rendering a checkbox in a Windows style");
    }
}

// Windows용 구체적인 팩토리
class WindowsFactory implements GUIFactory {
    public Button createButton() {
        return new WindowsButton();
    }

    public Checkbox createCheckbox() {
        return new WindowsCheckbox();
    }
}

우리가 구현한 GUI 라이브러리 사용하는 Application 만들기

이제 우리의 라이브러리를 활용하여 GUI 기반의 application을 구현하는 예를 봅시다. 인터페이스를 사용하여 특정 OS에 맞는 버튼과 체크박스를 생성합니다. 이렇게 하면 클라이언트 코드는 구체적인 클래스에 의존하지 않고, 추상 팩토리를 통해 필요한 객체를 생성할 수 있습니다.

class Application {
    private Button button;
    private Checkbox checkbox;

    public Application(GUIFactory factory) {
        button = factory.createButton();
        checkbox = factory.createCheckbox();
    }

    public void paint() {
        button.paint();
        checkbox.paint();
    }
}

class ApplicationMain {
    public static void main(String[] args) {
        GUIFactory factory;
        String osName = System.getProperty("os.name").toLowerCase();
        if (osName.contains("windows")) {
            factory = new WindowsFactory();
        } else {
            System.err.println("지원하지 않는 OS 입니다.");
            throw new Exception("Not supported OS")
        }

        Application app = new Application(factory);
        app.paint();
    }
}


이 예시에서는 Application 클래스가 추상 팩토리 GUIFactory를 사용하여 OS에 맞는 버튼과 체크박스를 생성합니다. ApplicationMain는 실행 환경에 따라 적절한 팩토리를 선택하여 Application 객체를 초기화합니다. 이 구조 덕분에 새로운 GUI 스타일이 추가되어도 기존 코드를 변경할 필요가 없으며, 새로운 팩토리와 제품 클래스만 추가하면 됩니다.

Mac OS 지원하기

예를 들어서 기존에 GUI 라이브러리를 새로운 OS인 Mac OS도 지원하기로 결정이 되었다고 가정합시다. 새로운 OS를 지원하는 일은 매우 큰 작업이 될 수 있습니다. 하지만 추상 팩토리 패턴을 사용해서 위와 같이 구현했다면 새로운 OS를 지원하는 것은 매우 쉬운 일입니다.

 

먼저 아래와 같이 GUIFactory와 각 구성 요소들에 대해서 Mac OS 버전만 새로 구현해 주면 됩니다. 기존의 코드는 전혀 수정할 필요가 없습니다.

// MacOS 버전의 구체적인 제품
class MacOSButton implements Button {
    public void paint() {
        System.out.println("Rendering a button in a MacOS style");
    }
}

class MacOSCheckbox implements Checkbox {
    public void paint() {
        System.out.println("Rendering a checkbox in a MacOS style");
    }
}

// MacOS용 구체적인 팩토리
class MacOSFactory implements GUIFactory {
    public Button createButton() {
        return new MacOSButton();
    }

    public Checkbox createCheckbox() {
        return new MacOSCheckbox();
    }
}

 

위의 코드와 같이 구현하면 MacOS 용으로 GUI Library의 확장이 끝났습니다.

MacOS를 지원하도록 최종 Application 수정하기

이제 MacOS 용 라이브러리를 이용하여 기존의 Application을 MacOS도 지원하도록 수정해 보겠습니다. 코드도 아래와 같이 최소한만 수정해서 다른 OS를 지원할 수가 있습니다.

class Application {
    private Button button;
    private Checkbox checkbox;

    public Application(GUIFactory factory) {
        button = factory.createButton();
        checkbox = factory.createCheckbox();
    }

    public void paint() {
        button.paint();
        checkbox.paint();
    }
}

class ApplicationMain {
    public static void main(String[] args) {
        GUIFactory factory;
        String osName = System.getProperty("os.name").toLowerCase();
        if (osName.contains("windows")) {
            factory = new WindowsFactory();
        } else if  (osName.contains("macOS"))  {
            factory = new MacOSFactory();
        } else {
            System.err.println("지원하지 않는 OS 입니다.");
            throw new Exception("Not supported OS")
        }

        Application app = new Application(factory);
        app.paint();
    }
}

 

위의 코드를 보면, 이 전에 구현한 application 코드와 비교하여 단 두 줄만 추가된 것을 알 수 있습니다. macOS 인지 체크하고, MacOSFactory를 생성하는 코드만 넣어주어서 새로운 os를 지원하는 작업을 마치게 됩니다.

 

이 구현은 추상 팩토리 패턴의 핵심 이점인 결합도의 감소, 코드 재사용성의 향상, 확장성 및 유연성 증가를 잘 보여줍니다. 프로그래머들은 이러한 예시를 통해 추상 팩토리 패턴의 구현 방법과 이점을 보다 명확하게 이해할 수 있습니다.

3. 추상 팩토리 패턴의 구현 방법 및 팁

기본 구현 방법

이전에 설명한 내용을 다시 리뷰하면 네 단계의 스탭으로 볼 수 있습니다.

  • 인터페이스 정의: 추상 팩토리 패턴의 첫 단계는 생성할 객체에 대한 추상 인터페이스를 정의하는 것입니다. 이 인터페이스는 팩토리가 생성해야 할 모든 제품에 대한 메서드를 포함합니다.

  • 구체적인 팩토리 생성: 각각의 구체적인 팩토리 클래스는 추상 팩토리 인터페이스를 구현합니다. 이들 팩토리는 실제 제품 객체를 생성하는 책임을 담당합니다.

  • 제품 인터페이스 및 구현: 제품에 대한 추상 인터페이스를 정의하고, 각 구체적인 팩토리에 의해 생성될 제품 클래스를 구현합니다.

  • 클라이언트 코드 작성: 클라이언트는 추상 팩토리 인터페이스를 사용하여 제품을 생성합니다. 이렇게 함으로써, 클라이언트 코드는 구체적인 제품 클래스에 대해 알 필요가 없으며, 확장이나 변경에 더 유연하게 대응할 수 있습니다.

고급 기법과 팁

  • 의존성 주입 활용: 의존성 주입(Dependency Injection)을 사용하여 팩토리 인스턴스를 클라이언트에 제공합니다. 이는 팩토리의 생성과 클라이언트 코드의 결합도를 낮추는 데 도움이 됩니다.

  • 싱글톤 패턴과의 결합: 팩토리 인스턴스가 여러 번 생성될 필요가 없는 경우, 팩토리 클래스를 싱글톤으로 구현할 수 있습니다. 이는 리소스 사용을 최적화하고, 인스턴스 관리를 단순화합니다.

  • 레이지 로딩: 제품 인스턴스를 필요할 때까지 생성하지 않는 '레이지 로딩(Lazy Loading)' 기법을 사용할 수 있습니다. 이는 초기 로딩 시간을 단축하고, 메모리 사용을 최적화합니다.

  • 팩토리 계층 구조: 복잡한 시스템에서는 팩토리 자체를 계층 구조로 설계할 수 있습니다. 이는 다양한 종류의 팩토리와 제품을 더 유연하게 관리할 수 있게 해 줍니다.

  • 추상 팩토리와 빌더 패턴의 조합: 복잡한 객체를 생성해야 하는 경우, 추상 팩토리 패턴과 빌더 패턴을 결합하여 사용할 수 있습니다. 이는 생성 과정을 더 세밀하게 제어할 수 있게 해 줍니다.

  • 유연한 팩토리 메서드: 팩토리 메서드를 확장하여 다양한 옵션과 매개변수를 통해 제품을 생성할 수 있도록 할 수 있습니다. 이는 클라이언트가 요구하는 특정 제품의 변형을 생성하는 데 유용합니다.

  • 리플렉션 사용: 어떤 시스템에서는 리플렉션을 사용하여 런타임에 팩토리 클래스를 동적으로 로드하고 인스턴스화할 수 있습니다. 이는 시스템의 확장성을 증가시키지만, 성능과 타입 안전성 측면에서 고려해야 할 사항이 있습니다.

4. 추상 팩토리 패턴의 실제 사례

추상 팩토리 패턴은 다양한 프로그래밍 언어와 산업 분야에서 폭넓게 활용되고 있습니다. 이 패턴은 객체 생성에 대한 유연성과 확장성을 제공하며, 시스템의 결합도를 낮추는 데 크게 기여합니다. 아래에서는 프로그래밍 언어와 산업 분야별로 구체적인 활용 사례를 살펴보겠습니다.

다양한 프로그래밍 언어에서의 활용

  • Java의 Swing 라이브러리: Java의 Swing 라이브러리는 추상 팩토리 패턴을 사용하여 다양한 GUI 컴포넌트를 생성합니다. 여기서 각 GUI 요소(버튼, 메뉴 등)는 특정 스타일 또는 테마에 맞게 생성됩니다.

  • Java의 Spring Framework: Spring은 의존성 주입을 위해 추상 팩토리 패턴을 사용합니다. 이를 통해 개발자는 구성을 통해 애플리케이션의 다양한 부분을 유연하게 조정할 수 있습니다.

  • 닷넷의 ADO.NET: .NET 프레임워크의 ADO.NET은 다양한 데이터베이스와의 연결을 위해 추상 팩토리 패턴을 사용합니다. DbProviderFactory 클래스는 데이터베이스 연결, 명령, 데이터 리더 등을 생성하는 데 사용됩니다.

  • Python의 Django Framework: Django의 ORM(Object Relational Mapping) 시스템은 다양한 데이터베이스 엔진(예: SQLite, PostgreSQL, MySQL)에 대해 동일한 API를 제공합니다. 이는 내부적으로 추상 팩토리 패턴을 사용하여 구현됩니다.

산업 분야 사례

  • IDEs (Integrated Development Environments) 소프트웨어 개발 툴: 다양한 프로그래밍 언어와 프레임워크를 지원하는 IDE들은 추상 팩토리 패턴을 사용하여 언어나 프레임워크에 특화된 툴킷, 디버깅 도구, 코드 에디터를 생성합니다.

  • 게임 엔진: Unity, Unreal Engine과 같은 게임 엔진은 추상 팩토리 패턴을 사용하여 다양한 플랫폼(Windows, MacOS, Linux 등)에 맞는 게임 오브젝트와 리소스를 생성합니다.

  • 금융 서비스 결제 게이트웨이 통합: 다양한 결제 방식(신용카드, PayPal, 암호화폐 등)을 지원하는 시스템은 추상 팩토리 패턴을 활용하여 각 결제 방식에 맞는 결제 프로세서를 생성합니다.

이러한 사례들에서 볼 수 있듯이, 추상 팩토리 패턴은 다양한 산업 분야에서 유연하고 확장 가능한 설계를 제공합니다. 이 패턴은 특히 시스템이 다양한 환경, 플랫폼, 또는 구성 요소를 지원해야 할 때 매우 유용합니다.

5. 추상 팩토리 패턴의 장점

추상 팩토리 패턴은 소프트웨어 설계에서 다양한 장점을 제공합니다. 이 패턴은 특히 객체 생성과 관련된 코드의 유연성과 재사용성을 증가시키는 데 중요한 역할을 합니다. 아래에서는 이 패턴의 주요 장점들을 자세히 살펴보겠습니다.

1) 유연성과 확장성

  • 추상 팩토리 패턴은 구체적인 클래스의 인스턴스 생성을 추상화함으로써, 시스템의 다른 부분과의 결합도를 낮춥니다. 이는 새로운 구체적인 팩토리나 제품을 시스템에 추가하거나 변경할 때 기존 코드의 수정을 최소화합니다.
  • 다양한 환경이나 상황에 맞게 제품군을 쉽게 교체할 수 있으며, 이는 특히 다중 플랫폼 애플리케이션 또는 다양한 외부 라이브러리와 상호 작용하는 시스템에서 유용합니다.

2) 객체 생성의 일관성

  • 추상 팩토리는 일련의 관련된 객체 또는 의존하는 객체들을 일관되게 생성합니다. 이는 객체들 간의 호환성을 보장하고, 오류를 줄이는 데 도움이 됩니다.
  • 클라이언트 코드는 구체적인 구현 대신 인터페이스에만 의존하므로, 생성되는 객체의 정확한 유형을 몰라도 작동합니다. 이는 코드의 일반성과 재사용성을 향상시킵니다.

3) 코드의 재사용성 증가

  • 팩토리는 재사용 가능한 컴포넌트로 설계되며, 다양한 컨텍스트에서 동일한 팩토리와 제품 클래스를 재사용할 수 있습니다.
  • 애플리케이션의 다른 부분에서 동일한 객체 생성 로직을 중복으로 구현할 필요가 없어, 유지 관리가 용이해집니다.

4) 시스템의 격리

  • 추상 팩토리는 클라이언트 코드를 구체적인 클래스의 구현으로부터 격리시킵니다. 이는 시스템의 다른 부분에 영향을 주지 않고 새로운 팩토리나 제품을 추가하거나 교체할 수 있게 해 줍니다.
  • 이러한 격리는 테스트와 유지 보수를 용이하게 하며, 대규모 시스템의 복잡성을 관리하는 데 도움이 됩니다.

5) 테스트 및 유지 보수의 용이성

  • 추상 팩토리 패턴은 단위 테스트와 통합 테스트를 보다 용이하게 합니다. 테스트 시에는 실제 구현 대신 목(mock)이나 스텁(stub) 객체를 사용할 수 있으며, 이는 테스트의 신뢰성과 일관성을 향상시킵니다.
  • 시스템의 유지 보수가 용이해지며, 새로운 기능이나 제품을 추가하거나 변경하는 작업이 간편해집니다.

6. 추상 팩토리 패턴의 한계 및 주의점

추상 팩토리 패턴은 객체 생성을 추상화하는 데 강력한 도구이지만, 모든 상황에서 이상적인 해결책은 아닙니다. 이 패턴의 사용에는 몇 가지 한계와 주의해야 할 점이 있습니다.

1) 복잡성 증가

  • 추상 팩토리 패턴은 추가적인 인터페이스와 클래스를 요구합니다. 이로 인해 시스템의 전체적인 복잡성이 증가할 수 있으며, 간단한 문제에 대해 과도한 설계로 이어질 수 있습니다.
  • 코드의 양이 증가하고, 구조가 복잡해짐에 따라 새로운 개발자가 시스템을 이해하고 유지 보수하는 데 어려움을 겪을 수 있습니다.

2) 유연성 제한

  • 팩토리가 생성하는 제품군이 확장되면, 모든 팩토리 클래스에 새로운 메서드를 추가해야 합니다. 이는 기존 코드의 수정을 필요로 하며, 오픈/클로즈 원칙을 위반할 수 있습니다.
  • 추상 팩토리 패턴은 고정된 제품 집합에 최적화되어 있어, 동적으로 변화하는 제품 요구사항에는 적합하지 않을 수 있습니다.

3) 오버엔지니어링의 위험

  • 간단한 요구사항에 대해 추상 팩토리 패턴을 사용하는 것은 오버엔지니어링으로 간주될 수 있습니다. 모든 설계 상황에서 이 패턴이 필요한 것은 아니며, 때로는 더 간단한 패턴이나 접근 방식이 더 적합할 수 있습니다.

4) 리팩토링의 어려움

이미 구현된 시스템에 추상 팩토리 패턴을 도입하려고 할 때, 기존 코드의 대규모 리팩토링이 필요할 수 있습니다. 이는 특히 크고 복잡한 시스템에서는 리스크가 될 수 있습니다.

5) 테스트와 디버깅의 복잡성

추상 팩토리 패턴을 사용하는 시스템은 테스트와 디버깅이 더 복잡해질 수 있습니다. 특히 팩토리가 여러 계층을 거쳐 객체를 생성하는 경우, 오류 추적과 문제 해결이 어려워질 수 있습니다.

7. 추상 팩토리 패턴과 다른 디자인 패턴과의 비교

추상 팩토리 패턴은 객체 생성을 추상화하는 디자인 패턴 중 하나입니다. 이 패턴은 다른 생성 관련 패턴들과 자주 비교되며, 각각의 패턴은 특정 문제 해결에 적합한 고유한 특징을 가지고 있습니다. 아래에서는 추상 팩토리 패턴을 팩토리 메서드 패턴, 빌더 패턴, 프로토타입 패턴과 비교하여 설명하겠습니다.

1) 팩토리 메서드 패턴과의 비교

  • 공통점: 두 패턴 모두 객체 생성의 책임을 클라이언트 코드에서 분리하여, 객체 생성과 관련된 로직을 캡슐화합니다.
  • 차이점: 추상 팩토리 패턴은 여러 종류의 관련된 객체들을 생성하는 인터페이스를 제공합니다. 이 패턴은 서로 다른 제품군을 생성하는 데 사용됩니다. 팩토리 메서드 패턴은 하나의 객체를 생성하는 인터페이스만 제공합니다. 이 패턴은 주로 하나의 제품에 대한 생성 로직의 확장성에 중점을 둡니다.

2) 빌더 패턴과의 비교

  • 공통점: 빌더 패턴과 추상 팩토리 패턴 모두 복잡한 객체의 생성 과정을 단순화하고, 객체 생성을 캡슐화합니다.
  • 차이점: 추상 팩토리 패턴은 관련 객체의 그룹을 생성하는 데 중점을 둡니다. 이 패턴은 주로 서로 다른 스타일 또는 테마의 제품군을 생성하는 데 사용됩니다. 빌더 패턴은 복잡한 객체의 단계적인 생성을 가능하게 합니다. 이 패턴은 생성 과정이 복잡하거나 여러 단계를 거쳐야 하는 객체에 적합합니다.

3) 프로토타입 패턴과의 비교

  • 공통점: 프로토타입 패턴과 추상 팩토리 패턴은 모두 객체 생성에 대한 유연성을 제공합니다.
  • 차이점: 추상 팩토리 패턴은 일련의 관련된 객체들을 생성하기 위한 인터페이스를 제공합니다. 이 패턴은 특히 서로 다른 환경이나 조건에서 동작하는 제품군을 생성할 때 유용합니다. 프로토타입 패턴은 기존 객체를 복제(clone)하여 새로운 객체를 생성합니다. 이 패턴은 복잡한 객체의 초기화 비용이 높거나 객체의 상태를 보존하는 것이 중요한 경우에 적합합니다.

마치며

추상 팩토리 패턴은 소프트웨어 설계에서 객체 생성의 복잡성을 관리하고 유연성을 극대화하는 데 있어 중요한 역할을 합니다. 이 패턴은 개발자가 플랫폼 독립적인 코드를 작성하도록 돕고, 시스템의 유지보수를 용이하게 만듭니다. 그러나 이 패턴을 사용할 때에는 오버엔지니어링을 피하고, 프로젝트의 요구사항에 적합한지 면밀히 검토해야 합니다. 잘 구현된 추상 팩토리 패턴은 소프트웨어의 설계와 유지보수를 한층 더 효율적이고 강력하게 만들 수 있습니다.