[GoF 디자인 패턴] 팩토리 메소드 패턴 (Factory method)과 추상 팩토리(Abstract factory) 패턴

JMM·2025년 1월 5일
0

GoF 디자인 패턴

목록 보기
2/11
post-thumbnail

1. 팩토리 메소드 패턴 : 구체적으로 어떤 인스턴스를 만들지는 서브 클래스가 정한다.

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

Before

Ship을 생성하는 과정에서 이름 검증, 준비, 커스터마이징, 알림 등 여러 작업을 단일 메서드 orderShip에 포함하고 있다.


ShipFactory

public class ShipFactory {

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

        prepareFor(name);

        Ship ship = new Ship();
        ship.setName(name);

        // Customizing for specific name
        if (name.equalsIgnoreCase("whiteship")) {
            ship.setLogo("\uD83D\uDEE5️");
        } else if (name.equalsIgnoreCase("blackship")) {
            ship.setLogo("⚓");
        }

        // coloring
        if (name.equalsIgnoreCase("whiteship")) {
            ship.setColor("whiteship");
        } else if (name.equalsIgnoreCase("blackship")) {
            ship.setColor("black");
        }

        // notify
        sendEmailTo(email, ship);

        return ship;
    }

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

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

}

문제점

1) 단일 책임 원칙 위반:

  • Ship 생성과 관련된 검증, 준비, 커스터마이징, 알림 등 모든 작업이 orderShip 메서드에 집중되어 있음.
  • 새로운 Ship 타입이 추가되면 orderShip 메서드를 수정해야 함.

2) 확장성 부족:
- 새로운 Ship 타입이 추가될 때마다 조건문을 추가해야 하므로 코드가 점점 복잡해짐.

3) 유지보수 어려움:

  • Ship 생성 로직과 관련 없는 작업(예: 검증, 알림)이 섞여 있어 유지보수가 어려움.

Ship

public class Ship {

    private String name;

    private String color;

    private String logo;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public String getLogo() {
        return logo;
    }

    public void setLogo(String logo) {
        this.logo = logo;
    }

    @Override
    public String toString() {
        return "Ship{" +
                "name='" + name + '\'' +
                ", color='" + color + '\'' +
                ", logo='" + logo + '\'' +
                '}';
    }
}
---

Client

public class Client {

    public static void main(String[] args) {
        Ship whiteship = ShipFactory.orderShip("Whiteship", "keesun@mail.com");
        System.out.println(whiteship);

        Ship blackship = ShipFactory.orderShip("Blackship", "keesun@mail.com");
        System.out.println(blackship);
    }

}

After

ShipFactory (인터페이스)

Ship 생성의 공통 로직ShipFactory 인터페이스에 정의하고, Ship 생성(createShip)은 하위 클래스가 구현하도록 분리.

public interface ShipFactory {

    default Ship orderShip(String name, String email) {
        validate(name, email);
        prepareFor(name);
        Ship ship = createShip(); //하위 클래스에서 구현
        sendEmailTo(email, ship);
        return ship;
    }

    void sendEmailTo(String email, Ship ship);

    Ship createShip();

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

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

}

DefaultShipFactory

public abstract class DefaultShipFactory implements ShipFactory {

    @Override
    public void sendEmailTo(String email, Ship ship) {
        System.out.println(ship.getName() + " 다 만들었습니다.");
    }

}

공통적으로 사용되는 메서드인 sendEmailTo를 기본 구현으로 제공함.


BlackshipFactory

public class BlackshipFactory extends DefaultShipFactory {
    @Override
    public Ship createShip() {
        return new Blackship();
    }
}

WhiteshipFactory

public class WhiteshipFactory extends DefaultShipFactory {

    @Override
    public Ship createShip() {
        return new Whiteship();
    }
}

BlackshipFactory & WhiteshipFactory

각각의 Ship 타입에 따라 구체적인 Ship 생성 로직을 구현.


Whiteship

public class Whiteship extends Ship {

    public Whiteship() {
        setName("whiteship");
        setLogo("\uD83D\uDEE5️");
        setColor("white");
    }
}

Blackship

