추상 팩터리 패턴 (Abstract Factory Pattern)

초코칩·2024년 4월 15일
0

Design Pattern

목록 보기
4/5
post-thumbnail

추상 팩터리

추상 팩토리는 관련 객체들의 구상 클래스들을 지정하지 않고도 관련 객체들의 모음을 생성할 수 있도록 하는 생성패턴이다.

문제점

가구 판매장을 위한 프로그램을 만들고 있다. 현재 생성해야 되는 가구와 제춤군을 다음과 같다.

  • 관련 제품들로 형성된 패밀리(제품군): Chair(의자) + Sofa(소파) + CoffeeTable(커피 테이블)

  • 해당 제품군의 디자인: Modern(현대식), Victorian(빅토리안), ArtDeco(아르데코 양식)

이제 새로운 개별 가구 객체를 생성했을 때, 이 객체들이 기존의 같은 제품군 내에 있는 다른 가구 객체들과 스타일(디자인)을 가지도록 할 방법이 필요하다.

또, 가구 공급업체들은 카탈로그를 매우 자주 변경하기 때문에, 그들은 새로운 제품 또는 제품군(패밀리)을 추가할 때마다 기존 코드를 변경해야 하는 번거로움을 피하고 싶을 것이다.

해결

개별 인터페이스 명시

추상 팩터리 패턴의 첫 번째 방안은 각 제품 제품군에 해당하는 개별적인 인터페이스를 명시적으로 선언하는 것이다. (예: 의자, 소파 또는 커피 테이블).

그 다음, 제품의 모든 변형이 위 인터페이스를 따르도록 한다. 예를 들어, 모든 의자의 변형들은 Chair(의자) 인터페이스를 구현한다; 모든 커피 테이블 변형들은 CoffeeTable(커피 테이블) 인터페이스를 구현한다, 등의 규칙을 명시합니다.

추상 팩터리 선언

추상 공장 패턴의 다음 단계는 추상 팩토리 패턴을 선언하는 것입니다. 추상 공장 패턴은 제품 패밀리 내의 모든 개별 제품들의 생성 메서드들이 목록화되어있는 인터페이스입니다. (예: createChair(의자 생성), createSofa(소파 생성), createCoffeeTable(커피 테이블 생성) 등).

다음은 제품 변형을 다룰 차례입니다. 제품 패밀리의 각 변형에 대해 AbstractFactory 추상 팩토리 인터페이스를 기반으로 별도의 팩토리 클래스를 생성합니다. 팩토리는 특정 종류의 제품을 반환하는 클래스입니다. 예를 들어 Modern­Furniture­Factory​(현대식 가구 팩토리)​에서는 다음 객체들만 생성할 수 있습니다: Modern­Chair​(현대식 의자), Modern­Sofa​(현대식 소파) 및 Modern­Coffee­Table​(현대식 커피 테이블).

클라이언트 코드는 자신에 해당하는 추상 인터페이스를 통해 팩토리들과 제품들 모두와 함께 작동해야 합니다. 그래야 클라이언트 코드에 넘기는 팩토리의 종류와 제품 변형들을 클라이언트 코드를 손상하지 않으며 자유자재로 변경할 수 있습니다.

클라이언트 코드

클라이언트가 팩토리에 의자를 주문했다고 가정해 봅시다. 클라이언트는 팩토리의 클래스들을 알 필요가 없으며, 팩토리가 어떤 변형의 의자를 생성할지 전혀 신경을 쓰지 않습니다. 클라이언트는 추상 Chair(의자) 인터페이스를 사용하여, 현대식 의자이든 빅토리아식 의자이든 종류에 상관없이 모든 의자를 항상 동일한 방식으로 주문하며, 그가 의자에 대해 아는 유일한 사실은 제품이 sitOn(앉을 수 있다) 메서드를 구현한다는 것뿐입니다. 그러나, 생성된 의자의 변형은 항상 같은 팩토리 객체에서 생성된 소파 또는 커피 테이블의 변형과 같을 것입니다.

여기에서 명확히 짚고 넘어가야 할 점이 있습니다. 클라이언트가 추상 인터페이스에만 노출된다면 실제 팩토리 객체를 생성하는 것은 무엇일까요? 일반적으로 프로그램은 초기화 단계에서 구상 팩토리 객체를 생성합니다. 그 직전에 프로그램은 환경 또는 구성 설정에 따라 팩토리 유형을 선택해야 합니다.

적용

적용 전

클라이언트가 직접 ModernChair, VictorianChair, ModernSofa, VictorianSofa와 같은 구상 클래스를 생성하고 있다. 이는 클라이언트 코드가 특정 의자와 소파 구현에 의존함을 의미합니다. 때문에 만약 새로운 종류의 의자나 소파가 추가되거나 기존의 의자나 소파 구현이 변경되면 클라이언트 코드도 수정해야 합니다. 이는 유지보수가 어렵고 확장성이 낮은 코드를 만들게 됩니다.

