Factory Pattern(팩토리 패턴)

Singleton Pattern

Flutter/Dart 클래스(객체) 이해하기

이번 글에서는 디자인 패턴 중 하나인 Factory Pattern에 대해서 작성하도록 하겠다.

Factory 패턴은 무엇이고 왜 사용해야 하는가 ? 먼저 Factory 패턴은 객체를 생성하기 위해 인터페이스를 생성하고 인터페이스를 사용하는 클래스에서 어떤 객체를 만들지를 결정하게 하는 패턴이라고 한다. 잘 이해가 되지 않을 것이다. 하지만 알고 사용하지는 않았더라도 한 번쯤은 사용해 봤을 패턴이다.

Factory Pattern에 대한 자세한 내용은 다른 블로그의 글을 참고하도록 하자. 여기서는 예제를 통해 Factory Pattern에 대해서 이해해보고, Flutter에서 Factory Pattern을 어떤 방식으로 사용해야 하는지 알아보자.

Dart

먼저 팩토리 패턴을 사용하지 않고 자동차의 가격 정보를 출력하는 예제를 만들어 보자.

추상 클래스로 Car 객체를 생성하여 getPrice() 메소드로 가격을 출력할 수 있도록 하였다.

abstract class Car {
  void getPrice();
}

Car 추상 클래스를 상속하여 3개의 자동차에 대한 정보를 가지고 있는 객체를 각각 생성해 준다. 추상 클래스에서 만들었던 getPrice 메소드를 재정의(override)하여 price를 출력할 수 있도록 구현해 준다.

class GenesisG70 implements Car{
  final int price = 5000;
  
  
  void getPrice(){
    print("Price : $price");
  }
}

class GenesisG80 implements Car{
  final int price = 8000;
  
  
  void getPrice(){
    print("Price : $price");
  }
}

class GenesisG90 implements Car{
  final int price = 14000;
  
  
  void getPrice(){
    print("Price : $price");
  }
}

열거형(enum)을 사용하여 각 자동차의 타입을 만들어준다.

enum CarType {g70, g80, g90;}

위에서 생성한 타입별 가격을 출력해주는 기능을 main 함수안에 작성하고 실행해보자. 정상적으로 출력이 되는 것을 확인할 수 있다.

void main() {
  CarType type = CarType.g80;
  switch(type){
    case CarType.g70:
      GenesisG70().getPrice();
      break;
    case CarType.g80:
      GenesisG80().getPrice();
      break;  
    case CarType.g90:
      GenesisG90().getPrice();
      break;
  }
}

자 이번에도 위의 예제와 같이 추상 클래스로 Car 객체를 다시 만들어 주는데, static 메소드를 사용하여 main 함수에 넣었던 로직을 추상 클래스 안에 만들어 보자.

abstract class Car {
  void getPrice();
  static carFactory(CarType type){
    switch(type){
    case CarType.g70:
      GenesisG70().getPrice();
      break;
    case CarType.g80:
      GenesisG80().getPrice();
      break;  
    case CarType.g90:
      GenesisG90().getPrice();
      break;
  }
  }
}

main 함수에 Car 객체의 static carFactory에 접근하여 아래와 같이 실행해 주자.

위에서와 똑같은 기능을 갖추고 있지만 코드가 매우 간결해진 것을 볼 수 있다.

void main() {
  Car.carFactory(CarType.g90);
}

static 메소드를 사용하지 않고 factory로 생성하여도 같은 결과 값이 나올 것이다.

abstract class Car {
  void getPrice();
  factory Car(CarType type){
    switch(type){
    case CarType.g70:
      return GenesisG70();
    case CarType.g80:
      return GenesisG80();
    case CarType.g90:
      return GenesisG90();
    }
  }
}

main 함수를 다시 수정하여 실행해보자. 역시나 같은 기능을 가지고 있다는 것을 알 수 있다.

이것이 팩토리 패턴을 사용하는 이유이다. 하지만 팩토리 패턴을 사용하지 않고도 같은 기능은 얼마든지 구현할 수 있는 것도 맞는 말이다.

팩토리 패턴을 사용한 예제를 보면 추상 클래스 안에서 자식 클래스가 어떤 객체인지를 알지 못하더라도 객체의 상태만 알고 있으면 기능을 사용할 수 있다는 것을 확인했다.

지금은 간단한 예제이므로 팩토리 패턴의 필요성을 느끼지 못할 수도 있지만 구현해야 하는 기능을 추상 클래스에 계속 추가한다면 자식 클래스도 추상 클래스를 재정의해야 하기에 모든 자식 클래스를 수정해줘야 한다. 하지만 팩토리 패턴을 사용하게 되면 추상 클래스에서는 생성될 자식 클래스를 관여하지 않고 상태만 알고 있으면 되기에 팩토리 패턴만 수정해주면 된다.

void main() {
  Car(CarType.g90).getPrice();
}

Flutter

이번엔 Flutter에서 팩토리 패턴을 구현하는 예제를 살펴보도록 하자. 팩토리 패턴은 어찌보면 method 형태와 비슷하다고 생각할 수 있는데, 헷갈리시면 안된다.

Factory Pattern을 구현하기 가장 쉽고 직관적인 예제가 플랫폼 별 분기 UI를 보여주는 부분이다. 먼저 플랫폼 별로 버튼을 만들어보자.

열거형(enum)으로 각 플랫폼을 넣어주자.

enum PlatformTarget {
  android,
  ios;
}

추상 클래스로 팩토리 패턴에 사용할 PlatformButton 클래스를 만들어주고 build안에 필수 파라미터로 버튼이 눌렸을 때 사용할 함수와 위젯을 받아올 수 있도록 만들어준다.

