헷갈리는 팩토리들 구별하기

패턴이나 모범 사례에 대해 학습하다 보면 팩토리라는 용어를 자주 접하게 됩니다.

팩토리 메서드 패턴, 정적 팩토리 메서드, 추상 팩토리 패턴, 심플 팩토리 등 너무 많은 용어들이 있고,

같은 기법에 대해 다른 명칭을 붙이기도 하고, 다른 기법에 대해 같은 명칭을 붙이기도 합니다.

제가 그랬듯이, 초보 개발자는 이런 혼재된 명칭과 정의들 때문에 각 기법에 대한 이해에 벽을 느끼게 됩니다.

이 글의 목표는 "팩토리"라고 불리는 것들을 구별하는 것을 돕는 것입니다.

제가 각 개념의 혼동을 해결하는 데 도움을 준 리팩토링 그루의 Factory Comparison이라는 아티클에서 사용한 명칭을 기반으로 정리해보도록 하겠습니다.

특별히 명시하지 않은 경우 각 방식의 정의는 위 링크를 따랐습니다.

"팩토리"라 불리는 각각의 방식들

생성 메서드(creation method)

정의

생성 메서드는 생성자 호출을 감싸주는 메서드입니다.

우선 코드를 보시겠습니다.

public class Number {

    private int value;

    public Number(int givenValue) {
        this.value = givenValue;
    }

    public Number next() {
        return new Number(this.value+1);
    }
}

Number 클래스는 int값을 가지고 있는 클래스입니다. 이 클래스의 public Number next()가 생성 메서드입니다.

Number(int givenValue) 생성자를 직접 사용하는 대신에, next()를 사용해서 얻는 이점은 무엇일까요?

생성자와 달리 생성 메서드는 이름을 가질 수 있습니다. 이 점에서 많은 차이가 나타납니다.

  • 직관적입니다. number.next() 호출은 number의 다음 수를 반환받는다고 이해하기 쉽습니다. 생성자는 이 객체가 생성된다는 것만 보여주지만, 이름이 들어가면 포함된 로직에 대한 설명이 가능해집니다.

  • 다른 이름으로 정의할 수 있다는 것은 동일한 매개변수를 갖는 여러가지 클래스 인스턴스 생성이 가능해진다는 겁니다. 생성자는 동일 시그니처 당 하나만 있어야 하기에 제약이 있습니다. 다음과 같이 기존 메서드와 동일 매개변수를 받는 메서드를 동시에 선언할 수 있습니다.

        public Number previous() {
            return new Number(this.value-1);
        }

정리

  • 생성 메서드는 생성자 호출을 감싼 메서드다.
  • 생성자와 달리 생성 메서드는 이름을 가질 수 있다. 때문에 생성자는 할 수 없는 동일 매개변수를 갖는 여러 메서드 정의가 가능하다.

정적 생성 메서드(static creation method)

우선, 생성 메서드를 사용하는 다음 코드를 봅시다.

public class Human {

    public Human bornWithCry() {
        cry();
        return new Human();
    }
    private void cry() {
        System.out.println("응애!");
    }
}

Human 클래스의 생성 메서드 bornWithCry()는 사람이 울며 태어나게 해줍니다. 이 상황에서 우는 아이를 태어나게 하고 싶다면 어떻게 해야 할까요?

Human클래스의 메서드를 사용하기 위해서 먼저 Human의 인스턴스 human이 선언되어야 합니다. 그 다음으로 human.bornWithCry()를 호출하여 우는 아이가 태어나게 할 수 있겠죠. 하지만 이 방법은 여러 가지로 문제 있습니다.

  • Human클래스의 인스턴스를 만드는 메서드를 호출하려고 생성자로 Human의 인스턴스 human을 만드는 것은 불필요한 반복입니다.

  • 객체 생성의 역할을 bornWithCry() 생성 메서드와 Human()생성자가 나누어 담당하게 되었는데, 두 가지 생성 방식 명칭에 일관성이 없습니다. (생성자는 이름을 못 바꾸기 때문입니다.)

    만일 일관성을 위해 울지 않으며 태어나는 경우의 생성 메서드 bornWithCry()를 만든다 해도 Human()생성자는 여전히 Human클래스의 첫 인스턴스를 생성하는데 사용됩니다.

이런 이유로 Human 클래스의 인스턴스를 만들지 않더라도, bornWithCry()메서드가 호출될 수 있게 만드는 것이 필요합니다. 이것에 필요한 것이 정적 생성 메서드입니다.

정의

static 키워드가 붙은 생성 메서드입니다.

정적 생성 메서드는 생성 메서드에 static 키워드를 붙인 것입니다. 즉 생성 메서드의 일종입니다. static 키워드가 붙은 메서드이기에, 해당 클래스의 인스턴스를 생성하지 않고도 사용할 수 있습니다. 즉 위의 Human클래스의 문제를 해결할 수 있습니다.

