내가 알고 있던게 🥸가짜 팩토리 패턴🏭이었던 썰

Broccolism·2023년 2월 26일
20
post-thumbnail

dart 로 시작하여 디자인패턴으로 끝나는 글입니다. dart에 대한 지식이 없어도 무방합니다. 팩토리 패턴에 대한 내용만 보고 싶다면 곧바로 '🍕 진짜 팩토리 패턴 만나기'로 건너뛰어도 좋습니다.

서론

플러터 작업을 하느라 dart 언어를 사용하다보면 이런 코드를 종종 만나곤 했다.

  factory Pizza(String name) {
    return _cache.putIfAbsent(name, () => Pizza._internal(name));
  }
🤔 음.. 앞에 `factory` 가 붙었구만. 이게 바로 말로만 듣던 팩토리 패턴..?!
일단 작업할게 밀려있으니까 대충 넘어아고 이거나 마저 만들자.

그리고 진짜로 별 신경 안 쓰고 그냥 넘어갔다. 사건은 헤드퍼스트 디자인패턴 책으로 스터디를 하면서 발생했다. '4장 팩토리 패턴'을 공부하다보니 위 코드가 전혀 팩토리 패턴을 따른게 아니란 사실을 깨닫게 된 것이다.

본론

사실 factory는 싱글톤 패턴을 위한 키워드입니다.

침착하게 먼저 Dart 언어 공식 문서에서 factory 키워드를 찾아보자. 친절하게도 구체적인 예시 코드까지 들면서 설명해주고 있다.

Use the factory keyword when implementing a constructor that doesn’t always create a new instance of its class.
매번 새 인스턴스를 만들지 않는 생성자를 구현해야 할 때 factory 키워드를 사용하세요.
https://dart.dev/guides/language/language-tour#factory-constructors

그러니까 미리 인스턴스 1개만 딱 만들어놓고, 생성자를 호출하면 그 인스턴스를 계속 리턴하고 싶을 때 factory 키워드를 붙이는 것이다. 문서에 나와있는 코드를 활용해서 factory constructor 를 만들면 이런 결과가 나온다.


main() {
  Pizza logger1 = Pizza("cheese");
  Pizza logger2 = Pizza("cheese");
  
  Pizza logger3 = Pizza("bulgogi");
  Pizza logger4 = Pizza("bulgogi");
  
  print("logger1: ${logger1.hashCode}");
  print("logger2: ${logger2.hashCode}\n");
  print("logger3: ${logger3.hashCode}");
  print("logger4: ${logger4.hashCode}");
}
logger1: 833010529
logger2: 833010529

logger3: 852721352
logger4: 852721352

dart에서는 hashCode가 같으면 완전히 같은 객체임을 의미한다. 즉, 위 코드에서 같은 파라미터를 주고 생성한 Pizza 객체는 항상 같은 객체인 것이다.

🍕 진짜 팩토리 패턴 만나기

이제 피자를 만들면서 진짜 팩토리 패턴을 만나보자. 예시 코드와 정의는 헤드퍼스트 디자인패턴 책에서 가져왔다.

심플한 (가짜) 팩토리 함수

사실 이렇게 객체의 인스턴스를 생성하는 부분만 따로 분리하는 방식은 아주 널리 쓰인다. 하지만 OOP 세상의 디자인패턴이 말하는 '팩토리 패턴'은 이것과 완전히 다르다. 그래서 헤드퍼스트 디자인패턴 책에서는 이 방식을 디자인 패턴이 아닌 "Simple Factory 간단한 팩토리", "관용구"라고 말하기도 한다.

간단한 팩토리의 장점

그래도 팩토리이긴 하니까 잠깐 장점을 짚고 넘어가보자. 이렇게 객체를 생성하는 부분만 따로 분리하면 뭐가 좋을까? 그냥 코드를 다른 곳에 옮겨놓을 뿐인데 왜 다들 이렇게 만드는걸까?
가장 큰 이유는 의존성 역전을 실현할 수 있기 때문이다. 아래 예시를 살펴보자. v0의 line5 ~ line9가 v1의 line 7에 대응된다.

v0에서는 PizzaStore 클래스가 여러가지 피자의 종류에 의존하게 된다. CheesePizza의 생성자에 파라미터가 추가되면 orderPizza 함수를 수정해야 하고 새로운 종류의 피자 클래스가 생겨도 코드를 수정해야 한다. 반면, v1에서는 orderPizza 함수에서 이러한 변경 사항을 알 필요도 처리할 필요도 없다. PizzaStore 클래스는 더이상 여러가지 Pizza 클래스에 의존하지 않는다. 대신 SimplePizzaFactory 클래스가 여러가지 Pizza 클래스에 의존하면서 피자 생성을 담당한다. 따라서 orderPizza 함수는 피자 주문이라는 기능에만 온전히 집중할 수 있다.

