본문 바로가기
Computer Sience/Desgin Pattern

[Design Pattern] 추상 팩토리(Abstract Factory) 패턴

by 제우제우 2024. 10. 18.

추상 팩토리 (Abstract factory) 패턴?

추상 팩토리 패턴 (Abastract Factory Pattern)은 객체 생성과 관련된 디자인 패턴 중 하나이다.

관련된 객체들의 그룹을 생성하는 인터페이스를 제공하는 패턴이다. 

즉 상세한 클래스 명시 없이 객체를 생성할 수 있는 방법을 제공한다. 

이 패턴은 팩토리 메소드 패턴보다 한층 추상화된 구조

여러 제품 군을 생성할 수 있는 팩토리들을 조직화하는데 사용된다.

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

 

추상 팩토리 패턴 적용  Before

public interface Anchor {
}

public class WhiteAnchor implements Anchor {
}

public interface Wheel {
}

public class WhiteWheel implements Wheel {
}


public class Ship {
    private String name;
    private String color;
    private Wheel wheel;
    private Anchor anchor;
    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 Wheel getWheel() {
        return wheel;
    }
    public void setWheel(Wheel wheel) {
        this.wheel = wheel;
    }
    public Anchor getAnchor() {
        return anchor;
    }
    public void setAnchor(Anchor anchor) {
        this.anchor = anchor;
    }

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

public class BlackShip extends Ship{
    public BlackShip(){
        setName("blackship");
        setColor("black");
    }
}

public class WhiteShip extends Ship {
    public WhiteShip (){
        setName("whiteship");
        setColor("white");
    }
}

public interface ShipFactory {
    default Ship orderShip(String name, String email) {
        validate(name, email);
        prepareFor(name);
        Ship ship = createShip();
        sendEmailTo(email, ship);
        return ship;
    }
    // 2개의 추상 메소드
    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 + " 만들 준비 중");
    }
}

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

 

클래스 Ship 

클래스 BlackShip extends Ship

클래스 WhiteShip extends Ship

 

WhiteShip & BlackShip은 Ship 종류이다. 

이전에 Ship 인스턴스를 만들 때 팩토리 메소드 패턴을 적용해서 
배의 기본 공정은 Creator: ShipFactory<인터페이스> default 메소드(자바8) & private 메소드(자바9) 
배의 생성 책임을 구체적인 ConcreteCreator: WhiteShipFactory / BlackShipFactory 에게 위임했었다. 

팩토리 메소드 패턴의 적용을 통해 OCP가 지켜졌다. 

https://20240228.tistory.com/344

 

이번 예제에서는 Ship의 구성품(Parts)가 추가되었다. 

인터페이스 Anchor / 인터페이스 Wheel 

 

2개의 구성품 Anchor와 Wheel은 여러 종류의 인스턴스가 생성될 가능성이 있어서 인터페이스 타입으로 만들고

구체적인 구성품은 해당 인터페이스를 구현하도록 하였다.

 

이제 그럼 해당 구성품을 조립해서 생성하는 

기존 ConcreteCreator인 WhiteShipFactory를 리팩토링 해보겠다. 

public class WhiteShipFactory extends DefaultShipFactory {
    @Override
    public Ship createShip() {
        Ship ship = new WhiteShip();
        ship.setAnchor(new WhiteAnchor());
        ship.setWheel(new WhiteWheel());
        return ship;
    }
}

 

ShipFactory<인터페이스> → DefaultShipFactory<추상 클래스> (sendEmailTo 메소드 구현) 

→ WhiteShipFactory<클래스> createShip 메소드 구현 

 

WhiteShipFactory 에서는

WhiteShip 생성자를 통해서 기본적인 이름 색깔을 지정했다. 

그 이후에 구성품인 Anchor와 Wheel을 지정하고 있다. 

 

만약? 구성품이 바뀌면 어떻게 될까? 

ex) WhiteAnchor → AdvancdedWhiteAnchor 

ex) WhiteWheel →  AdvancdedWhiteWheel

 

현재는 구성품이 2개밖에 없지만 많다고 생각해 보자.