(이펙티브 자바에서는 정적 팩터리 메서드라고 부릅니다.)

정적 생성 메서드를 사용한 버전의 Human 클래스 코드를 살펴봅시다.

public class Human {

    private Human() {
    }

    public static Human bornWithCry() {
        cry();
        return new Human();
    }

    public static Human bornWithSilence() {
        return new Human();
    }

    private static void cry() {
        System.out.println("응애!");
    }
}

이제 우리는 Human클래스의 인스턴스를 얻기 위해서 처음 한 번은 꼭 Human() 생성자를 부를 필요가 없어졌습니다. 덕분에, 기본 생성자 public Human()private Human()으로 바꿔서 Human 클래스의 생성을 위해 사용하는 방식을 깔끔하게 정리할 수 있게 되었습니다.

  • bornWithCry(): 울면서 사람이 태어나게 함
  • bornWithSilence(): 울지 않고 사람이 태어나게 함
  • Human() 생성자: 이제는 강제로 사용할 수 없게 만들었고, 덕분에 코드가 더 명확해졌습니다.

생성자를 private하게 만들고, 메서드를 통해서만 클래스 인스턴스에 접근할 수 있게 만드는 것은 상당히 유용합니다. 예를 들어 다음의 경우

public class Human {

    private boolean andThenThereWereNone = false;

    public Human bornWithCry() {
        if (andThenThereWereNone) {
            System.out.println("그리고 아무도 없었다.");
            return null;
        }
        cry();
        return new Human();
    }

    public Human bornWithSilence() {
        if (andThenThereWereNone) {
            System.out.println("그리고 아무도 없었다.");
            return null;
        }
        andThenThereWereNone = true;
        return new Human();
    }

    private static void cry() {
        System.out.println("응애!");
    }

}

false로 초기화되는 boolean andThenThereWereNone 필드가 추가되었습니다. 침묵 속에 태어난 아이가 생성될 경우, andThenThereWereNone 필드는 true가 됩니다. 이 이후로는 bornWithCry()bornWithSilence()변수 모두 andThenThereWereNone 조건에 걸려 새 인스턴스를 생성하지 못하고 null을 리턴합니다. 즉, 우는 아이가 한 명이라도 태어나면 어떤 방식으로도 새 인스턴스를 호출하지 않게 바꿀 수 있다는 겁니다. 정적 생성 메서드를 사용하면 생성자를 private하게 숨길 수 있기 때문에 객체의 생성을 통제할 수 있다는 겁니다.

객체 생성을 통제

정적 생성 메서드이 가진 객체 생성을 통제하는 기능이 어떤 강점을 갖는지 알아보기 위해서, 이번에는 유용하게 쓰이는 방법을 소개해보겠습니다.

아브라함 계통의 종교(유대교, 기독교, 이슬람교) 같은 일신교의 신을 생각해 봅시다. 이 신은 유일한 God이어야지, 여러 명 있는 gods가 되어서는 안 됩니다. 이 조건을 만족시키기 위한 클래스 God을 만들어 봅시다. (신이 인스턴스화 가능하냐는 형이상학적인 질문은 일단 접어둡시다.)

public class God {

    private static final God GOD = new God();

    private God() {
        System.out.println("아브라함이 있기 전에 내가 있었다.");
    }

    public static God pray() {
        return GOD;
    }
}

private God() 생성자이기 때문에 외부에서 God 인스턴스를 생성할 수는 없습니다. God의 인스턴스를 얻을 수 있는 유일한 방법은 정적 변수인 pray()를 통해서입니다. 그런데 pray()메서드는 God 인스턴스를 새로 생성하는 것이 아니라, God 클래스의 클래스 변수 GOD을 가져다 씁니다. 이 필드는 final 이기에 변경이 불가능합니다. 처음 생성된 God인 인스턴스 GOD은 애플리케이션이 종료될 때 까지 유일합니다.

이러한 생성 패턴을 싱글톤(Singleton)이라고 합니다. 클래스의 인스턴스를 단 하나만 생성하고 그 생성된 인스턴스를 계속 가져와 사용하는 것입니다. 싱글톤 패턴에 대해서는 여러 특징이 있지만 여기서는 하나의 인스턴스만을 생성하기에 메모리를 절약할 수 있다는 점만 짚고 넘어갑시다.

리턴 타입의 유연성

정적 생성 메서드의 또 다른 강점은 반환 타입의 하위 타입을 반환할 수도 있다는 점입니다. 이번에는 고양이를 만든다고 생각해 봅시다. 고양이에도 여러 종류가 있습니다. 한국 고양이로 코리안 숏 헤어가 있고, 러시아 고양이로 러시안 블루가 있습니다. Cat 클래스 한 군데에서 두 종류의 고양이 모두를 만든다고 생각해봅시다.

