SW Engineering - 디자인 패턴(생성 패턴)

윤형·2025년 4월 20일

Sofrware Engineering

목록 보기
7/9
post-thumbnail

서론

드디어 디자인 패턴 부분이다. 디자인 패턴을 잘 알고있으면 어떻게 코드를 설계하는게 좋은지, 또 어떠한 방식으로 하는게 효율적인지 등등 여러 도움되는 내용들을 얻을 수 있다. 패턴의 종류는 엄청 다양해서 모든걸 다 다루지는 못한다.

디자인 패턴

반복적으로 발생하는 문제들에 대해 적합한 설계 솔루션을 정의한 것

  • 패턴 사용 장점: 전문가들의 경험을 바탕으로 한거임
    -> 솔루션에 대한 논쟁 제거(이미 선대에 다함)
    -> 개발 생산성 증가
  • 생성 패턴: 객체의 생성 과정이나 초기화에 관한 패턴들
  • 구조 패턴: 클래스나 객체를 어떻게 구조화 하는지에 관한 패턴들
  • 행위 패턴: 클래스나 객체들이 상호작용하는 방법과 책임을 분산하는 방법에 대한 패턴들

생성 패턴 (Creation Patterns)

객체의 생성과정을 효율적으로 하기 위한 패턴 및 솔루션

1. Abstract Factory (추상 팩토리)

관련 있는 여러 객체들을 일관된 방식으로 생성할 수 있게 해주는 패턴

enum Themes {
  THEME_LIGHT,
  THEME_DARK
};

Themes g_currentTheme = THEME_LIGHT;

class TitleBar;
class LightThemeTitleBar : public TitleBar {};
class DarkThemeTitleBar : public TitleBar {};

class CloseButton;
class LightThemeCloseButton : public CloseButton {};
class DarkThemeCloseButton : public CloseButton {};

void Window::OnCreate() {
  if (g_currentTheme == THEME_DARK) {
    m_titleBar = new DarkThemeTitleBar();
    m_closeButton = new DarkThemeCloseButton();
  } else {
    m_titleBar = new LightThemeTitleBar();
    m_closeButton = new LightThemeCloseButton();
  }
}

만약에 이렇게 생성시, 각 UI의 테마색을 바꾸는 코드가 있다고 해보자.
아무생각 없이 짜게 되면 각 버튼, 패널마다 변수를 확인해 밝은색 또는 검은색으로 구현을 할것이다. 하지만... 당연하게도 새로운 색이 추가가되거나 새로운 UI요소가 생기게 되면 무수히 많은 코드를 수정해야 한다. 이것은 아무도 원치 않을 것이다.

// 위젯 부분
class TitleBar;
class CloseButton;
class WidgetFactory {
  public:
    virtual TitleBar *CreateTitleBar() = 0;
    virtual CloseButton *CreateCloseButton() = 0;
};

extern WidgetFactory *g_widgetFactory;

widgetFactory를 만들고, 외부 노출할 부분만 한다.
*g_widgetFactory를 외부로 선언해 어디서는 호출할 수 있게 해준다.

// widget.cpp
class LightTitleBar : public TitleBar {};
class DarkTitleBar : public TitleBar {};
class LightCloseButton : public CloseButton {};
class DarkCloseButton : public CloseButton {};

class LightWidgetFactory : public WidgetFactory {
  public:
    TitleBar *CreateTitleBar() {
      return new LightTitleBar();
    }
    CloseButton *CreateCloseButton() {
      return new LightCloseButton();
    }
};

class DarkWidgetFactory : public WidgetFactory { ,,, 이 부분에 내용 추가,,,};

// 테마가 선택되는 것에 따라 이 factory 를 바꿔준다.
WidgetFactory *g_widgetFactory = new LightWidgetFactory();
  • 이렇게 작성해두고 추후에 색이 추가된다고 하면 새로운 Class를 만들어 WidgetFactory를 상속받는다.
  • 색 변경은 g_widgetFactory를 변경하는 식으로 관리할 수 있다.
void Window::OnCreate() {
  m_titleBar = g_widgetFactory->CreateTitleBar();
  m_closeButton = g_widgetFactory->CreateCloseButton();
}
  • window 생성하는 부분에서는 생성만 수행한다.

<사용처>

  • 서로 연관된 객체들을 세트로 생성해야 할때
  • 구체적인 클래스에 의존하지 않게 만들고 싶을 때

2. Builder

복잡한 객체를 단계별로 생성할 수 있도록 도와주는 패턴

  • 필드가 많거나, 조립 방식이 복잡한 객체를 만들 때
  • 생성자 파라메터가 너무 많아질 때
class User {
  public:
    User(string name, int age) {}
    User(string name, int age, string id) {}
};
vector<User *> readUserFile(vector<string> lines) {
  vector<User *> users;
  for (l : lines) {
    User *u;
    list<string> fields = splitField(l);
    if (fields.size() == 2) {
      string name = fields[0];
      int age = atoi(fields[1].c_str());
      u = new User(name, age);
    } else if (fields.size() == 3) {
      string name = fields[0];
      int age = atoi(fields[1].c_str());
      string id = fields[2];
      u = new User(name, age, id);
    }
    users.push_back(u);
  }
  return users;
}
  • 만약에 이렇게 생성자가 여러개 있는 경우가 있다. 이러면 필드의 개수를 새서 2개면 name,age로 반환하고 3개면 name, age, id를 가진 객체를 생성한다.

  • 하지만 단점이 눈에 보인다. 일단 케이스가 2개밖에 없는데 복잡해서 눈에 잘 읽히지 않는다. 또한 더 많은 생성자가 계속 등장하게 되면 점점 복잡해질 것이다.

  • 이 상황을 해결하기 위해 Build를 사용한다.
  • User 생성자를 위한 인자가 모두 준비된 후에 생성자를 호출하는 것이 아니라, 인자가 준비되면 그때그때 사용하는 방식이다.