그럼 바뀌는 구성품의 클라이언트 코드를 전부 수정해야 한다.

여기서 말하는 클라이언트는 구성품들을 사용하여 배 인스턴스를 생성하는 WhiteShipFactory가 클라이언트이다. 

 

또한 이런 구성품 조합이 1개가 아니라 N개라고 해보자 

현재 구성품은 배의 기본적인 부품들 세트이지만 배의 또 다른 구성품 (침대 / 침대 시트)

이런 비슷한 성향을 가지는 세트 부품들이 또 존재할 것이다. 

또한 확장 가능성이 존재한다고 생각해 보자 

그럼 매번 클라이언트 코드를 전부 수정해야 할까?

 

해당 문제를 추상 펙토리 패턴을 적용해서 해결해 보자 

추상 팩토리 패턴 적용  After

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

/**
 * Abstract Factory 추상 펙토리
 * 서로 관련있는 여러 객체를 만들어주는 인터페이스
 */
public interface ShipPartsFactory {
    Anchor createAnchor();
    Wheel createWheel();
}

public class WhiteShipPartsFactory implements ShipPartsFactory {
    @Override
    public Anchor createAnchor() {
        return new WhiteAnchor();
    }
    @Override
    public Wheel createWheel() {
        return new WhiteWheel();
    }
}

public class WhiteShipFactory extends DefaultShipFactory {
    private ShipPartsFactory shipPartsFactory;
    public WhiteShipFactory(ShipPartsFactory shipPartsFactory){
        this.shipPartsFactory = shipPartsFactory;
    }
    /**
     * 구체적으로 어떤 클래스의 인스턴스를(concrete product)를 사용하는지 감출 수 있다
     */
    @Override
    public Ship createShip() {
        Ship ship = new WhiteShip();
        ship.setAnchor(shipPartsFactory.createAnchor());
        ship.setWheel(shipPartsFactory.createWheel());
        return ship;
    }
}

 

ShipPartsFactory <인터페이스>  추상 펙토리이다. 

Anchor & Wheel 생성하는 추상 메소드가 존재한다. 

 

Anchor & Wheel 조합을 원하는 조합으로 생성해 주고 싶으면 이제 

ShipPartsFactory를 구현하고 해당 구현체를 배를 생성하는 Factory에 DI 해주면 된다. 

이를 통해서 클라이언트 코드인 WhiteShipFactory는 어떤 구체적인 클래스를 사용하는지에 대한 숨김

즉 캡슐화되었고 DI 해주는 ShipPartsFactory만 바꿔주면 조합 또한 쉽게 변경이 가능해졌다. 

 

구성품이 바뀐다면?(확장)

public class WhiteAnchorPro implements Anchor {
}

public class WhiteWheelPro implements Wheel {
}

public class WhiteShipProPartsFactory implements ShipPartsFactory {
    @Override
    public Anchor createAnchor() {
        return new WhiteAnchorPro();
    }
    @Override
    public Wheel createWheel() {
        return new WhiteWheelPro();
    }
}

public class WhiteShipFactory extends DefaultShipFactory {
    private ShipPartsFactory shipPartsFactory;
    public WhiteShipFactory(ShipPartsFactory shipPartsFactory){
        this.shipPartsFactory = shipPartsFactory;
    }
    /**
     * 구체적으로 어떤 클래스의 인스턴스를(concrete product)를 사용하는지 감출 수 있다
     */
    @Override
    public Ship createShip() {
        Ship ship = new WhiteShip();
        ship.setAnchor(shipPartsFactory.createAnchor());
        ship.setWheel(shipPartsFactory.createWheel());
        return ship;
    }
}

Pro 이름을 붙여서 구성품을 추가하였다. 

새로 추가된 구성품의 조합을 생성해 주는 Factory를 만들어주었다. 

→ WhiteShipProPartsFactory extends ShipPartsFactory(추상 펙토리)

 

이제 WhiteShip 생산을 Pro 구성품으로 바꾼다고 했을 때 

생산을 하는 클라이언트 코드인 WhiteShipFactory 코드가 바뀔까?

 

전혀 바뀌지 않는다. 

