본문 바로가기
Design Pattern/Creational Design

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

by song.ift 2023. 3. 28.

디자인 패턴 유형

  • Creation Design Pattern
    • 객체의 생성에 관련된 패턴
    • 객체의 생성 로직을 숨김으로써 유연성을 향상시킨다.
  • Strutural Design Pattern
    • 클래스와 객체의 구성에 관련된 패턴
  • Behavioral Design Pattern
    • 객체와 객체 간의 통신에 관련된 패턴

 

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

추상 팩토리 패턴은 구체적인 클래스에 의존하지 않고, 서로 연관되거나 의존적인 객체들의 조합을 만드는 인터페이스를 제공하는 패턴이다. 관련성 있는 여러 종류의 객체를 일관된 방식으로 생성할 때 유용하다. 팩토리 패턴을 확장한 패턴이라고 할 수 있다.

  1. 객체를 사용하는 코드에서 객체의 생성을 떼어내서 별도의 객체에 추상화 한다.
  2. 상속관계의 경우, 상위 객체가 중요한 뼈대를 담당하고, 하위 객체에서 객체 생성에 관한 구체적인 내용을 결정한다.

 

이름은 비슷하지만, 팩토리 패턴과의 차별성

추상 팩토리 패턴은 여러 개의 팩토리 메서드를 그룹으로 묶은 것과 유사하다. 추상 팩토리는 다양한 객체 생성 과정에서 그룹화가 필요할 때, 매우 유용한 패턴이며 공장의 개념을 추상화한 것이다. (팩토리 메서드는 추상 팩토리와 동일하게 추상화 과정을 적용할 수 있지만, 단일 그룹으로 제한한다.) 

 

기존 팩토리의 구현과 문제점

추상 팩토리. 추상화(Abstact) + 팩토리(Factory)를 합친 단어이다. 먼저 단어부터 뜯어 보도록 하면. 추상화란, 구체화의 반대말로 생각하면 편하다. 실제 코드 부분이 작성되지 않고 어떻게 사용할지 명세(인터페이스라고 한다)만 정의하는 것이다. 그렇다면 팩토리는 무엇인가. 팩토리를 이해하기 위해서 아래 코드를 살펴보도록 하자.

function takeOutCoffee(type) {
    let coffee;

    if (type === 'latte') coffee = new Latte();
    else if (type === 'espresso') coffee = new Espresso();
    else if (type === 'cappuccino') coffee = new Cappuccino();

    coffee.prepare();
    coffee.make();
    coffee.boxing();

    return coffee;
}

takeOutCoffee()는 coffee type을 받아서 해당하는 커피를 가공한 후, 완성된 coffee를 리턴해주는 함수이다. 자 이제 이 함수의 문제점은 무엇인지 생각해 보자.

좋은 코드는 변화에 유연하다. 유연하다는 것은 최소한의 수정으로 요구사항을 반영할 수 있어야 한다는 것을 의미한다. 만약 새로운 종류의 커피, 예를 들면 Mocha가 추가되었다고 가정해 보자. 그렇다면 아래 코드는 새로운 커피 종류에 맞춰 분기를 추가해야 할 것이다.

function takeOutCoffee(type) {
    let coffee;

    if (type === 'latte') coffee = new Latte();
    else if (type === 'espresso') coffee = new Espresso();
    else if (type === 'cappuccino') coffee = new Cappuccino();
    else if (type === 'mocha') coffee = new Mocha(); // 새로운 커피 추가

    coffee.prepare();
    coffee.make();
    coffee.boxing();

    return coffee;
}

하나만 추가하는 것은 어렵지 않다. 중요한 것은 커피의 종류가 얼마나 많이 추가될지 모른다는 것이다. 그때마다 if문을 추가해준다면 takeOutCoffee() 함수는 매우 비대해질 것이다. (물론 여기서는 커피 인스턴스를 생성하고, 가공하는 기능밖에 없지만 실제 코드들은 더욱 복잡할 가능성이 크다.) 그럴수록 의존성은 높아질 것이다