public class Cat {

    public void sleep() {
        System.out.println("Zzz");
    }

    public static Cat createKoreanShortHair() {
        return new KoreanShortHair();
    }

    public static Cat createRussianBlue() {
        return new RussianBlue();
    }

}
public class KoreanShortHair extends Cat {

    @Override
    public void sleep() {
        System.out.println("코숏 한 마리가 잡니다...zzz");;
    }
}
public class RussianBlue extends Cat {

    @Override
    public void sleep() {
        System.out.println("러시안 블루 한 마리가 잡니다...zzz");;
    }
}

createKoreanShortHair()메서드는 Cat을 리턴타입으로 갖지만 Cat의 서브클래스인 KoeanShortHair를 반환할 수 있습니다. 마찬가지로 Cat을 리턴하는 createRussianBlue()메서드는 RussianBlue를 반환할 수 있습니다. 그렇다면 굳이 createKoreanShortHair() 메서드와 createRussianBlue() 메서드를 별도의 메서드로 빼낼 이유가 없습니다. 다음과 같이 바꿀 수 있습니다:

public class Cat {

    public void sleep() {
        System.out.println("Zzz");
    }

    public static Cat createCat(String type) {
        if (type.equalsIgnoreCase("Korean")) {
            return new KoreanShortHair();
        }
        if (type.equalsIgnoreCase("russian")) {
            return new RussianBlue();
        }
        throw new IllegalArgumentException("유효하지 않은 고양이 타입입니다.");
    }
}

이제 createCat(String type) 메서드가 기존의 createKoreanShortHair()createRussianBlue() 두 메서드가 수행하던 고양이 생성 기능을 한 군데에서 수행할 수 있게 되었습니다. 매개변수인 type에 어떤 인자가 들어오느냐에 따라 서로 다른 Cat의 자손 클래스를 생성, 리턴할 수 있습니다. 어느 새 우리는 정적 생성 메서드에서 출발하여 단순한 팩토리(Simple Factory)라 부르는 방식에 도착했습니다. 단순한 팩토리에 대해 다루기 전에, 한 번 정리해봅시다.

정리

  • 정적 생성 메서드는 생성 메서드에 static 키워드가 붙은 버전이다.
  • 정적 생성 메서드는 객체 생성을 통제할 수 있다.
  • 리턴 타입의 하위 타입을 반환할 수 있다.

단순한 팩토리(Simple Factory)

정의

우선 단순한 팩토리(Simple Factory)라는 용어는 헤드퍼스트 디자인 패턴에서 사용했다는 것을 언급해 둡니다.

단순한 팩토리 패턴은 메서드 패러미터에 따라 어떤 상품을 인스턴스화하고 리턴하는지 결정하는 조건문을 가진 단일 생성 메서드를 가진 클래스 입니다.

The Simple factory pattern describes a class that has one creation method with a large conditional that based on method parameters chooses which product class to instantiate and then return.

말이 너무 기니까, 끊어서 다시 표현해보겠습니다: 단순한 팩토리는 단일 생성 메서드를 가진 클래스입니다. 이 클래스에는 정적 생성 메서드가 있습니다. 이 정적 생성 메서드는 어떤 종류의 하위 인스턴스를 반환해야 하는지를 결정하는 매개변수를 갖습니다. 인자로 받은 값을 조건문(ifswitch 같은)을 통해 검사해서 거기에 맞는 인스턴스를 반환합니다.

말은 복잡합니다만, 코드로 보면 명확합니다. 다시 아까 전에 보여드렸던 코드를 가져와 보겠습니다.

public class Cat {

    public void sleep() {
        System.out.println("Zzz");
    }

    public static Cat createCat(String type) {
        if (type.equalsIgnoreCase("Korean")) {
            return new KoreanShortHair();
        }
        if (type.equalsIgnoreCase("russian")) {
            return new RussianBlue();
        }
        throw new IllegalArgumentException("유효하지 않은 고양이 타입입니다.");
    }
}

Cat 클래스가 여기서 단순한 팩토리이고, public static Cat createCat(String type)이 단순한 팩토리가 갖는 정적 생성 메서드입니다. 정적 생성 메서드의 매개변수 type에 어떤 값이 들어오느냐에 따라 다른 종류의 인스턴스를 반환합니다. 가령 Korean이 인자로 들어온다면, 조건문에 의해 새 KoreanShortHair 인스턴스를 생성, 반환합니다.

단순한 팩토리 방식의 진가는 생성 메커니즘이 한 군데에 몰려 있다는 점입니다. 아래를 봅시다.