class User {
  public:
    User(string name, int age) {}
    User(string name, int age, string id) {}
};
class UserBuilder {
  private:
    string m_name;
    int m_age;
    string m_id;
  public:
    void setName(string name) {m_name = name;}
    void setAge(int age) {m_age = age;}
    void setId(string id) {m_id = id;}
    User *build() {
      if (m_id.empty()) {
        return new User(m_name, m_age);
      } else {
        return new User(m_name, m_age, m_id);
      }
    }
};
  • id가 비어있으면 2개인수 생성, 있으면 3개짜리 인수 생성
vector<User *> readUserFile(vector<string> lines) {
  vector<User *> users;
  for (l : lines) {
    UserBuilder *builder = new UserBuilder;
    list<string> fields = splitField(l);
    string name = fields[0];
    builder->setName(name);
    int age = atoi(fields[1].c_str());
    builder->setAge(age);
    if (fields.size() >= 3) {
      string id = fields[2];
      builder->setId(id);
    }
    User *u = builder->build();
    users.push_back(u);
  }
  return users;
}
  • 필드가 3이상이면 id를 추가한다.
  • builder의 build만 실행한다.

이해를 위해 게임으로 간략하게 예시를 들어보겠다.
만약에 유저 객체를 생성하려고 하면

Character c = new Character("Arthur", "Warrior", 
"Sword", "Shield", "Berserk", "Wolf");
// 너무 길고, 순서 틀리면 버그나기 쉬움

이렇게 되어있으면 누가봐도 힘들다 하지만!

Character c = new Character.Builder("Arthur")
                .job("Warrior")
                .weapon("Sword")
                .skill("Berserk")
                .pet("Wolf")
                .build();

이런식으로 필요한 부분만 그때끄때 추가하면 빌더인 것이다.

3. Factory Method

객체를 생성할 때 어떤 클래스를 생성할지 서브클래스에서 결정하는 것.
-> 직접 new키워드를 사용하지 않고 "생성을 담당하는 메서드"가 생성한다.

카페에서 “디저트 주세요!” 하면
케이크 가게는 케이크를 주고
아이스크림 가게는 아이스크림을 준다.

// Product
abstract class Dessert {
    abstract void eat();
}

class Cake extends Dessert {
    void eat() { System.out.println("케이크 냠냠"); }
}

class IceCream extends Dessert {
    void eat() { System.out.println("아이스크림 찬찹"); }
}

// Creator
abstract class DessertFactory {
    abstract Dessert createDessert();
}

// Concrete Creators
class CakeFactory extends DessertFactory {
    Dessert createDessert() {
        return new Cake();
    }
}

class IceCreamFactory extends DessertFactory {
    Dessert createDessert() {
        return new IceCream();
    }
}

// 클라이언트 코드
public class Main {
    public static void main(String[] args) {
        DessertFactory factory = new CakeFactory(); // 상황에 따라 교체 가능
        Dessert dessert = factory.createDessert();
        dessert.eat();
    }
}
  • 확장이 자주 일어날 때 유용함
  • 객체의 생성과정을 캡슐화 하고 싶을때

4. Prototype

미리 만들어둔 객체를 복제해서 객체를 생성
new를 사용하지 않고 이미 있는 애들 복제해서 사용한다.

  • 앞에서 말한 Factory method와 뭐가 다른거지? -> Factory method는 공통의 부모 객체를 가져야 하기 때문에 재사용을 하는 것과는 거리가 있다.
class Shape implements Cloneable {
    private String color;
    
    public Shape(String color) {
        this.color = color;
    }

    @Override
    public Shape clone() {
        try {
            return (Shape) super.clone();  // 메모리 복사
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

    public void setColor(String color) {
        this.color = color;
    }

    public String toString() {
        return "Shape with color: " + color;
    }
}

// 3. 클라이언트 코드
public class Main {
    public static void main(String[] args) {
        Shape original = new Shape("red");
        Shape copy = original.clone();

        copy.setColor("blue");

        System.out.println(original); // red
        System.out.println(copy);     // blue
    }
}
  • 이렇게 new를 한번만 사용하고 복제를 통해 관리한다.
  • 결론: 그냥 clone이나 copy가 있으면 prototype임.

5. Singleton

한 클래스에 객체가 오직 하나만 존재하는 것을 보장하기 위해 사용한다.

class Singleton {
    public:
        static Singleton* GetInstance();
    private:
        static Singleton* s_instance;
        Singleton();
};
Singleton* Singleton::s_instance = NULL;
    
Singleton* Singleton::GetInstance () {
    if (s_instance == NULL) {
        s_instance = new Singleton;
    }
    return s_instance;
}

이렇게 전역으로 Singleton객체를 가지는 전역 변수를 만들고, 이걸 호출해서 유일하게 만들면 되는 것이다. 참고로 싱글톤 패턴은 게임개발에서 필수적으로 사용되는 중요한 기법이다.

GameManager, SystemManager등등 자주 사용하게 된다.

profile
제가 관심있고 공부하고 싶은걸 정리하는 저만의 노트입니다.

0개의 댓글