public class Blackship extends Ship {

    public Blackship() {
        setName("blackship");
        setColor("black");
        setLogo("⚓");
    }
}

Ship 타입 구현
Ship 클래스를 상속받아 각각의 Ship에 맞는 속성을 설정.


Client

public class Client {

    public static void main(String[] args) {
        Client client = new Client();
        client.print(new WhiteshipFactory(), "whiteship", "keesun@mail.com");
        client.print(new BlackshipFactory(), "blackship", "keesun@mail.com");
    }

    private void print(ShipFactory shipFactory, String name, String email) {
        System.out.println(shipFactory.orderShip(name, email));
    }

}

사용자는 WhiteshipFactory 또는 BlackshipFactory를 통해 원하는 Ship을 생성할 수 있음.


개선된 설계의 장점

1) 단일 책임 원칙 준수:

  • ShipFactory는 Ship 생성 로직만 담당하며, 검증, 준비, 알림 등의 부가 작업은 각기 다른 메서드로 분리.

2) 확장성 향상:

  • 새로운 Ship 타입을 추가하려면 Ship과 관련 팩토리 클래스만 생성하면 됨. 기존 코드 수정 없이 새로운 기능 추가 가능(Open-Closed Principle 준수).

3) 유지보수 용이:

  • Ship 생성 관련 로직이 각각의 팩토리 클래스에 분리되어 있어 각 Ship에 대한 변경이 독립적임.

4) 코드 중복 제거:

  • 공통 로직(예: validate, prepareFor, sendEmailTo)은 상위 인터페이스나 추상 클래스에서 처리.


확장에 열려있고 변경에 닫혀있는 구조로 수정되었다!


팩토리 클래스 복습

  1. 팩토리 메소드 패턴 장단점:

    • 장점: 확장성, 유지보수 용이성, 코드 가독성, 의존성 감소.
    • 단점: 클래스 수 증가, 런타임 제어 부족.
  2. OCP (Open-Closed Principle):

    • 소프트웨어 엔터티는 확장에는 열려 있고, 변경에는 닫혀 있어야 함.
    • 새로운 기능 추가 시 기존 코드를 수정하지 않아야 함.
  3. 자바 8의 default 메소드:

    • 인터페이스에서 기본 구현을 제공할 수 있음.
    • 유지보수와 코드 중복 감소에 유리하지만, 다중 상속 충돌 문제와 설계 순수성 저하 단점이 있음.

팩토리 메소드 패턴 실무에서는?

1) 단순한 팩토리 패턴

  • 매개변수의 값에 따라 또는 메소드에 따라 각기 다른 인스턴스를 리턴하는 단순한 버전
    의 팩토리 패턴
  • java.lang.Calendar 또는 java.lang.NumberFormat

2)스프링 BeanFactory
- Object 타입의 Product를 만드는 BeanFacotry라는 Creator!


2. 추상 팩토리 패턴 : 서로 관련있는 여러 객체를 만들어주는 인터페이스

  • 구체적으로 어떤 클래스의 인스턴스를(concrete product)를 사용하는지 감출 수 있다.

추상 팩토리(Abstract factory) 패턴 구현 방법

  • 클라이언트 코드에서 구체적인 클래스의 의존성을 제거한다.

Before

WhiteshipFactory

  • WhiteshipFactory에서 Ship 생성 시 직접 WhiteAnchorWhiteWheel을 생성하여 의존성을 가지고 있음.
  • 새로운 Anchor나 Wheel 타입(예: Pro 버전)을 추가할 경우, 기존 WhiteshipFactory 코드를 수정해야 하므로 OCP(확장에는 열려 있고, 변경에는 닫혀 있어야 한다) 원칙을 위반.
public class WhiteshipFactory extends DefaultShipFactory {

    @Override
    public Ship createShip() {
        Ship ship = new Whiteship();
        ship.setAnchor(new WhiteAnchor()); // 의존성 직접 생성
        ship.setWheel(new WhiteWheel());  // 의존성 직접 생성
        return ship;
    }
}

After

