소프트웨어 디자인 패턴 [생성 패턴]

밀루·2023년 10월 14일
0

괴발개발 개발일지

목록 보기
21/26

Creational Patterns

코드의 확장성과 재사용성을 높이는 디자인 패턴이다.

1. Factory Method

부모 클래스로 객체를 생성하는 방식으로, 자식 클래스가 객체의 타입을 바꿀 수 있는 디자인 패턴이다.

어떨 때 좋을까?

만약 트럭이라는 클래스로 트럭 객체를 관리하고 있다고 하자. 그런데 트럭이 아닌 배로 물품을 수송할 경우가 생겼다. 트럭이라는 클래스로 수송 함수를 관리하고 있었는데 수송 함수는 같게 쓰이지만 다른 property를 가진 객체를 또 추가해야한다면 기존 코드의 많은 구조를 바꾸어야 한다.

적용 방법

여러 하위 클래스들이 가질 공통 속성을 수퍼클래스로 정의하고, 수퍼클래스를 상속하는 하위클래스가 필요한 성질을 정의하도록 한다.

예제코드


class UserModel(BaseModel):
    id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
    username: str = Field(default=None)
    OU: str = Field(default=None)
    admin: bool = Field(default=None)
    # mongodb에서 foreignkey를 사용하려면 한 필드를 업데이트할때 모든 유관 필드를 업데이트해줘야 한다는 단점이 있음
    # password: str

    class Config:
        orm_mode = True
        allow_population_by_field_name = True
        arbitrary_types_allowed = True
        json_encoders = {ObjectId: str}
        shcema_extra = {
            "example": {
                "username": "John Doe",
                "OU": "example",
                "admin": False,
            }
        }


class UserLoginModel(UserModel):
    username: str = Field(default=None)
    password: str = Field(defualt=None)

    class Config:
        the_schema = {
            "user_demo": {
                "username": "user_demo",
                "password": 123,
            }
        }
    

실제로 내가 만든 FastAPI 로그인과 관련된 함수를 찾아왔다.
usermodel과 userloginmodel이 있는데, usermodel은 유저를 생성할때, userloginmodel은 유저가 로그인할때 사용하는 모델이다.
UserLoginModel이 UserModel을 상속하므로 UserModel 안의 모든 instance를 가지고 있지만 (e.g. admin, OU 등) 막상 로그인할때 사용하는 것은 아이디와 패스워드 뿐이다.
이런 상속을 통해 부모 클래스의 property를 그대로 가져오면서 password등 필요한 부분을 확장해 쓸 수 있다.

2. Abstract Factory

구체적인 클래스에 의존하지 않고 관련 객체들을 만드는 방법.

어떨 때 좋을까?

소파, 의자, 테이블 등 여러 종류의 가구를 생산해야 하는데, 각 가구에도 여러 디자인이 존재한다고 생각해보자. 가구의 디자인은 아주 자주 바뀐다. 그리고 당신은 가구점을 운영하면서 즉각 손님이 요구하는 가구를 가져다 줘야 한다.

적용 방법


각 개체가 필수적으로 가져야 할 특성 (소파, 의자, 커피테이블 등)은 인터페이스에 정의하고 가구의 디자인 타입을 지정한 하위 클래스를 생성한다. 이 하위 클래스는 부모 클래스의 함수를 오버라이드함으로서 해당 객체가 필요한 특징을 구현할 수 있다.

추가 공부가 필요한 부분: 인터페이스와 클래스의 차이
https://www.tutorialspoint.com/differences-between-interface-and-class-in-java

예제 코드

from abc import ABC, abstractmethod

class Browser(ABC):
    """
    Creates "Abstract Product A"
    """

    # Interface - Create Search Toolbar
    @abstractmethod
    def create_search_toolbar(self):
        pass

    # Interface - Create Browser Window
    @abstractmethod
    def create_browser_window(self):
        pass

class Messenger(ABC):
    """
    Creates "Abstract Product B"
    """

    @abstractmethod
    # Interface - Create Messenger Window
    def create_messenger_window(self):
        pass
        
class VanillaBrowser(Browser):
    """
    Type: Concrete Product
    Abstract methods of the Browser base class are implemented.
    """

    # Interface - Create Search Toolbar
    def create_search_toolbar(self):
        print("Search Toolbar Created")

    # Interface - Create Browser Window]
    def create_browser_window(self):
        print("Browser Window Created")


class VanillaMessenger(Messenger):
    """
    Type: Concrete Product
    Abstract methods of the Messenger base class are implemented.
    """

    # Interface - Create Messenger Window
    def create_messenger_window(self):
        print("Messenger Window Created")

class SecureBrowser(Browser):
    """
    Type: Concrete Product
    Abstract methods of the Browser base class are implemented.
    """

    # Abstract Method of the Browser base class
    def create_search_toolbar(self):
        print("Secure Browser - Search Toolbar Created")

    # Abstract Method of the Browser base class
    def create_browser_window(self):
        print("Secure Browser - Browser Window Created")

    def create_incognito_mode(self):
        print("Secure Browser - Incognito Mode Created")