또다른 장점은 코드를 재사용할 수 있다는 점이다. 피자 생성 코드가 필요한 다른 클래스가 생긴 경우를 생각해보자. v0의 방식을 고수한다면 결국 코드를 복사 붙여넣기하는 수밖에 없다. v1에서는 SimplePizzaFactory 클래스의 클라이언트 즉, 피자를 생성해야하는 클래스가 몇개든 관계 없이 함수 하나만 호출해주면 된다. 변경이 필요할 때도 SimplePizzaFactory 한 군데만 고치면 된다.

간단한 팩토리를 사용하기 위해

v1처럼 간단한 팩토리를 사용하려면 아래 두가지 조건을 만족해야 한다.

  • 팩토리에서 만드는 인스턴스는 추상 클래스를 extend하거나 인터페이스를 implement해야 한다.
  • 팩토리에서 만드는 인스턴스는 concrete 클래스여야 한다.

위 예시에 대입해보자. '팩토리에서 만드는 인스턴스'는 CheesePizzaGreekPizza이고 이들이 구현하는 추상 클래스는 Pizza 클래스다.

Factory Method 팩토리 메소드

이제 진짜 팩토리 패턴을 알아보자. 심플 팩토리보다 좀 더 복잡한 개념을 활용하지만 코드는 간단하니 예시를 먼저 살펴보자. 똑같이 피자를 만드는 예시다.

v1과 비교해보자. PizzaStore 클래스의 orderPizza 함수는 크게 달라진게 없어 보인다. line 7에서, line 2에 있는 추상 메소드 abstract Pizza createPizza()를 호출한다는 점만 빼면 말이다. 추상 메소드에서 눈치챘겠지만 이제 PizzaStore는 추상 클래스가 되었다. 이 추상 클래스를 extend하는 concrete 클래스에서 createPizza() 함수 body를 구현한다. 오른쪽의 NYPizzaStore가 그 예시다. 뉴욕 스타일 피자를 만드는 뉴욕 피자 가게다. 따라서 NYPizzaStorecreatePizza() 함수는 모두 NYCheesePizza, NYGreekPizza와 같은 NY* * *Pizza를 리턴한다.
이게 가능하려면 NYCheesePizza, NYGreekPizza 모두 Pizza 를 상속받은 클래스여야 한다. 기존의 CheesePizza, GreekPizza가 그랬던 것처럼 말이다.

프레임워크화

재밌는 점은 orderPizza()에선 무슨 타입의 피자가 나오든 똑같은 일을 수행할 수 있다는 점이다. 즉, orderPizza() 함수는 v0에서 그랬던 것처럼 온전히 피자 주문을 처리하는 작업만 수행해도 된다. 피자를 생성하는 작업은 팩토리 메소드가 알아서 할 것이다. 피자의 종류가 바뀌어도 이 작업은 변함이 없어야 하며 실제로도 코드 변경이 필요 없다.
이런식으로 어떤 동작을 일반화하여 재사용할 수 있게 하는 것을 '프레임워크화 한다'라고 표현할 수 있다. 이 예시에서는 피자 주문에 대한 프레임워크를 제공한 셈이다. 바로 이 점이 v0과의 가장 큰 차이점이다. v0에서는 단순히 피자 생성에 대한 일회성 해결책만 제시한 수준이라면 팩토리 메소드는 프레임워크를 제공할 수 있게 해준다.

팩토리 메소드 패턴의 정의

정리해보자. PizzaStore라는 추상 클래스가 있다. 여기에는 concrete 메소드와 추상 메소드가 각각 1개씩 있다. 이 concrete 메소드는 추상 메소드를 호출해서 피자 인스턴스를 얻는다. 구체적으로 어떤 타입의 인스턴스를 넘겨줄지는 서브클래스인 NYPizzaStore에서 결정한다. 이 패턴을 적용하기 위해 팩토리가 만드는 concrete class인 NYCheesePizza, NYGreekPizza가 있고, 이들이 구현하는 Pizza 인터페이스(혹은 추상 클래스)가 존재한다.

팩토리 메소드 패턴(Factory Method Pattern)에서는 객체를 생성할 때 필요한 인터페이스를 만듭니다. 어떤 클래스의 인스턴스를 만들 때는 서브클래스에서 결정합니다. 팩토리 메소드 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에게 맡기게 됩니다.
(헤드퍼스트 디자인 패턴 168p.)