public class CatFactory {

    public static Cat createCat(String type) {
        doSomething();
        if (type.equalsIgnoreCase("Korean")) {
            return new KoreanShortHair();
        }
        if (type.equalsIgnoreCase("russian")) {
            return new RussianBlue();
        }
        throw new IllegalArgumentException("유효하지 않은 고양이 타입입니다.");
    }

    private static  void doSomething() {
        // 잘 모르지만 뭔가 뭔가 있음
    }
}

앞으로 doSomething()메서드에 어떤 로직이 들어갈 지는 모릅니다. 하지만 어떤 식으로든 로직을 바꾼다고 해도 CatFactory 외부에서는 createCat() 메서드만으로 고양이를 생성할 것입니다. 이는 아무리 CatFactory의 구현 로직을 바꾼다 하더라도, catFactory()의 시그니처를 수정하거나 하지 않는 이상 외부의 코드에 손을 대지 않아도 된다는 것입니다.

public class KoreanShortHair extends Cat {
    public KoreanShortHair() {
        // 고양이 탄생 로직
    }

    public static Cat createKoreanShortHair() {
        // 고양이 탄생 로직
        return new KoreanShortHair();
    }
}

예를 들어, KoreanShortHair 클래스의 인스턴스를 생성하기 위해서는 KoreanShortHair()생성자나 KoreanShortHair 클래스의 정적 생성 메서드 createKoreanShortHair()를 사용해야 합니다. 만일 KoreanShortHair 생성 메커니즘을 공통적으로 바꾸고 싶다면, 그 경우 KoreanShortHair() 생성자와 createKoreanShortHair() 메서드 내부 구현을 모두 손대야 합니다. 만약에 KoreanShortHair의 생성 과정도 CatFactorycreateCat()으로 위임했다면 이럴 필요 없이 createCat()을 손보기만 하면 됩니다. 다음과 같이 createKoreanShortHair()에서 직접적으로 KoreanShortHairnew 하지 않고, CatFactorycreateCat()메서드를 호출하는 식으로 해결할 수도 있습니다.

    public static Cat createKoreanShortHair() {
        // 고양이 탄생 로직
        return CatFactory.createCat("Korean");
    }
    // 혹은
    public static KoreanShortHair createKoreanShortHair() {
        // 고양이 탄생 로직
        return (KoreanShortHair) CatFactory.createCat("Korean");
    }

방법은 다양합니다만, 핵심은 CatFactory라는 단순한 팩토리를 사용함으로써, 모든 종류의 고양이 생성을 createCat()에 집중시킬 수 있고, 한 군데에서 간단하게 관리가 가능하다는 점입니다.

정리

  • 단순한 팩토리는 정적 생성 메서드를 가지고 있는 클래스입니다.
  • 정적 생성 메서드는 인자로 받은 값을 바탕으로 조건문을 돌려서 해당하는 (리턴값의) 하위 클래스의 인스턴스를 생성, 반환합니다.
  • 한 군데에서 생성 메커니즘을 관리한다는 장점이 있습니다.

팩토리 메서드 패턴(Factory method Pattern)

다시 단순한 팩토리 방식으로 구현된 CatFactory클래스로 돌아가 봅시다.

만일 생성하려는 고양이의 종류가 추가된다면 어떻게 하면 될까요? Cat을 확장 또는 구현하는 새로운 고양이 클래스를 만들고, createCat()메서드에 해당 고양이에 해당하는 조건문을 덧붙여야 할 것입니다.

그런데 Cat클래스가 종류 별로 가져야 하는 필드들이 여럿 있다면 어떨까요? 예를 들어 털 색깔, 무늬, 덩치, 눈 색깔을 모두 지정해 줘야 하는 겁니다. 한 고양이 종류 당 세팅해줘야 하는 필드가 4가지인 겁니다. 이번에는 그 방식대로 변경된 Cat을 생산하는 단순한 팩토리를 봅시다.

우선, 필드가 추가된 Cat입니다.

public abstract class Cat {

    public String eyeColor;
    public String furColor;
    public String furPattern;
    public String size;

    public void scratch() {
        // 각 고양이들이 하는 기본적인 긁기 행동, 재정의할 수 있다.
        System.out.println("그르릉 그르릉");
    }

}

팩토리의 경우는:

public class CatFactory {

    public static Cat createCat(String type) {
        Cat cat = null;
        cry();
        if (type.equalsIgnoreCase("Korean")) {
            cat = new KoreanShortHair();
            cat.eyeColor = "yellow";
            cat.furColor = "white and black";
            cat.furPattern = "cow";
            cat.size = "large";
            return cat;
        }
        if (type.equalsIgnoreCase("russian")) {
            cat = new RussianBlue();
            cat.eyeColor = "blue";
            cat.furColor = "grey";
            cat.furPattern = "mono";
            cat.size = "small";
            return cat;
        }
        throw new IllegalArgumentException("유효하지 않은 고양이 타입입니다.");
    }