Abstract Factory 패턴 적용

  • Ship의 부품(Anchor, Wheel)을 생성하는 책임을 ShipPartsFactory라는 인터페이스로 분리.
  • 각 부품의 구현체(WhiteAnchor, WhiteWheel 등)를 생성하는 역할은 구체 팩토리 클래스(예: WhiteshipPartsFactory, WhitePartsProFactory)가 담당.
  • WhiteshipFactoryShipPartsFactory를 의존성으로 받아 부품 생성을 위임하여 확장성을 높임.

설계 요소

1. ShipPartsFactory (추상 팩토리)

  • Ship의 Anchor와 Wheel을 생성하는 추상 팩토리 인터페이스.
  • 부품을 생성하는 공통 메서드(createAnchor, createWheel)를 정의.
public interface ShipPartsFactory {
    Anchor createAnchor();
    Wheel createWheel();
}

2. WhiteshipPartsFactory (구체 팩토리)

  • 기본 부품(WhiteAnchor, WhiteWheel)을 생성하는 팩토리.
public class WhiteshipPartsFactory implements ShipPartsFactory {

    @Override
    public Anchor createAnchor() {
        return new WhiteAnchor();
    }

    @Override
    public Wheel createWheel() {
        return new WhiteWheel();
    }
}

3. WhitePartsProFactory (구체 팩토리)

  • Pro 버전 부품(WhiteAnchorPro, WhiteWheelPro)을 생성하는 팩토리.
public class WhitePartsProFactory implements ShipPartsFactory {

    @Override
    public Anchor createAnchor() {
        return new WhiteAnchorPro();
    }

    @Override
    public Wheel createWheel() {
        return new WhiteWheelPro();
    }
}

4. WhiteshipFactory (ShipFactory 구현체)

  • Ship의 부품 생성 책임을 ShipPartsFactory로 위임.
  • 새로운 부품 타입이 추가되어도 팩토리를 교체하는 방식으로 확장 가능.
public class WhiteshipFactory extends DefaultShipFactory {

    private ShipPartsFactory shipPartsFactory;

    public WhiteshipFactory(ShipPartsFactory shipPartsFactory) {
        this.shipPartsFactory = shipPartsFactory; // 의존성 주입
    }

    @Override
    public Ship createShip() {
        Ship ship = new Whiteship();
        ship.setAnchor(shipPartsFactory.createAnchor()); // 부품 생성 위임
        ship.setWheel(shipPartsFactory.createWheel());   // 부품 생성 위임
        return ship;
    }
}

다이어그램


Pro 버전 추가

1. 새로운 부품 클래스

  • Pro 버전 Anchor와 Wheel 구현체를 추가.
public class WhiteAnchorPro implements Anchor {
    // Pro 버전 Anchor 구현
}

public class WhiteWheelPro implements Wheel {
    // Pro 버전 Wheel 구현
}

2. Pro 버전 팩토리

  • Pro 버전 부품을 생성하는 WhitePartsProFactory.
public class WhitePartsProFactory implements ShipPartsFactory {
    @Override
    public Anchor createAnchor() {
        return new WhiteAnchorPro();
    }

    @Override
    public Wheel createWheel() {
        return new WhiteWheelPro();
    }
}

ShipInventory (클라이언트 코드)

ShipInventory

  • 클라이언트는 WhiteshipFactory를 사용하되, 필요한 부품 팩토리(기본 또는 Pro)를 전달받아 Ship을 생성.
  • 팩토리를 교체하는 방식으로 쉽게 확장 가능.
public class ShipInventory {

    public static void main(String[] args) {
        // 기본 부품 팩토리 사용
        ShipFactory shipFactory = new WhiteshipFactory(new WhiteshipPartsFactory());
        Ship ship = shipFactory.createShip();
        System.out.println(ship.getAnchor().getClass()); // WhiteAnchor
        System.out.println(ship.getWheel().getClass());  // WhiteWheel

        // Pro 부품 팩토리 사용
        ShipFactory proShipFactory = new WhiteshipFactory(new WhitePartsProFactory());
        Ship proShip = proShipFactory.createShip();
        System.out.println(proShip.getAnchor().getClass()); // WhiteAnchorPro
        System.out.println(proShip.getWheel().getClass());  // WhiteWheelPro
    }
}