class SecureMessenger(Messenger):
    """
    Type: Concrete Product
    Abstract methods of the Messenger base class are implemented.
    """

    # Abstract Method of the Messenger base class
    def create_messenger_window(self):
        print("Secure Messenger - Messenger Window Created")

    def create_privacy_filter(self):
        print("Secure Messenger - Privacy Filter Created")

    def disappearing_messages(self):
        print("Secure Messenger - Disappearing Messages Feature Enabled")

java에 abstract class가 있다. 자바의 추상 클래스는 이를 상속받은 다른 모든 객체들이 "반드시" 메소드를 오버라이딩해야한다.

여기서 잠깐, 오버라이딩과 오버로드의 차이점이 뭘까?
overriding : 부모 클래스에서 정의한 method를 자식 클래스에서 재정의하는 것을 말한다.

overloading: 두 메서드가 같은 이름을 가지고 있으나 인자의 수나 자료형이 다른 경우를 말한다.

// 오버로딩
public double computeArea(Circle c) { ... }
public double computeArea(Circle c1, Circle c2) { ... }
public double computeArea(Square c) { ... }
https://gmlwjd9405.github.io/2018/08/09/java-overloading-vs-overriding.html

// 오버라이딩
public abstract class Shape {
  public void printMe() { System.out.println("Shape"); }
  public abstract double computeArea();
}
public class Circle extends Shape {
  private double rad = 5;
  @Override // 개발자의 실수를 방지하기 위해 @Override(annotation) 쓰는 것을 권장
  public void printMe() { System.out.println("Circle"); }
  public double computeArea() { return rad * rad * 3.15; }
}
public class Ambiguous extends Shape {
  private double area = 10;
  public double computeArea() { return area; }
}
https://gmlwjd9405.github.io/2018/08/09/java-overloading-vs-overriding.html

참고

추상 팩토리 패턴: https://gmlwjd9405.github.io/2018/08/08/abstract-factory-pattern.html

3. Builder Pattern


복잡한 클래스를 하나씩 정의할 수 있도록 해주는 클래스.
빌더 패턴은 복잡한 객체를 생성하는 클래스와 표현하는 클래스를 분리하여, 동일한 절차에서도 서로 다른 표현을 생성하는 방법을 제공한다.

팩토리 패턴과 추상 팩토리패턴의 문제점

  1. 클라이언트 프로그램으로부터 팩토리 클래스로 많은 parameter를 넘겨서 인스턴스를 생성해야 할 때
    1-1. 경우에 따라 불필요한 parameter는 일일이 null로 정의해줘야 한다.
  2. 만들어야 하는 자식 클래스가 무거워지고 복잡해지면 결국 부모인 팩토리 클래스도 복잡해지게 된다.

빌더 패턴은 Builder라는 별도의 클래스를 만들어 필수 값에 대해서는 생성자를 통해, 선택적인 값에 대해서는 메소드를 통해 step-by-step으로 값을 입력받는다. 그리고 builder() method를 이용해 최종적으로 하나의 인스턴스를 만든다.

어떨 때 쓸까?

복잡한 여러 단계를 거쳐 집을 짓는다고 생각해보자. 집이라는 instance에는 수많은 옵션이 붙어있다. 창문의 개수, 수영장 여부, 정원 여부, 문의 재질, 창문 모양 등 엄청나게 많은 특성들이 부여된다. 이 모든 특성을 한 클래스로 정의하면 어떤 옵션이 꼭 필수적인 옵션인지, 어떤 옵션이 어떤 값들을 가질 수 있는지에 대해 눈에 들어오지 않을 것이다.

이런 경우 빌더 패턴을 사용하면 좋다. 복잡한 객체를 생성하는 클래스와 property를 지정하는 클래스를 분리해, 한 눈에 각종 property를 관리하기 쉽게 만드는 것이다.
아래 파이썬 예제 코드를 보자.

예제 코드

class Car:
    def __init__(
        self,
        make,
        model,
        year,
        color,
        num_door,
        engine_type,
        transmission_type,
        sunroof=False,
        gps=False
    ):
        self.make=make
        self.model=model
        self.year=year
        self.color=color
        self.num_door=num_door
        self.engine_type=engine_type
        self.transmission_type=transmission_type
        self.sunroof=sunroof,
        self.gps=gps
        

class CarBuilder:
    def __init__(self):
        self.make=None
        self.model=None
        self.year=None
        self.color=None
        self.num_door=None
        self.engine_type=None
        self.transmission_type=None
        self.sunroof=None
        self.gps =None
    
    def set_make(self, make):
        self.model=model
        return self
        
    def set_year(self, year):
        self.year=year
        return self
    
    def set_color(self, year):
        self.year=year
        return self
    
    def set_num_doors(self, num_doors):
        self.num_doors=num_doors
        return self
        
    def set_engine_type(self, engine_type):
        self.=engine_type
        return self
        
    def set_transmission_type(self, transmission_type):
        self.=transmission_type
        return self
        
    def set_sunroof(self, sunroof):
        self.sunroof=sunroof
        return self
        
    def set_gps(self, gps):
        self.gps=gps
        return self
        
        
    def build(self):
        return Car(
            self.make,
            self.model,
            self.year,
            self.color,
            self.num_doors,
            self.engine_type,
            self.transmission_type,
            sunroof=self.sunroof,
            gps=self.gps
        )
        