    private static void cry() {
        //모든 고양이는 태어날 때 "야~~~옹!" 하고 웁니다.
        System.out.println("야~~~옹!");
    }
}

한 종류 당 세팅해줘야 하는 속성이 늘어나니, 코드가 벌써부터 복잡해졌습니다. 각 고양이 종류 마다 설정해야 하는 것이 너무 많습니다.

고양이의 종류가 몇 가지인지 아시나요? 어떤 글에서는 40가지 이상으로 잡히는 경우까지 소개합니다. 만약에 40가지 고양이의 생성 메커니즘을 createCat()메서드 안에 쑤셔넣는다면 어떨까요? 일단 필드 수정만으로 160줄의 코드가 생기겠군요. 개별 타입별로 필드를 지정해줘야 하니 가독성도 떨어지고, 문제가 생겼을 때 수정하기도 힘들 겁니다.

팩토리 메서드 패턴은 이런 문제를 해결하는데 유용합니다. 이 패턴의 명칭은 4인방(GoF, 에릭 감마, 리처드 헬름, 랄프 존슨 존 블리시디스)의 "Design Patterns" 책에서 등장했습니다. 우선 정의부터 봅시다.

정의

객체를 생성하는 인터페이스를 정의합니다. 단 서브클래스가 어떤 클래스를 인스턴스화할 것인지 정합니다. 팩토리 메서드는 클래스가 인스턴스 생성을 서브클래스에게 미루게끔 합니다.

Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.

가상 생성자(Virtual Construtor)라고도 합니다.

  • 인스턴스 생성을 서브클래스로 미룬다는 것은 무엇일까요?

만일 각 고양이 종류 별로 생성을 담당하는 클래스가 있다면 어떨까요? 일단 코드로 봅시다.

public interface Creator {

    public default Cat birthOfCat(String type) {
        cry();
        return this.createCat(type);
    }

    Cat createCat(String type);

    private void cry() {
        // 모든 종류의 고양이들이 생성될 때 공통적으로 하는 일
        System.out.println("야옹! 야옹!");
    }

}
public class KoreanShortHairCreator implements Creator {
    @Override
    public Cat createCat(String type) {
        Cat cat = new KoreanShortHair();
        cat.eyeColor = "blue";
        cat.furColor = "grey";
        cat.furPattern = "mono";
        cat.size = "small";
        return cat;
    }
}

이제 KoreanShortHair 종류를 생성하는 코드를 전담하는 KoreanShortHairCreatorcreateCat() 메서드가 생겼습니다. 이제 만약 KoreanShortHair 생성에 문제가 생겼을 경우, 다른 Cat들을 생성하는 코드를 살펴 볼 필요 없이 KoreanShortHairCreatorcreateCat()을 찾기만 하면 됩니다.

  • 팩토리 메서드 패턴의 경우 구체적인 고양이들, KoreanShortHairRussianBlue 각각에 대해 생성을 전담하는 KoreanShortHairCreatorRussianBuleCreator의 작동을 규정하는 인터페이스 Creator를 갖습니다. CreatorcreateCat()을 재정의함으로써 새로운 종류의 고양이가 등장하더라도 기존 코드의 변경 없이 확장 가능합니다. 즉 팩토리 패턴은 객체지향의 OCP원칙을 지키는데 도움이 됩니다. (OCP 원칙의 경우 추후 다른 글에서 SOLID 원칙 전체를 다루면서 자세히 설명하겠습니다.)

    • 클라이언트가 구체적인 클래스 KoreanShortHairCreator가 아니라 그 인터페이스인 Creator 클래스를 사용하게 만든다는 점도 중요합니다. 이로 인해 구체적인 구현들을 숨길 수 있고(캡슐화), 상황에 따라 적절한 Creator 구현 클래스를 선택하는 데 도움이 됩니다. (이 부분은 추상 팩토리 패턴을 다룰 때 더 자세히 다루도록 합시다.)
  • default 메서드인 birthOfCat() 메서드가 createCat()을 호출하는 형태로 구성되었다는 점도 중요합니다. 이로 인해

    • 모든 종류의 고양이들이 생성 시 공통으로 수행해야 하는 과정: 위에서는 cry() 메서드를 birthOfCat()모아 둘 수 있습니다. 필요에 따라 이런 공통 로직들을 빼야 하는 TomCat클래스가 등장하는 경우 해당 클래스를 담당하는 TomcatCreator에서 메서드 birthOfCat()을 재정의해서 사용하면 됩니다.
  • 팩토리 패턴이 아니라 팩토리 메서드 패턴이라는 점에도 주목합시다. 팩토리 메서드 패턴의 핵심은 Creator의 하위 클래스에서 생성을 담당하는 메서드 createCat()을 오버라이드하여 하위 클래스에서 인스턴스의 생성을 담당하게 할 수 있다는 점입니다. 이 장점으로 인해 정적 생성 메서드처럼, 객체의 생성을 통제할 수 있기도 합니다. 생성을 담당하는 구체적인 CreatorKoreanShortHairCreator반드시 새로운 객체를 생성할 필요는 없다는 점을 유의합시다.