그럼 어떻게 좋은 코드로 바꿀 수 있을까? 코드를 유연하게 만드는 방법은 어렵지 않다. 공통된 부분끼리 묶는다. 그게 첫걸음이라 생각한다. 여기서 공통으로 묶을 수 있는 부분은 어디인가? 바로 type에 따라 커피 인스턴스를 생성해 주는 부분이다.

type에 따라 해당하는 인스턴스를 생성해주는 어떤 요소를 만들어주면 어떨까? 사용하는 사람은 내부 구현을 생각할 필요 없이 type을 넘겨주기만 하면 해당하는 인스턴스를 리턴해주는 것이다. 이렇게 하면 새로운 타입이 추가되었을 때, 해당 부분만 수정하면 되기 때문에 찾기도 쉽고, takeOutCoffee 함수를 수정할 필요도 없지 않을까?

위와 같은 생각을 거쳐 특정 인스턴스를 만들어주는 클래스(또는 함수)를 팩토리(Factory)라 부른다. 말그대로 해당 타입(coffee type)을 주문하면 완성품(coffee instance)을 만들어주는 공장이다.

function takeOutCoffee(type) {
    let coffee = coffeeFactory(type);

    coffee.prepare();
    coffee.make();
    coffee.boxing();

    return coffee;
}

// 커피 타입을 넘겨주면, 인스턴스를 만들어주는 공장!
function coffeeFactory(type) {
    let coffee;
    
    if(type === 'latte') coffee = new Latte();
    else if(type === 'espresso') coffee = new Espresso();
    else if(type === 'cappuccino') coffee = new Cappuccino();
    else if(type === 'mocha') coffee = new Mocha();
    
    return coffee;
}

이렇게 구현하면 새로운 커피가 아무리 추가되어도 takeOutCoffee 함수를 고칠 필요는 없어졌다. '단순히 생성하는 부분을 함수로 만들어 분리한 게 아닌가'라는 생각을 할 수도 있다. 예제 코드는 정말 최소한만 사용했지만, 프로덕션에서 작성되는 코드는 함수 하나에 몇백 줄이 들어갈 수도 있으며, 메인으로 돌아가는 함수도 존재할 것이다. 이런 환경에서 메인 함수를 수정하는 것은 큰 부담이 될 수 있는데, 이렇게 수정되는 부분을 분리(의존성을 분리한다고 한다)하면, 버그가 발생했을 때 어디를 고쳐야 할지 쉽게 찾을 수 있고, 요구사항의 변화에도 쉽게 대응할 수 있다.

이제 팩토리라는 단어에 대해 이해했는데, 그렇다면 추상 팩토리는 무엇을 의미하는 건지 탐구해보자. 우리가 만든 커피 팩토리의 문제점은 무엇일까? 바로 새로운 종류의 커피가 추가되었을 때 끊임없이 if문을 추가해야 한다는 것이다. 이를 해결하려면 어떻게 해야 할까? 이때 추상화란 개념을 사용한다.

앞서 추상화의 반대는 구체화라 했다. 구체화는 다시 말하면 구현된 코드를 의미한다. 커피 팩토리에서 구현된 코드 무엇인가? 바로 타입에 따라 coffee 변수에 인스턴스를 할당하는 부분이다. 인스턴스를 타입에 따라 생성해야 하기 때문에 분기문이 들어가고 의존성이 생기게 된다. 그렇다면 인스턴스를 바깥에서 만든다면? 만든다는 행위만 정의 한다면 분기문을 제거할 수 있지 않을까? 이 행위를 정의하는 생각이 바로 추상화이다.

 

추상화 적용

class CoffeeFactory {
    static createCoffee(factory) {
        return factory.createCoffee(); // 인스턴스를 만드는 행위를 추상화.
    }
}

class LatteFactory {
    static createCoffee () {
        return new Latte();
    }
}

class EspressoFactory {
    static createCoffee () {
        return new Espresso();
    }
}