if __name__ == '__main__:
    # 1안: builder 타입 없이 인스턴스를 생성하기
    my_car = Car(
        "Toyota",
        "Camry",
        2022,
        "Blue",
        4,
        "Gasoline",
        "Automatic",
        sunroof=True,
        gps=True
        )
    
    # 2안. builder 디자인 패턴대로 인스턴스를 생성하기
    # 복잡한 오브젝트를 만들 때 도움이 된다.
    builder = CarBuilder()
    my_car=builder.set_make("Toyota") \
                .set_model("Carmy") \
                .set_year(2022) \
                .set_color("Blue") \
                .set_engine_type("Gasoline") \
                .set_transmission_type("Automatic") \
                .set_sunroof(True) \
                .set_gps(True) \
                .build()

4. Singleton Pattern

어떨 때 좋을까?

오로지 단 하나의 객체만을 생성하는 클래스. 싱글턴 패턴을 이용하면 한 class가 늘 같은 메모리에 저장되는 동일한 instance만을 반환하기 때문에 instance의 유일성을 보장할 수 있다. 가령 자율주행차량의 차량 운행 정보를 저장하는 database라는 인스턴스가 필요하다고 가정하자. 이 인스턴스에는 차량의 속도, 방향이 저장되고 이 instance에 속도와 방향을 write하면 그 값대로 차량이 운행한다고 가정하자. 여러 센서들이 이 instance의 value를 읽어오는데, 이럴때마다 database를 생성하는 객체가 별도의 instance를 반환하면 제대로 운전을 할 수 없다. 따라서 이럴때 늘 같은 메모리에 저장되는 싱글턴 패턴이 적용된 클래스를 정의한다.

특징은 다음과 같다.
1. 공유하는 data에 대해 동시에 접속할 때
2. data에 접근하는 포인트를 하나만 만들어야 할 때
3. 한 프로그램이 실행되는 동안 한 class에서 단 하나의 instacne만을 만들어야 할 때.

예제 코드

class MongoManager:
     __instance = None
     @staticmethod 
     def getInstance():
         if MongoManager.__instance == None:
             MongoManager()
         return MongoManager.__instance
     def __init__(self):
        if MongoManager.__instance != None:
            raise Exception("This class is a singleton!")
        else:
            MongoManager.__instance = pymongo.MongoClient('localhost', 27017)
            
if __name__ == "__main__":
	db = MongoManager.getInstance()

keywords: getInstance, protect method(in case of c++ or java)

발생할 수 있는 문제점

  1. 경쟁 상태 발생
    멀티스레드에서 한가지 자원을 공유하는 상태에 흔히 일어날 수 있는 문제다. 한 스레드가 해당 데이터에 write를 하고 있을때 다른 스레드가 접근하면 race condition(경쟁 상태)가 발생할 수 있다.
  2. 테스트가 난해
    정적 필드는 프로그램이 종료되기 전까지 계속 살아있게 된다. 또한 singleton 패턴으로 만들어진 클래스의 인스턴스를 이용하는 다른 함수, 클래스들과도 강한 연관성을 가지므로 독립적인 테스트가 어렵다.
  3. 변경에 취약해진다.
    어디에서나 접근할 수 있으므로 싱글턴의 구조나 동작에 변경이 일어나면 싱글턴에 의존하고 있는 클래스에서도 역시 문제가 발생한다.

참고

https://refactoring.guru/design-patterns/catalog

abstract factory pattern 예제코드 참고:
https://stackabuse.com/abstract-factory-design-pattern-in-python/
빌더 클래스: https://4z7l.github.io/2021/01/19/design_pattern_builder.html
빌더 클래스 예제 코드 참고: https://www.linkedin.com/pulse/building-objects-easy-way-exploring-builder-pattern-adam-dejans-jr-
팩토리 메소드 예제 코드 참고: https://www.geeksforgeeks.org/factory-method-python-design-patterns/
Builder 클래스 설명 참고: https://readystory.tistory.com/121
싱글턴 패턴의 단점: https://blog.hexabrain.net/394#%EB%AC%B8%EC%A0%9C%EC%A0%90
싱글턴 패턴의 장점: https://velog.io/@yukina1418/OOP%EC%97%90%EC%84%9C-IoC%EC%99%80-DI%EA%B0%80-%EC%A4%91%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC-%EC%9E%91%EC%84%B1%EC%A4%91#%EC%8B%B1%EA%B8%80%ED%86%A4%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80
몽고connection을 singleton 패턴으로 만들기: https://stackoverflow.com/questions/59137628/cant-grasp-making-a-simple-singleton-in-python-for-a-mongodb-manager

profile
벨로그에 틀린 코드나 개선할 내용이 있을 수 있습니다. 지적은 언제나 환영합니다.

0개의 댓글