피자 예시에 대입해보자.

  • '객체를 생성할 때 필요한 인터페이스': PizzaStore 클래스
  • '어떤 클래스': Pizza 클래스
  • '서브클래스': NYPizzaStore 클래스

Abstract Factory 추상 팩토리

잠깐 머리를 식히고 다음 팩토리 패턴을 살펴보자. 추상 팩토리이다. 이번에는 함수가 아닌 클래스로 팩토리를 제공할 것이다. 굳이 클래스까지 도입한 이유는 단순히 한가지 인스턴스만 돌려주지 않고 서로 연관된 여러가지 인스턴스를 만들고싶기 때문이다. 바로, 피자가 아닌 피자에 들어갈 여러가지 재료를 만드는 팩토리다. 뉴욕 피자 가게에서는 이제 뉴욕 스타일 피자를 만드는 데 필요한 재료를 묶어서 공급받고 싶어졌다. 이를 위해 각 재료를 만드는 함수를 여러개 모아서 클래스로 만들 것이다.

먼저 가장 위쪽 NYPizzaStore를 보자. 여전히 PizzaStore를 extend한다. 가장 큰 변화는 더이상 NYCheesePizza와 같은 지역 특화 피자 클래스가 없다는 점이다. line 7, line 10에서 확인할 수 있듯이 지역별로 같은 피자 클래스를 사용하되, 지역에 맞는 factory를 넘겨주는 방식으로 바뀌었다.
이렇게 하면 각 지역별 피자 클래스를 만들지 않고도 지역별로 서로 다른 피자를 만들어 낼 수 있다. 이를 위해 아래쪽의 PizzaIngredientFactory 인터페이스가 필요하다. line 7, line 10에서 짐작할 수 있듯이 CheesePizza, GreekPizza 클래스 내부에서는 Dough dough = ingredientFactory.createDough(); 와 같은 형태로 각 재료를 사용할 것이기 때문이다.

아래쪽은 지역별 팩토리 클래스를 만드는 코드다. 먼저 PizzaIngredientFactory 인터페이스를 정의하고 각 지역별 팩토리 클래스가 이를 implement한다. NYPizzaIngredientFactory가 그 예시다.

프레임워크화

v3도 v2처럼 프레임워크를 제공할 수 있다. 지역별로 CheesePizza를 만들기 위한 재료가 다르더라도 순서는 통일할 수 있다. 재료는 지역별 재료를 공급해주는 ingredientFactory에서 가져오고, 그 재료로 피자를 만드는 코드는 CheesePizza에 적으면 된다.


public class CheesePizza extends Pizza {
	PizzaIngredientFactory factory;
    
    public CheesePizza(PizzaIngredientFactory factory) {
    	this.factory = factory;
    }
    
    void prepare() {
    	dough = factory.createDough();
        sauce = factory.createSauce();
        cheese = factory.createCheese();
    }
}

추상 팩토리 패턴의 정의

정리해보자. 우리가 원하는건 피자 하나를 만드는 팩토리가 아니다. 피자 재료를 공급하는 팩토리다. 피자 재료는 여러가지가 있기 때문에 함수 하나로 처리할 수 없다. 클래스를 사용해야 한다. NYPizzaIngredientFactory가 그 예시다.
이 클래스는 PizzaIngredientFactory 인터페이스를 implement 했는데, 굳이 인터페이스를 만든 이유는 지역에 관계 없이 똑같이 수행되어야 하는 일이 있기 때문이다. 바로 도우를 펴고 소스를 뿌리는 등 피자를 만드는 일련의 작업 순서다. 인터페이스 덕분에 이 작업을 Pizza의 서브 클래스 모두가 똑같이 처리할 수 있다.

추상 팩토리 패턴(Abstract Factory Pattern)은 concrete 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공합니다. concrete 클래스는 서브클래스에서 만듭니다.
(헤드퍼스트 디자인 패턴 190p.)

피자 예시에 대입해보자.

  • '서로 연관되거나 의존적인 객체로 이루어진 제품군': Dough, Source, Cheese 등 피자 재료
  • '인터페이스': PizzaIngredientFactory
  • 'concrete 클래스': CheesePizza, GreekPizza
  • '서브클래스': NYPizzaIngredientFactory

결론

표 하나로 정리하기

(새 창으로 열면 크게 볼 수 있습니다)

심플 팩토리, 팩토리 메소드, 추상 팩토리 모두 각자가 잘 하는 일이 있다. 항상 뭐든 그렇지만 상황에 맞게 적절히 활용해서 쓰는게 중요할 것 같다.

profile
코드도 적고 그림도 그리고 글도 씁니다. 넓고 얕은 경험을 쌓고 있습니다.

2개의 댓글

comment-user-thumbnail
2023년 3월 2일

찢었다

1개의 답글