// 의자 클래스들
class ModernChair {
    void sitOn() {
        System.out.println("Sitting on a modern chair.");
    }
}

class VictorianChair {
    void sitOn() {
        System.out.println("Sitting on a Victorian chair.");
    }
}

// 소파 클래스들
class ModernSofa {
    void lieOn() {
        System.out.println("Lying on a modern sofa.");
    }
}

class VictorianSofa {
    void lieOn() {
        System.out.println("Lying on a Victorian sofa.");
    }
}

// 클라이언트 코드
public class Client {
    public void orderModernChair() {
        ModernChair chair = new ModernChair();
        chair.sitOn();
    }

    public void orderVictorianChair() {
        VictorianChair chair = new VictorianChair();
        chair.sitOn();
    }

    public void orderModernSofa() {
        ModernSofa sofa = new ModernSofa();
        sofa.lieOn();
    }

    public void orderVictorianSofa() {
        VictorianSofa sofa = new VictorianSofa();
        sofa.lieOn();
    }

    public static void main(String[] args) {
        Client client = new Client();
        client.orderModernChair();
        client.orderVictorianChair();
        client.orderModernSofa();
        client.orderVictorianSofa();
    }
}

적용 후

추상 팩터리 패턴을 적용하여 각각의 가구 팩토리는 관련된 의자와 소파를 생성하며, 클라이언트는 필요한 가구 팩토리를 선택하여 사용할 수 있습니다. 클라이언트는 추상 인터페이스를 통해 팩토리에서 생성된 제품들을 사용하므로, 구상 클래스에 대한 의존성이 감소하고 유연성과 확장성이 향상됩니다.

// 추상 의자 인터페이스
interface Chair {
    void sitOn();
}

// 추상 소파 인터페이스
interface Sofa {
    void lieOn();
}

// 현대식 의자 구현
class ModernChair implements Chair {
    @Override
    public void sitOn() {
        System.out.println("Sitting on a modern chair.");
    }
}

// 빅토리아식 의자 구현
class VictorianChair implements Chair {
    @Override
    public void sitOn() {
        System.out.println("Sitting on a Victorian chair.");
    }
}

// 현대식 소파 구현
class ModernSofa implements Sofa {
    @Override
    public void lieOn() {
        System.out.println("Lying on a modern sofa.");
    }
}

// 빅토리아식 소파 구현
class VictorianSofa implements Sofa {
    @Override
    public void lieOn() {
        System.out.println("Lying on a Victorian sofa.");
    }
}

// 추상 가구 팩토리 인터페이스
interface FurnitureFactory {
    Chair createChair();
    Sofa createSofa();
}

// 현대식 가구 팩토리 구현
class ModernFurnitureFactory implements FurnitureFactory {
    @Override
    public Chair createChair() {
        return new ModernChair();
    }

    @Override
    public Sofa createSofa() {
        return new ModernSofa();
    }
}

// 빅토리아식 가구 팩토리 구현
class VictorianFurnitureFactory implements FurnitureFactory {
    @Override
    public Chair createChair() {
        return new VictorianChair();
    }

    @Override
    public Sofa createSofa() {
        return new VictorianSofa();
    }
}

// 클라이언트 코드
public class Client {
    private FurnitureFactory factory;

    public Client(FurnitureFactory factory) {
        this.factory = factory;
    }

    public void orderChairAndSofa() {
        Chair chair = factory.createChair();
        Sofa sofa = factory.createSofa();

        chair.sitOn();
        sofa.lieOn();
    }

    public static void main(String[] args) {
        FurnitureFactory modernFactory = new ModernFurnitureFactory();
        Client modernClient = new Client(modernFactory);
        modernClient.orderChairAndSofa();

        FurnitureFactory victorianFactory = new VictorianFurnitureFactory();
        Client victorianClient = new Client(victorianFactory);
        victorianClient.orderChairAndSofa();
    }
}

장단점

장점

  • OCP(개방 폐쇄 원칙): 기존 클라이언트 코드를 훼손하지 않고 제품의 새로운 변형들을 생성할 수 있습니다. 새로운 요구사항이나 기능 추가 시 기존 코드에 최소한의 영향을 미치게 된다.
  • 단일 책임 원칙. 제품 생성 코드를 한 곳으로 추출하여 코드를 더 쉽게 유지보수할 수 있습니다.
  • 의존성 감소: 구상 제품들과 클라이언트 코드 사이의 단단한 결합을 피할 수 있습니다.

단점

  • 클래스 계층 구조의 복잡성: 패턴과 함께 새로운 인터페이스들과 클래스들이 많이 도입되기 때문에 코드가 필요 이상으로 복잡해질 수 있습니다.

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

Ref

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

profile
초코칩처럼 달콤한 코드를 짜자

0개의 댓글