ShipPartsFactory 주입만 구현체를 바꿔서 해주면 끝이다. 

public class ShipInventory {
    public static void main(String[] args) {
        // ShipFactory shipFactory = new WhiteShipFactory(new WhiteShipPartsFactory());
        ShipFactory shipFactory = new WhiteShipFactory(new WhiteShipProPartsFactory());
        WhiteShip ship = (WhiteShip)shipFactory.createShip();
        System.out.println(ship.getAnchor().getClass());
        System.out.println(ship.getWheel().getClass());
    }
}

WhiteShipFactory 생성자에 WhiteShipProPartsFactory || WhiteShipPartsFactory 어떤 펙토리를 제공하는지에 따라서 

구성품이 달라진다. 

추상 팩토리 패턴 VS 팩토리 메소드 패턴 

팩토리 메소드 패턴과 추상 팩토리 패턴은 서로 매우 닮았는데 뭐가 다를까?

GoF의 디자인 패턴들은 코드만 보면 구분이 잘 안되는 경우가 많다. 

하지만 패턴을 목적과 사용의 관점으로 바라보면 쉽게 구분할 수 있다. 

 

모양이 비슷하다 

추상 팩토리와 팩토리 메소드 패턴 모두 구체적인 객체 생성 과정을 추상화한 인터페이스를 제공한다. 

 

관점이 다르다  . 

팩토리 메소드 패턴은 팩토리를 구현하는 방법 (Inheritance)에 초점을 둔다. 

추상 팩토리 패턴은 팩토리를 사용하는 방법 (Composition)에 초점을 둔다. 

 

목적이 다르다 

팩토리 메소드 패턴은 구체적인 객체 생성 과정을 구체적인 클래스로 옮기는 것이 목적이다. 

추상 팩토리 패턴은 관련있는 여러 객체를 구체적인 클래스에 의존하지 않고 묶어서 만들 수 있게 해주는 것이 목적이다. 

추상 팩토리 패턴 장단점 

장점 

구체적인 클래스와의 의존성 제거(캡술화)

클라이언트는 구체적인 클래스에 의존하지 않고, 추상 팩토리 인터페이스에만 의존하기 때문에 

구체적인 클래스의 변경이 클라이언트 코드에 영향을 미치지 않는다. 

즉, 구현 세부사항을 감추고 클라이언트는 제품 생성 과정에 대해 몰라도 된다. (SRP)

 

확장성

새로운 제품군이나 구성을 추가할 때 기존 코드를 수정할 필요 없이 새로운 팩토리를 추가하기만 하면 된다. 

이로 인해 시스템이 확장될 때 유연성이 높다. (OCP)

단점 

설계의 복잡성 증가

구조가 복잡해진다. 초기 개발 단계에서 구현해야 할 코드의 양이 늘어난다. 

구체적인 제품군이 많아지면 관리가 어려워진다. 

확장 가능성이 많고 여러가지 객체의 조합을 사용할 가능성이 많다면 
추상 팩토리 패턴을 고려해야겠다.

스프링 프레임워크 추상 팩토리 패턴 사용 

/**
 * FactoryBean -> Abstract Factory
 * Ship 인스턴스를 Bean 만들어준다.
 * 스프링 내부에서 사용
 * 이렇게 확장이 가능한 이유: OCP 지켜지기 때문에
 */
public class ShipFactory implements FactoryBean<Ship> {
    @Override
    public Ship getObject() throws Exception {
        Ship ship = new WhiteShip();
        ship.setName("hello");
        return ship;
    }
    @Override
    public Class<?> getObjectType() {
        return Ship.class;
    }
}

@Configuration
public class FactoryBeanConfig {
    @Bean
    public ShipFactory shipFactory(){
        return new ShipFactory();
    }
}

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(FactoryBeanConfig.class);
        ShipFactory bean = ac.getBean(ShipFactory.class);
        Ship shipBean = ac.getBean(Ship.class);
        System.out.println(shipBean.getName());
    }
}

출력
hello

참고자료

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4

 

코딩으로 학습하는 GoF의 디자인 패턴 강의 | 백기선 - 인프런

백기선 | 디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를

www.inflearn.com