구조를 한 번 살펴봅시다.

팩토리 메서드 패턴은 다음과 같은 요소로 구성됩니다:

  • Creator 인터페이스
    • ConcreteCreator의 동작 방식을 규정하는 인터페이스 입니다. 상속해야 하는 팩토리메서드(위에서는 createCat()) 뿐만 아니라 모든 경우에 필요한 로직(cry())을 규정해둘 수도 있습니다.
  • Creator를 구현한 ConcreteCreator 클래스들 (위에서는 KoreanShortHairCreator)
    • 정적 팩토리 메서드 createProduct()를 오버라이드 하여 하위 인스턴스를 반환합니다.
  • Product 인터페이스
    • 생산해야 하는 것들을 묶어주는 인터페이스 입니다.(위에서는 Cat) 이 경우에도 하위 클래스들에 공통인 부분을 정의해둘 수 있습니다.
  • Prodct를 구현한 ConcreteProduct 클래스들 (위에서는 KoreanShortHair)
    • 결과적으로 인스턴스가 생성되는 클래스입니다. Product에 미리 규정된 공통 동작을 오버라이드 할 수도 있습니다.

정리해 봅시다.

정리

  • 팩토리 메서드 패턴은, 구체적인 객체 생성을 하위 클래스로 넘겨 주는 패턴입니다.
  • 팩토리 메서드 패턴은 사용자가 인터페이스를 사용하게 하기에 OCP를 지키는 확장성이 좋은 패턴입니다.
  • 생성 메커니즘 등에서 공통적인 부분을 미리 규정해둘 수도 있으며, 공통적이지 않은 부분은 재정의가 가능합니다: 유연합니다.

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

이전 팩토리 메서드 패턴의 예에서 요구사항이 추가된다고 생각해 봅시다.

  • 생산하는 동물의 종류에 Cat 고양이들 말고도 Bear 곰이 추가됩니다.
  • 고객에게 제공되는 두 가지 동물(Cat, Bear)은 고객이 설정한 지역(Korea, Russia)에 따라 제공됩니다.
    • 예를 들어 고객이 Korea로 지역을 설정한 경우 고양이는 KoreanShortHair, 곰의 경우는 반달가슴곰 HalfMoonBear을 제공받아야 합니다. 다른 종류가 섞여서는 안 됩니다.

고객의 설정정보에 따라 같은 종류만 반환해야 한다는 점이 중요합니다. 이처럼 객체 생성을 특정 연관된 것들의 무리(family) 단위로 해야 할 때, 추상 팩토리 패턴이 유용합니다.

정의

추상 팩토리 패턴도 팩토리 메서드 패턴처럼 GoF의 "디자인 패턴" 책에서 소개되었습니다. 정의를 봅시다.

구체 클래스를 지정하지 않고 연관되거나 의존적인 객체들의 군(families)을 만들 수 있는 인터페이스를 제공합니다.

Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

구체적인 클래스를 지정하지 않고 객체 생성 과정을 캡슐화한다는 점은 팩토리 메서드와 비슷합니다.

가장 큰 차이점은, 팩토리 메서드 패턴이 단일 객체 생성에 대한 것이라면,

추상 팩토리 메서드 패턴은 서로 연관된 객체들을 무리 지어 생성하는 것과 관련되었다는 점입니다.

코드로 살펴봅시다.

public interface AnimalFactory {
    Cat createCat();
    Bear createBear();
}

동물 생성을 위한 추상 팩토리입니다.

public class KoreanAnimalFactory implements AnimalFactory {

    @Override
    public Cat createCat() {
        Cat cat = new KoreanShortHair();
        cat.eyeColor = "blue";
        cat.furColor = "grey";
        cat.furPattern = "mono";
        cat.size = "small";
        return cat;
    }

    @Override
    public Bear createBear() {
        Bear bear = new HalfMoonBear();
        bear.furColor = "black";
        return bear;
    }
}

개별 그룹(Korea 설정지역)에 따른 추상 팩토리의 구현입니다. 같은 방식으로 설정 지역이 Russia일 경우 생산할 RussianBlueBrownBear을 담당하는 RussianAnimalFactory도 생성합니다.