Abstract Factory 패턴의 장점

  1. 확장성:

    • 새로운 부품 타입(Pro 버전 등)이 추가되어도 기존 팩토리와 Ship 코드를 수정할 필요가 없음.
    • 단지 새로운 팩토리를 구현하고 교체하면 됨(OCP 준수).
  2. 유지보수 용이성:

    • 객체 생성 로직(부품 생성)이 ShipPartsFactory에 캡슐화되어 유지보수가 쉬움.
    • 부품 생성 로직과 Ship 조립 로직이 분리되어 역할이 명확.
  3. 의존성 감소:

    • WhiteshipFactory는 구체적인 부품 클래스(예: WhiteAnchor, WhiteWheel)에 의존하지 않음.
    • 대신 인터페이스(ShipPartsFactory)에 의존하므로 의존성이 낮아지고 테스트가 쉬워짐.

Abstract Factory 패턴의 단점

  1. 복잡성 증가:

    • 부품별로 팩토리 클래스와 구현체가 추가되므로 클래스 수가 증가.
  2. 범용성이 제한:

    • 특정한 제품 계열(Anchor, Wheel 등)만 생성할 수 있으므로 다른 종류의 객체 생성에는 사용할 수 없음.

팩토리 메소드 패턴과 추상 팩토리(Abstract Factory) 패턴의 차이


1. 공통점

  1. 구체적인 객체 생성 과정을 추상화:

    • 둘 다 객체 생성 로직을 캡슐화하여 클라이언트가 객체 생성 방식을 알 필요가 없도록 함.
  2. 클라이언트 코드의 의존성 감소:

    • 클라이언트는 객체의 구체적인 클래스 대신 팩토리나 인터페이스를 통해 객체를 생성.

2. 차이점

특징팩토리 메소드 패턴추상 팩토리 패턴
중점"팩토리를 구현하는 방법 (상속)""팩토리를 사용하는 방법 (구성)"
구조- 팩토리 메소드를 가진 클래스에서 객체 생성 책임을 하위 클래스로 위임.- 관련 있는 여러 객체(Product)를 생성하는 팩토리를 정의.
객체 생성 방식단일 객체 생성 (한 번에 하나의 Product 생성).계열(related)의 여러 객체를 한꺼번에 생성.
확장성새로운 객체를 추가하려면 하위 클래스를 생성.새로운 객체 계열을 추가하려면 팩토리를 구현.
사용 사례구체적인 객체 생성 과정을 하위 클래스에서 오버라이드할 때 사용.관련 있는 여러 객체를 함께 생성해야 할 때 사용.

팩토리 메소드 패턴과 추상 팩토리 패턴의 관점 차이

  1. 팩토리 메소드 패턴:

    • 상속(Inheritance)을 사용하여 객체 생성 로직을 하위 클래스로 옮기는 데 초점.
    • 목적: 구체적인 객체 생성 과정을 하위 클래스에서 구현.

    예제:

    public abstract class ShapeFactory {
        public abstract Shape createShape();
    }
    
    public class CircleFactory extends ShapeFactory {
        @Override
        public Shape createShape() {
            return new Circle();
        }
    }
  2. 추상 팩토리 패턴:

    • 구성(Composition)을 사용하여 관련 객체를 함께 생성하는 데 초점.
    • 목적: 관련 있는 여러 객체를 구체적인 클래스에 의존하지 않고 생성.

    예제:

    public interface WidgetFactory {
        Button createButton();
        Checkbox createCheckbox();
    }
    
    public class MacOSWidgetFactory implements WidgetFactory {
        @Override
        public Button createButton() {
            return new MacOSButton();
        }
    
        @Override
        public Checkbox createCheckbox() {
            return new MacOSCheckbox();
        }
    }

추상 팩토리 패턴의 실무 사용 사례

1. 자바 표준 라이브러리

  • 자바 표준 라이브러리에는 추상 팩토리 패턴의 구현이 여러 곳에서 사용된다.
  • 대표적인 예는 XML 처리와 관련된 팩토리 클래스들이다.