class Latte {
  constructor() {
    return this.name = "Latte";
  }
}

class Espresso {
  contructor() {
    return this.name = "Espresso";
  }
}

CoffeeFactory.createCoffee(LatteFactory);
CoffeeFactory.createCoffee(EspressoFactory);

자바스크립트에는 abstract keyword가 없다. 또 오버 라이딩이 불가능하고, 상속이란 개념이 존재하지만 약간 미묘하게 다르다. 따라서 문법을 이용한 추상화에 제약이 있으므로, ES6의 클래스 문법을 사용해서 비슷하게 구현해 보도록 하자.

CoffeeFactory에서 인스턴스를 만드는 행위만 정의(추상화) 해놓고, 행위에 대한 구현은 세부적인 팩토리들을 만들어서 createCoffee()라는 공통적인 메서드를 이용하여 생성하도록 정의하였다. CoffeeFactory라는 상위 클래스가 중요한 뼈대(여기서는 Coffee를 만들기 위한 기능)를 결정한다. 그리고 하위 클래스(LatteFactory, EspressoFactory)는 해당 메서드를 구체화 하는 역할을 하고 있다. 정리하면 추상 팩토리는 인스턴스의 생성을 서브클래스에게 위임함으로써 의존성을 낮춘다. 

추가로 이렇게 외부에서 의존성을 넘겨주는 방법을 의존성 주입(Dependency Injection)이라 부른다. 왜냐하면 CoffeeFactory에서 LatteFactory 등의 인스턴스를 생성하지 않고, LatteFactory에서 생성한 인스턴스를 CoffeeFactory에 주입하고 있기 때문이다.

실질적으로 라떼를 만드는 과정을 수정해야 할 경우 CoffeeFactory는 전혀 신경 쓸 필요 없고, Latte 클래스에서 구현부를 수정하면 된다. 왜냐하면 실질적인 구현체는 LatteFactory에서 생성한 인스턴스이고, 해당 인스턴스는 CoffeeFactory에 주입되고 있기 때문이다. 많은 디자인 패턴이나 프레임워크에서 자주 사용하는 기법이므로 기억해 두는게 좋다.

여기서 정적 메서드를 정의한 것을 볼 수 있는데, 정적 메서드를 사용하면 두 가지 장점이 있다.

  1. 정적 메서드는 객체를 생성하지 않고, 클래스의 메서드를 사용할 수 있기 때문에 메모리를 절약할 수 있다.
  2. 개별적인 인스턴스에 묶이지 않으면서 클래스 내부의 함수를 정의할 수 있다는 장점이 있다.

또 커피 팩토리를 추상화 함으로써 인자가 바뀌었기 때문에, takeOutCoffee 함수의 인자도 바꿔주어야 한다.

function takeOutCoffee(factory) {
    let coffee = coffeeFactory.createCoffee(factory);

    coffee.prepare();
    coffee.make();
    coffee.boxing();

    return coffee;
}

 

장점

  • 상위 클래스와 하위 클래스가 분리되기 때문에 느슨한 결합을 가지며, 상위 클래스에서는 인스턴스의 구현에 대해 신경쓰지 않기 때문에 더 많은 유연성을 가지게 된다.
  • 또한, 객체 생성 로직이 분리되어 있기 때문에 코드를 리팩토링 하더라도 한 곳만 고치게 된다. 따라서 유지보수성이 증가한다.
    • 추상 팩토리의 그룹은 동일한 처리 로직을 갖고 있고, 다른 그룹으로 변경돼도 하위 클래스를 통해 선택적 객체를 다르게 생성할 수 있다.

단점

  • 새로운 종류의 군을 추가하는 것이 쉽지 않다.
    • 기존 군에서 새로운 군을 추가하여 확장할 때 모든 서브 클래스들이 동시에 변경돼야 한다.
  • 추상 팩토리는 팩토리 메서드와 비슷하지만, 관리할 그룹이 많다.
    • 계층의 크기가 커질수록 복잡한 문제가 발생.

 

댓글