클라이언트는 인터페이스를 사용한다

팩토리 메서드 패턴과 추상 팩토리 패턴이 갖는 공통적인 장점은 고객이 구현 클래스에 접촉하지 않고, 그것들의 인터페이스에 접촉하는 구조로 짜여져 있다는 점입니다. 팩토리 메서드 패턴의 경우 구현인 KoreanShortHairCreatorcreateCat()을 호출하지 않고, 그것의 인터페이스인 CatFactorycreateCat()을 호출하는 식으로 사용이 가능하다는 점입니다. 마찬가지로 추상 팩토리 패턴의 경우 KoreanAnimalFactorycreateCat()이 아닌 AnimalFactorycreateCat()으로 호출하는 식이겠죠.

실제로 이렇게 사용하기 위해선 마지막으로 한 가지 단계까 필요합니다. 실제로 고객에게 사용할 KoreanAnimalFactory를 담아줘야 하는 것이죠. 이는 의존성 주입(Dependency Injection)으로 가능합니다.

의존성 주입

public class Client {

    private AnimalFactory factory;

    public Client() {
        this.factory = Config.readConfig();
    }
    
    public void callAnimals() {
        Bear bear = factory.createBear();
        Cat cat = factory.createCat();

        bear.growl();
        cat.scratch();
    }
}

두 가지 점에 집중합시다:

  • Client는 어떤 구체적인 AnimalFactory 를 사용하는 지 모르고 있습니다. 그냥 인터페이스인 AnimalFactory를 호출하고 있을 뿐입니다.
  • Client에게 어떤 종류의 AnimalFactory를 제공하느냐는 Client와 분리된 별도의 설정정보 Config가 수행하고 있습니다.

직접적으로 클라이언트가 자신이 사용할 구현 클래스를 모르고 있기에 이는 유연한 변경 가능성을 갖습니다. 만일 클라이언트가 사용할 팩토리의 종류가 바뀐다고 해도 위의 Client 코드는 변동이 없습니다. 구현 클래스 대신 인터페이스에 의존한다DIP; Dependency Iversion Principle을 지킨 것입니다.

한편, 구체적인 AnimalFactory를 어떤 것으로 사용할 것인지 정하고, 그것을 생성하는 역할을 Client가 갖지 않는다는 점에도 주목합시다. 이는 객체를 생성하는 관심사(Config의 역할)와 사용하는 관심사(Client의 역할)를 구별하고, 그 책임을 적절히 분배해준 것입니다. Client는 인터페이스를 바라보고 있기에, 구현 객체에 대한 의존성이 없습니다. Client가 어떤 구현 객체를 사용하는가에 대한 의존성을 외부의 Config가 주입해주었기 때문에, 이를 의존성 주입이라고 합니다.

연관된 객체들의 무리

다시 추상 팩토리 패턴으로 돌아옵시다. 위의 클라이언트 코드에서 callAnimals() 메서드를 호출했을 때, BearHalfMoonBear가 생성되지만, CatRussianBlue가 생성되는 경우가 존재할 수 있을까요? 답은 불가능입니다. 설정 정보에 따라 주어지는 구현 팩토리는 같은 종류의 동물들(HalfMoonBearKoreanShortHair)만을 생산하게끔 만들어져 있기 때문입니다. 그렇기 때문에 추상 팩토리 메서드는 연관된 객체의 생성을 묶어서 다루는 데 최적화되어 있습니다.

public interface AnimalFactory {
    Cat createCat();	// 같은 지역의 고양이와 곰을 묶어서 다룬다면 적절하다!
    Bear createBear();
}

한가지 더 짚어두어야 할 점은 어떤 것들을 연관된 것으로 보느냐는 요구사항에 따라 다르다는 점입니다. 같은 지역의 생물들끼리 묶어서 다뤄져야 한다면 위의 AnimalFactory 인터페이스는 적합합니다. 하지만 고양이는 고양이끼리, 곰은 곰끼리 다뤄져야 한다면 다음과 같은 구조가 적합합니다.

public interface AnimalFactory {
    KoreanAnimal createKoreanAnimal();
    RussianAnimal createRussianAnimal();
}

이 경우 생산하려는 동물 종류가 설정값이 되어야 겠죠. 예를 들어 Cat을 생산해야 하는 경우라면 AnimalFactory를 구현한 CatFactory를 사용하게 될 것이고, 거기서는 각 메서드에 따라 각각 KoreanShortHair, RussianBlue를 리턴할 겁니다.

