추상 팩토리 패턴 Abstract Factory Pattern

2019.04.11 01:27Design Pattern

 디자인 패턴(Design Pattern)은 학부생 때 열심히 공부했었는데, 지금은 딱히 패턴을 생각하며 코드를 짜고 있는 것 같진 않다. 필요한 부분에 적절하게 기능별로 모으고, 추상화를 하다 보면 자연스럽게 패턴이 되기 때문이다. 하지만 다른 사람에게 설명하거나 표준화(?)된 코드를 더 잘 짜기 위해선 다시 한번 복습해보면 좋을 것 같다는 생각도 든다. 따라서 최근 GoF의 디자인 패턴을 JavaScript로 이식한 깃허브 저장소 보고 있다. ES5와 ES6로 잘 구현을 해놨지만 다소 설명이 충분해 보이지 않았다. 이번 기회에 한 땀 한 땀 따라가 보면서 정리해 보도록 하자.

 디자인 패턴 책이나 구현 방법에 대한 글을 찾아보면, 클래스 다이어그램이나 코드 위주로 설명이 되어있다. 물론 다이어그램과 코드도 이해를 돕는데 중요하지만, 왜 사용해야 하는지에 대해서는 다소 무게감 있게 다루는 글은 드물었다. 개인적으로 왜 사용하는지를 굉장히 중요하게 생각하기 때문에, 앞으로 정리할 글에서는 구현보단 왜, 어떻게, 언제 사용해야 하는지를 중점적으로 다루어 보도록 하겠다. 여기서 구현은 JavaScript 언어를 사용하지만, 해당 언어를 몰라도 다른 프로그래밍 언어를 익힌 사람이라면 이해하는데 큰 어려움은 없을 것이라 생각한다.

추상 팩토리 패턴?

 앞으로 진행할 디자인 패턴 시리즈(?)의 첫번째 타자는 바로 추상 팩토리 패턴(Abstract Factory Pattern)이다. 왜 추상 팩토리 패턴이 첫번째인가라는 합리적인 의심을 가질 수 있는데, 단순히 깃허브 저장소에서 위에서부터 아래로 진행하기로 결정했기 때문이다(...) 아무튼 사소한 건 신경 쓰지 말고 추상 팩토리에 대해 알아보도록 하자.

 추상 팩토리. 추상화(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 CappuccinoFactory {
    static createCoffee () {
        return new Cappuccino();
    }
}

class MochaFactory {
    static createCoffee () {
        return new Mocha();
    }
}

// 실행 해보자
CoffeeFactory.createCoffee(LatteFactory);
CoffeeFactory.createCoffee(EspressoFactory);
CoffeeFactory.createCoffee(CappuccinoFactory);
CoffeeFactory.createCoffee(MochaFactory);

 자바스크립트에는 abstract keyword가 없다. 또 오버 라이딩이 불가능하고, 상속이란 개념이 존재하지만 약간 미묘하게 다르다. 따라서 문법을 이용한 추상화에 제약이 있으므로, ES6의 클래스 문법을 사용해서 비슷하게 구현해 보도록 하자. 커피 팩토리에서 인스턴스를 만드는 행위만 정의(추상화) 해놓고, 행위에 대한 구현은 세부적인 팩토리들을 만들어서 createCoffee()라는 공통적인 메서드를 이용하여 생성하도록 정의하였다. 정리하면 추상 팩토리는 인스턴스의 생성을 서브클래스에게 위임함으로써 의존성을 낮춘다. 

 여기까지 잘 따라왔다면 추상화에 대한 개념과 어떻게 해야 의존성을 분리시킬 수 있는지 이해할 수 있을 것이다. 추가로 이렇게 외부에서 의존성을 넘겨주는 방법을 의존성 주입(Dependency Injection)이라 부른다. 많은 디자인 패턴이나 프레임워크에서 자주 사용하는 기법이므로 기억해 두도록 하자. 또 커피 팩토리를 추상화 함으로써 인자가 바뀌었기 때문에, takeOutCoffee 함수의 인자도 바꿔주도록 하자.

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

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

    return coffee;
}

 마지막으로 라떼의 종류가 여러 가지가 된다면 어떻게 하면 좋을까? 카페라떼, 캬라멜라떼등 특정 커피의 종류가 늘어났을 때 말이다. 한번 고민해 보도록 하자.

 

 

  • 프로필사진
    제르멩2019.04.11 12:52

    매번 팩토리 객체를 new 연산자로 생성해야하면 그냥 해당 커피 객체를 생성후 넘겨주는거보다 더 나빠진거 아닌가요?

    • 프로필사진
      La.place2019.04.12 00:36 신고

      coffeeFactory가 takeOutCoffee 함수안에 있어서 헷갈릴수도 있는데요. 인자로 받는건 추상 팩토리 패턴과 관련이 없습니다.

      let coffee = coffeeFactory.createCoffee(LatteFactory);
      coffee.prepare();

      let coffee = coffeeFactory.createCoffee(EspressoFactory);
      coffee.prepare();

      위 코드처럼 생성하는 부분을 추상화, 캡슐화 하여 의존성을 낮추고 응집도를 높이는데 목적이 있습니다. 본문의 예제에서는 추상팩토리를 통해 하나의 서브 클래스만 생성했지만, 실제로는 더 많은 서브 클래스를 생성할 수도 있습니다.

      만약 커피 객체를 생성 후 넘겨주려면 함수 바깥에

      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();

      코드가 다시 들어갈 수밖에 없습니다.

1 2 3 4 5 6 7 ··· 32