factory 부분에서 타겟에 맞는 버튼을 리턴하도록 팩토리 패턴을 사용하여 로직을 만들어주자.

abstract class PlatformButton {
  Widget build(VoidCallback onPressed, Widget child);

  factory PlatformButton(PlatformTarget target) {
    switch (target) {
      case PlatformTarget.android:
        return AndroidButton();
      case PlatformTarget.ios:
        return IOSButton();
      default:
        return AndroidButton();
    }
  }
}

안드로이드 버튼으로는 Material UI 버튼을 사용하자. 위에서 생성한 PlatformButton을 상속하여 build 부분을 재정의 해주자.

class AndroidButton implements PlatformButton {
  
  Widget build(VoidCallback onPressed, Widget child) {
    return ElevatedButton(onPressed: onPressed, child: child);
  }
}

IOS 플랫폼에서는 Cupertino UI를 사용하는 버튼 객체를 생성하자.

class IOSButton implements PlatformButton {
  
  Widget build(VoidCallback onPressed, Widget child) {
    return CupertinoButton(child: child, onPressed: onPressed);
  }
}

자 이제 팩토리 패턴으로 생성한 PlatformButton 객체를 사용하여 아래와 같이 작성해 주자. 각 플랫폼 버튼이 생성된 것을 볼 수 있다.

children: [
              PlatformButton(PlatformTarget.android).build(() {
                logger.e("Android Button !!");
              },
                  Container(
                    width: MediaQuery.of(context).size.width / 2 - 40,
                    color: Colors.amber,
                    child: const Center(
                      child: Text(
                        'Android Button',
                      ),
                    ),
                  )),
              PlatformButton(PlatformTarget.ios).build(() {
                logger.e("IOS Button !!");
              },
                  Container(
                    width: MediaQuery.of(context).size.width / 2 - 40,
                    color: Colors.amber,
                    child: const Center(
                      child: Text(
                        'IOS Button',
                      ),
                    ),
                  )),
            ],

이번에는 좀 더 간다한 팩토리 패턴을 만들자. build 부분의 필수 파라미터를 받아오지 말고 만들어보자.

abstract class PlatformIndicator {
  Widget build();

  factory PlatformIndicator(PlatformTarget target) {
    switch (target) {
      case PlatformTarget.android:
        return AndroidIndicator();
      case PlatformTarget.ios:
        return IOSIndicator();
      default:
        return AndroidIndicator();
    }
  }
}

안드로이드 기본 인디케이터를 만들자.

class AndroidIndicator implements PlatformIndicator {
  
  Widget build() {
    return const CircularProgressIndicator(
      color: Colors.green,
    );
  }
}

쿠퍼티노 인디케이터를 사용하여 IOS 플랫폼 인디케이터로 만들어주자.

class IOSIndicator implements PlatformIndicator {
  
  Widget build() {
    return const CupertinoActivityIndicator(
      color: Colors.green,
    );
  }
}

매우 심플하게 각 플랫폼 인디케이터를 생성하였다.

children: [
	PlatformIndicator(PlatformTarget.android).build(),
	const SizedBox(width: 40),
	PlatformIndicator(PlatformTarget.ios).build(),
	],

이번에는 상태가 변경되는 스위치를 만들어보자. 상태 변경시 필요한 onChanged 함수를 boolean을 리턴하도록 만들고, 현재 스위치의 상태를 가지는 isToggle 값을 받아오도로 하자.

abstract class PlatformSwitch {
  Widget build(Function(bool) onChanged, bool isToggle);

  factory PlatformSwitch(PlatformTarget target) {
    switch (target) {
      case PlatformTarget.android:
        return AndroidSwitch();
      case PlatformTarget.ios:
        return IOSSwitch();
      default:
        return AndroidSwitch();
    }
  }
}
bool isAndroid = false;

class AndroidSwitch implements PlatformSwitch {
  
  Widget build(Function(bool) onChanged, bool isToggle) {
    return Switch(
      value: isToggle,
      onChanged: onChanged,
      activeColor: Colors.cyan,
    );
  }
}
bool isIOS = false;	

class IOSSwitch implements PlatformSwitch {
  
  Widget build(Function(bool) onChanged, bool isToggle) {
    return CupertinoSwitch(value: isToggle, onChanged: onChanged);
  }
}
`

이제 스위치를 작동해 보면 잘 온오프가 되는 것을 확인할 수 있다.

 children: [
              PlatformSwitch(PlatformTarget.android).build((value) {
                setState(() {
                  isAndroid = value;
                });
              }, isAndroid),
              const SizedBox(width: 40),
              PlatformSwitch(PlatformTarget.ios).build((value) {
                setState(() {
                  isIOS = value;
                });
              }, isIOS),
            ],

Result

Git

https://github.com/boglbbogl/flutter_velog_sample/tree/main/lib/dart_lang/factory

마무리

간단한 예제를 통해서 Dart 언어와 Flutter 프레임워크에서 Factory Pattern을 어떻게 사용하는지에 대해서 확인해봤다. 팩토리 패턴은 알게 모르게 개발을 하면서 다들 접해봤을 내용이다.

팩토리 패턴과 메소드에 대해서 약간 헷갈릴 수도 있는데, 메소드로 UI 분기를 개발하는 것보다는 추상 클래스를 통해서 팩토리 패턴으로 만들어 주는 것이 성능상 좋다고 한다.

다음 글에서는 팩토리 패턴보다 더 재미있는 디자인 패턴인 싱글톤 패턴에 대해서 다뤄보도록 하겠다.

profile
Flutter Developer

0개의 댓글