따라서 추상 팩토리 패턴을 잘 사용하기 위해서는 이 연관된 객체들을 무리지어 생각하는 것이 적합한지가 중요하겠습니다. 이런 이유로 추상 팩토리 패턴의 예제로는 GUI가 많이 나옵니다. OS에 따라 구현방식이 판이하게 다르기 때문에, CheckBoxScrollBar 의 구현들은 각 운영체제에 따라 묶여서 제공되어야 하기 때문입니다.

확장성

위의 사례에서 한국이나 러시아가 아니라 미국, 일본, 중국을 추가한다면 어떻게 해야할까요? CatBear의 각국에 맞는 구현을 만들고, 추상 팩토리인 AnimalFactory의 구현을 각 나라에 맞게 추가하기만 하면 됩니다. 확장성이 매우 좋습니다. 기존 코드를 변경하지 않아도 확장이 가능합니다. (OCP) 하지만 만약 고양이와 곰 말고 개를 추가한다면 어떻게 될까요? 추상 팩토리인 AnimalFactory는 고양이와 곰을 생성하는 메서드만 규정해 두었고, 모든 구현된 공장들도 그 방식을 따라가기 때문에, 모든 구현된 공장 클래스들을 변경해야 합니다. 어떤 방향으로 확장될지에 대해 고민하는 것은 추상 팩토리를 사용하는 데 있어서 중요합니다. 잘 사용한다면 국적을 추가하는 것처럼 확장성이 매우 좋지만, 잘못 사용한다면 오히려 수정해야 할 코드만 늘리게 되니까요.

구조를 살펴봅시다.

  • ProductA, ProductB: 위에서는 CatBear였습니다. 각 상품 종류에 대한 인터페이스입니다.

  • 그것의 구현인 ConcreteProduct입니다.

  • 추상 팩토리 AbstractFactory입니다. ProductAProductB를 생산하는 각각의 메서드를 가집니다.

  • 추상팩토리의 구현 ConcreteFactory입니다. 각 구체 팩토리의 생산물들은 서로 연관관계가 있습니다. (위에서는 지역 Korea, Russia 였습니다.)

  • 사용자입니다. 인터페이스를 바라본다는 점에서 DIP를 잘 지키고 있습니다.

정리

  • 추상 팩토리 패턴은 연관된 것들을 묶어서 다루는 데 적합한 생성 패턴입니다.
  • 사용자는 인터페이스에 의존하기에 DIP를 지킬 수 있습니다. 클라이언트 코드를 수정하지 않고 생산할 것을 변경 가능합니다.
  • 새로운 생산품 묶음을 추가하는 데 있어 기존 코드의 수정 없이 확장 가능합니다. OCP
  • 인터페이스 자체가 수정되는 형태의 확장에는 취약합니다.

비교

  • 팩토리라는 말은 상당히 모호하게 쓰인다. 무언가를 만든다는 점에서 갖다 붙이기 쉬운 이름이니까.

  • 생성 메서드는 생성자를 감싼 메서드이다.

    • 생성 메서드에 static을 붙이면 정적 생성 메서드이다.
  • 단순한 팩토리는 하나의 생성 메서드를 사용한다. 그 생성 메서드는 조건문을 통해 어떤 구체적인 상품을 생산할 지 결정한다.

    • 조건문을 붙여 다양한 종류를 생산할 수 있는 생성 메서드
  • 팩토리 메서드 패턴은 추상 Creator를 구현한 각각의 구현 팩토리들이 각 상품 생산을 전담하는 방식이다.

    • 구체적인 인스턴스 생성을 인터페이스의 구현 클래스에 맡긴다는 점에서 하나의 메서드가 모든 생성을 총괄하는 단순한 팩토리와 다르다.

    • 정적 생성 메서드나, 정적 생성 메서드를 사용한 단순한 팩토리는 절대 팩토리 메서드 패턴일 수 없다.

      • 팩토리 메서드 패턴의 핵심은 하위 클래스가 생성 메서드를 재정의하여 구체적인 생성 메커니즘, 생성되는 객체의 종류를 결정하는데,
      • 정적 메서드는 재정의될 수 없기 때문이다.
  • 추상 팩토리 패턴은 묶어서 다뤄져야 하는 무리의 생성에 특화되었다.

    • 팩토리 메서드 패턴은 개별 생산물의 생성에 집중한다면, 추상 팩토리는 연관된 상품들을 묶어 다루는 데 특화되었다.
      • 팩토리 메서드 패턴의 인터페이스에는 한 종류의 상품만을 다루는 생성 메서드가 규정되어 있지만,
      • 추상 팩토리 패턴의 경우 둘 이상의 생성 메서드가 규정된다.
    • 단순한 팩토리나 팩토리 메서드 패턴의 클래스를 추상 클래스로 만든다고 해서 추상 팩토리 메서드인 것은 아니다.

참고자료 / 더 읽어볼만한 것들

좋은 웹페이지 즐겨찾기