1.1 javax.xml.xpath.XPathFactory#newInstance()
  • 역할: XPath 표현식을 처리하기 위한 팩토리 객체 생성.

  • 사용 예제:

    import javax.xml.xpath.XPath;
    import javax.xml.xpath.XPathFactory;
    
    public class Main {
        public static void main(String[] args) {
            XPathFactory factory = XPathFactory.newInstance();
            XPath xPath = factory.newXPath();
            System.out.println("XPath 객체 생성: " + xPath);
        }
    }
1.2 javax.xml.transform.TransformerFactory#newInstance()
  • 역할: XML 변환을 수행하는 Transformer 객체를 생성하는 팩토리.
1.3 javax.xml.parsers.DocumentBuilderFactory#newInstance()
  • 역할: XML 파싱을 위한 DocumentBuilder 객체를 생성하는 팩토리.

2. 스프링에서의 사용

2.1 FactoryBean 인터페이스
  • 스프링에서는 FactoryBean 인터페이스를 사용하여 빈 생성 로직을 커스터마이징할 수 있다.
  • 역할: 복잡한 객체 생성 로직을 캡슐화하여, 클라이언트가 객체 생성 과정을 알 필요 없도록 함.

예제:

import org.springframework.beans.factory.FactoryBean;

public class MyFactoryBean implements FactoryBean<MyService> {

    @Override
    public MyService getObject() throws Exception {
        // 복잡한 객체 생성 로직
        return new MyService();
    }

    @Override
    public Class<?> getObjectType() {
        return MyService.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

사용:

  • XML 또는 애노테이션 설정에서 FactoryBean을 등록하여 클라이언트가 원하는 빈을 주입받을 수 있음.
2.2 스프링 빈 설정
  • XML 예제:
    <bean id="myService" class="com.example.MyFactoryBean" />

요약

  1. 팩토리 메소드 패턴 vs 추상 팩토리 패턴:

    • 팩토리 메소드 패턴: 객체 생성 과정을 하위 클래스로 옮기는 데 초점(상속).
    • 추상 팩토리 패턴: 관련 있는 여러 객체를 생성하는 데 초점(구성).
  2. 추상 팩토리 패턴의 사용 목적:

    • 여러 객체(Product)를 생성해야 하는 경우, 객체 간의 관계를 유지하면서 구체적인 클래스에 의존하지 않고 객체를 생성.
  3. 실무에서의 사용 사례:

    • 자바 라이브러리: XPathFactory, TransformerFactory, DocumentBuilderFactory.
    • 스프링: FactoryBean을 통해 복잡한 객체 생성 로직 캡슐화.

추상 팩토리 패턴은 객체 생성의 복잡성을 줄이고, 확장성유지보수성을 높이는 데 매우 유용하다.


더 나아가 추상 팩토리 패턴이 SRP를 위반할 수 있다고 생각하여 찾아본 결과,

1) 추상 팩토리 패턴은 SRP를 위반할 수 있음:

  • 여러 객체(Product)를 생성하는 단일 팩토리가 서로 다른 책임(Anchor, Wheel 생성)을 가질 경우, 변경 사유가 여러 가지로 복잡해질 수 있음.

2) SRP 준수를 위한 개선:

  • 객체별로 팩토리를 분리하거나 내부적으로 구성(Composition)을 활용하여 책임을 분리.

3) 실무에서의 타협:

  • 단일 팩토리로 설계하는 것이 더 간단하고 효율적이라면 SRP를 일부 타협적으로 적용.
  • 복잡한 프로젝트에서는 객체별 팩토리 분리를 통해 SRP를 준수.

4) 최적의 설계는 상황에 따라 다름:

  • SRP, DRY, 유지보수성을 균형 있게 고려하여 설계.
  • 작은 규모의 프로젝트에서는 단순화된 설계, 대규모 프로젝트에서는 엄격한 SRP 준수가 적합.

trade off를 잘 따져보고 설계를 해봐야겠다 !

출처 : 코딩으로 학습하는 GoF의 디자인 패턴

0개의 댓글