-
[ 플러터/다트 ] 팩토리(Factory) 패턴 예제로 정복하기~!개발일지/flutter 2021. 7. 6. 22:53
안녕하세요 개발하는남자 개남입니다.
오랜만에 포스팅을 하게 되었는데 최근에 질문받은 것 중에
플러터를 하다보면 많이 접하게 되지만 무심코 넘어갔던 부분인 factory를 사용하는 이유
왜 쓰는 가에 대해서 질문을 받게 되었습니다.
질문에 답을 하기가 모호하고 ;; 저 조차 개념에 대해 명확하지 않다고 생각이 되어
이번 포스팅 주제를 팩토리 패턴에대해서 한번 다뤄 보면서 개념을 잡아 보겠습니다 :)
플러터에서 factory 라는 타입으로 시작하여 네이밍 생성자로 많이 사용해왔는데요.
플러터에서 factory를 이해 하기 위해서는 개발 디자인 패턴 중에 팩토리 패턴을 알아야 쉽게 이해할 수 있는 부분입니다.
그래서 가장먼저 디자인 패턴인 팩토리 패턴을 알아보겠습니다.
팩토리 패턴이란 무엇일까?
팩토리 메서드 패턴(Factory method pattern)은 객체지향 디자인 패턴이다. Factory method는 부모(상위) 클래스에 알려지지 않은 구체 클래스를 생성하는 패턴이며. 자식(하위) 클래스가 어떤 객체를 생성할지를 결정하도록 하는 패턴이기도 하다.
위키백과中팩토리 패턴이 객체지향 디자인 패턴인 건 알겠고, 부모 클래스에 알려지지 않은 구체 클래스를 생성하는 패턴이고 자식 클래스가 어떤 객체를 생성할지 결정한 다라는 말이 무엇일까?
예제를 봐야 이해가 될것 같습니다.
위키백과에 나오는 피자 예제가 자바나 다트 언어로 나온 것이 없어서 다트 언어로 만들어봤습니다.
이 예제는 피자가게에서 피자를 주문할때 피자 종류에 따라 가격을 출력해 주는 아주 간단한 예제입니다.
enum PizzaType { HamMushroom, Deluxe, Seafood }
우선 파자의 종류를 선택할 수 있는 타입을 열거 타입으로 생성해주겠습니다.
팩토리 패턴을 사용하지 않는 것과 사용하는 것을 비교하면 좀 더 쉽게 개념이 정리될 것 같아서 팩토리 패턴을 사용하지 않고 기능 구현을 해보도록 하겠습니다.
추상 클래스로 getPrice라는 기능을 하는 Pizza 클래스를 만들어주겠습니다.
abstract class Pizza { double getPrice(); }
이렇게 하면 Pizza를 상속받는 구현체(피자 객체)들은 모두 가격(double)을 반환하는 getPrice를 구현해줘야 합니다.
이제 각 피자 종류대로 객체들을 만들어주겠습니다.
class HamAndMushroomPizza implements Pizza { double price = 8.5; @override double getPrice() { return price; } } class DeluxePizza implements Pizza { double price = 10.5; @override double getPrice() { return price; } } class SeafoodPizza implements Pizza { double price = 11.5; @override double getPrice() { return price; } }
구현은 이제 끝이 났고 이제 돌려보겠습니다.
void main() { PizzaType userWantPizza = PizzaType.Deluxe; Pizza pizza; switch (userWantPizza) { case PizzaType.HamMushroom: pizza = HamAndMushroomPizza(); break; case PizzaType.Deluxe: pizza = DeluxePizza(); break; case PizzaType.Seafood: pizza = SeafoodPizza(); break; } print(pizza.getPrice()); // 결과 : 10.5 }
이렇게 간단하게 구현할 수 있습니다. 사실 팩토리 패턴을 사용하지 않고도 얼마든지 구현이 가능합니다.
그럼 왜 팩토리 패턴을 써야 할까요? 그것은 우선 팩토리 패턴을 만들어 보면서 알아보겠습니다.
팩토리 패턴으로 변경해보겠습니다.
가장 먼저 최상위 추상 클래스인 Pizza 클래스 먼저 수정해 주겠습니다.
abstract class Pizza { double getPrice(); static pizzaFactory(PizzaType type) { switch (type) { case PizzaType.HamMushroom: return HamAndMushroomPizza(); case PizzaType.Deluxe: return DeluxePizza(); case PizzaType.Seafood: return SeafoodPizza(); } } }
여기서 보시면 아까 메인 함수에서 swich문으로 사용자가 원하는 피자가 무엇인지에 따라 분기를 해줬던 부분이 추상 클래스로 들어와 있으면서 static으로 선언이 되어있습니다. static으로 선언을 하게 되면 클래스 변수로 선언이 된 것입니다. 인스턴스 변수가 아닌 클래스 변수 즉 생성자 없이도 접근이 가능한 상태입니다. 이것이 팩토리 패턴에 장점 중 하나입니다. 생성자 없이도 어디서든 원하는 피자 객체를 생성할 수 있는 것. 그리고 두 번째 장점은 클라이언트( 피자 객체를 만들어 사용해야하는 부분 이 예제에서는 main 이 되겠습니다. ) main 에서는 더이상 어떤 피자 객체가 있고 그 피자 객체에는 어떤 값들이 들어가야 하는지 전혀 몰라도 알아서 피자객체를 만들어 넘겨주게 됩니다.
void main() { print(Pizza.pizzaFactory(PizzaType.HamMushroom).getPrice()); }
위에서 사용되는 main(클라이언트)에서는 피자 객체가 (HamAndMushroomPizza, DeluxePizza , SeafoodPizza )가 있다는 것조차 몰라도 원하는 PizzaType 만 선택하여 getPrice()를 호출하면 원하는 가격이 출력되는 것을 볼 수 있습니다.
사실 이것만 봤을 때는 꼭 팩토리 패턴을 써야 할까? 싶기도 합니다.
하지만 다음의 장점을 확인하게 되면 팩토리 패턴을 잘 사용하면 정말 좋겠구나 하고 생각이 드실 것입니다.
자 다시 처음 만들었던 팩토리 패턴을 사용하지 않았던 소스로 돌아가 보겠습니다.
그리고 우리들은 개발을 할 때 위에 사용한 예제처럼 main 한 곳에서 사용할 일이 거의 없다는 것은 이미 아실 것입니다.
대게 이곳저곳에서 pizza 객체의 연관된 곳에서 피자 객체를 생성하곤 합니다.
그렇다는 것은 생성자가 여러 파일에서 참조되고 있을 것입니다.
예를 들어 파일 10개에서 DeluxePizza(); 객체를 직접 만들어 사용했고, 3개의 파일에서
switch (type) { case PizzaType.HamMushroom: return HamAndMushroomPizza(); case PizzaType.Deluxe: return DeluxePizza(); case PizzaType.Seafood: return SeafoodPizza(); }
타입에 따라 객체를 분기해서 생성했다고 가정하겠습니다.
구현하고 소스상 문제없이 처리가 완료되었습니다.
그런데 문제는 요구사항이 변경되었을 때 발생합니다.
피자 객체를 만들어 줄 때 orderNumber를 받아 저장시켜야 한다고 요구사항이 변경되었습니다.
요구사항에 맞춰 객체를 업데이트해주겠습니다.
abstract class Pizza { late String orderNumber; double getPrice(); }
공통 데이터이기 때문에 모두가 구현될 수 있도록 추상 클래스에 추가하였습니다.
그럼 나머지 3가지 pizza 클래스에서 orderNumber를 구현해달라고 오류를 뿜어댈 것입니다.
그럼 다음과 같이 소스를 추가해주겠습니다.
class HamAndMushroomPizza implements Pizza { double price = 8.5; @override String orderNumber; HamAndMushroomPizza(this.orderNumber); @override double getPrice() { return price; } } class DeluxePizza implements Pizza { double price = 10.5; @override String orderNumber; DeluxePizza(this.orderNumber); @override double getPrice() { return price; } } class SeafoodPizza implements Pizza { double price = 11.5; @override String orderNumber; SeafoodPizza(this.orderNumber); @override double getPrice() { return price; } }
이것으로 요구사항을 충족할 수 있도록 처리가 되었습니다.
그런데 문제는 아까 피자 객체를 참조하고 있던 13군데에서 모두 orderNumber를 업데이트해야 하는 상황이 발생합니다. 물론 13개를 소스를 수정하는 것은 문제 될 부분은 아니지만, 만일 참조 소스가 30~40개가 된다면;;; 정말 귀찮아질 것입니다. 그리고 그런 귀찮은 일이 매주마다 요구사항이 조금씩 바뀐다고 생각하면 아주 일이 하기 싫어질 수도 있게 됩니다.
자! 여기서 빛을 보게 해주는 것이 팩토리 패턴입니다.
이유는 클라이언트 쪽에서는 전혀 객체에 관여하는 부분이 없기 때문에 20개 40개에서 팩토리 패턴을 사용하여 피자 객체를 만들어 준다고 해도 전혀 수정할 곳이 없습니다. 수정할 부분은 오직 피자 객체들과 팩토리 패턴 부분만 수정해주면 됩니다.
그럼 수정해보도록 하겠습니다.
abstract class Pizza { late String orderNumber; double getPrice(); static pizzaFactory(PizzaType type, String orderNumber) { switch (type) { case PizzaType.HamMushroom: return HamAndMushroomPizza(orderNumber); case PizzaType.Deluxe: return DeluxePizza(orderNumber); case PizzaType.Seafood: return SeafoodPizza(orderNumber); } } } void main() { String orderNumber = "123"; print(Pizza.pizzaFactory(PizzaType.HamMushroom, orderNumber).getPrice()); }
이렇게 추상 클래스 쪽에서 생성자를 담당하기 때문에 소스 수정하는 부분은 추상 클래스 쪽에 몰려 있는 것을 볼 수 있습니다.
그리고 클라이언트 쪽에서는 orderNumber를 넘겨주기만 하면 됩니다.
아니 잠깐. 이것 역시 orderNumber 넘겨줘야 하기 때문에 20~40개 참조하고 있으면 모두 다 수정해줘야 하는 것은 똑같지 않나요?
할 수 있습니다. 물론 맞습니다. 상대적으로 소스 수정 양이 작을 뿐 아니라 보통은 객체를 생성하려고 할 때 값을 하나하나 파라미터로 넘기기보다 Map타입으로 담겨 있기 때문에 사실상 팩토리 함수만 수정해주면 되게 됩니다.
제가 말한 Map 타입으로 담겨 있는다는 말이 무슨 말이냐 하면 플러터 객체 만들어줄 때를 생각해보면 이해가 되실 것입니다.
다음과 같이 되게 됩니다.
void main() { print(Pizza.pizzaFactory(jsonData).getPrice()); } abstract class Pizza { late String orderNumber; double getPrice(); static pizzaFactory(Map<String,dynamic> json) { switch (json['type'] as PizzaType) { case PizzaType.HamMushroom: return HamAndMushroomPizza(json['orderNumber’]); case PizzaType.Deluxe: return DeluxePizza(json['orderNumber’]); case PizzaType.Seafood: return SeafoodPizza(json['orderNumber’]); } } }
이렇게 처리될 수 있습니다.
자 이제 마지막으로 플러터의 factory 타입이랑 위의 예제랑 무슨 차이가 있을까요?
큰 차이는 없습니다. static으로 선언된 부분을 factory로 바꿔 주시기만 하면 됩니다. 이하 기능과 사용방법은 동일합니다.
abstract class Pizza { late String orderNumber; double getPrice(); factory Pizza.fromJson(Map<String,dynamic> json) { switch (json['type'] as PizzaType) { case PizzaType.HamMushroom: return HamAndMushroomPizza(json['orderNumber’]); case PizzaType.Deluxe: return DeluxePizza(json['orderNumber’]); case PizzaType.Seafood: return SeafoodPizza(json['orderNumber’]); } } }
이름도 우리가 자주 사용하는 fromJson으로 변경해주니 이제 익숙해 보이시죠?
어떻게 보면 저 역시 factory를 사용했지만 반쪽만 사용하고 있었던 것 같습니다.
이번에 이렇게 정리하고 나니 좀 더 기능을 잘 설계하면 좀더 편한 개발이 되겠다 싶네요 ^^
조만간 관련 내용을 정리하여 영상에서도 다룰 예정입니다.
개발하는남자 방문하셔서 다양한 예제 소스들 정보들 얻어가세요 ^^구독 좋아요는 필수 아시죠? ㅎㅎ
감사합니다.
'개발일지 > flutter' 카테고리의 다른 글
[ 플러터 2.5 업데이트 ] 릴리즈 노트 간단 정리 flutter 2.5 release version update. (2) 2021.09.18 [플러터 / 라이브러리] API 통신에 편리한 dio의 기능정리. (1) 2021.08.21 [Flutter / 플러터] 제네릭, 제네릭 하는데 그게 뭐야? 개념 살펴보기 (0) 2021.06.10 Flutter 에서의 immutable / mutable한 클래스를 immutable 하게 사용하자. (2) 2021.04.30 플러터 immutable VS mutable 기본이지만 헷갈릴 수 있는 개념 잡고 가실께요~! (5) 2021.04.24