자주사용되는 디자인패턴 1

자주사용되는 디자인패턴 1

Python
DesignPattern
자주사용되는 디자인패턴 1
Author

gabriel yang

Published

September 25, 2024


디자인 패턴은 소프트웨어 개발에서 코드의 유지보수성을 높이고, 효율적이고 재사용 가능한 코드를 작성하기 위한 핵심 개념입니다. 다양한 패턴 중에서 성능과 최적화에 유리한 패턴을 선택하는 것은 특히 중요합니다.

1. 싱글톤 패턴 (Singleton Pattern)

싱글톤 패턴은 애플리케이션 내에서 클래스의 인스턴스를 하나만 생성하는 것을 보장합니다. 자주 사용되는 리소스를 전역적으로 하나만 생성하여 메모리를 절약하고 성능을 높이는 데 유용합니다.

사용 사례:

  • 데이터베이스 연결
  • 설정 관리 객체
  • 이 패턴은 전역 상태를 유지해야 할 필요가 있는 상황에서 유용하게 사용

예제 코드:

class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(Singleton, cls).__new__(cls)
        return cls._instance

# 사용 예시
singleton1 = Singleton()
singleton2 = Singleton()

print(singleton1 == singleton2)  # True, 동일한 인스턴스
True
  • Singleton 클래스는 _instance라는 클래스 변수를 정의합니다. 이 변수는 클래스의 인스턴스를 저장하는 용도로 사용됩니다.
  • new 메서드는 객체를 생성할 때 호출되는 메서드입니다. 이 메서드에서 인스턴스가 생성되는 과정을 제어합니다. 객체가 생성되기 전에 호출되므로, 새로운 인스턴스를 만들고 반환하는 로직을 정의할 수 있습니다.
  • __init__는 인스턴스가 생성된 후 초기화 작업을 수행하는 메서드로, 이미 생성된 인스턴스가 존재하는 경우에만 호출됩니다.
  • cls: Python의 클래스 메서드 및 new 메서드에서 사용되는 첫 번째 매개변수로, 현재 클래스 자체를 참조합니다. 인스턴스 메서드에서 self가 현재 인스턴스를 참조하는 것과 유사한 역할을 합니다.
  • if cls._instance is None: _instance가 None인지 확인합니다. 이는 클래스의 인스턴스가 아직 생성되지 않았음을 의미합니다.
  • cls._instance = super(Singleton, cls).__new__(cls): 인스턴스가 생성되지 않았다면, super()를 사용하여 부모 클래스의 new 메서드를 호출하여 인스턴스를 생성합니다.
  • return cls._instance: 항상 동일한 인스턴스를 반환합니다. _instance가 이미 존재한다면, 그 인스턴스를 반환합니다.

이 코드는 Singleton 클래스의 인스턴스를 하나만 생성하고, 이후에 생성된 모든 인스턴스는 동일한 객체를 참조하게 됩니다.

2. 팩토리 메서드 패턴 (Factory Method Pattern)

팩토리 메서드 패턴은 객체 생성을 하위 클래스에 위임하여 구체적인 클래스의 타입을 지정하지 않고도 객체를 생성할 수 있게 해줍니다. 코드의 확장성과 유연성을 높이는 데 유용합니다.

팩토리 메서드 패턴은 객체 생성 로직을 하위 클래스에 위임합니다. 즉, 객체 생성에 관한 결정을 상위 클래스가 아닌 하위 클래스에서 내리는 방식입니다. 팩토리 메서드 패턴은 객체 생성 코드를 확장 가능한 방식으로 관리할 수 있다는 장점이 있습니다.

클라이언트는 구체적인 클래스에 의존하지 않고 객체를 생성하고 사용할 수 있게 됩니다. 이 패턴은 특히 다양한 종류의 객체를 생성해야 하거나, 객체 생성 로직을 확장해야 하는 상황에서 유용합니다.

사용 사례:

  • 객체 생성 로직이 복잡할 때
  • 클래스 인스턴스를 동적으로 선택해야 할 때

예제 코드:

from abc import ABC, abstractmethod

# 결제 방식의 인터페이스
class PaymentMethod(ABC):
    @abstractmethod
    def pay(self, amount):
        pass

# 카드 결제 방식
class CardPayment(PaymentMethod):
    def pay(self, amount):
        print(f"Paid {amount} using Card.")

# 페이팔 결제 방식
class PayPalPayment(PaymentMethod):
    def pay(self, amount):
        print(f"Paid {amount} using PayPal.")

# 비트코인 결제 방식
class BitcoinPayment(PaymentMethod):
    def pay(self, amount):
        print(f"Paid {amount} using Bitcoin.")


# 결제 방식을 동적으로 선택하는 함수
def get_payment_method(method: str) -> PaymentMethod:
    if method == "card":
        return CardPayment()
    elif method == "paypal":
        return PayPalPayment()
    elif method == "bitcoin":
        return BitcoinPayment()
    else:
        raise ValueError(f"Unknown payment method: {method}")


def process_payment(method: str, amount: float):
    # 결제 방식을 동적으로 선택
    payment_method = get_payment_method(method)
    # 선택된 결제 방식으로 결제 처리
    payment_method.pay(amount)

# 사용 예시
process_payment("card", 100.0)      # Paid 100.0 using Card.
process_payment("paypal", 200.0)    # Paid 200.0 using PayPal.
process_payment("bitcoin", 300.0)   # Paid 300.0 using Bitcoin.
Paid 100.0 using Card.
Paid 200.0 using PayPal.
Paid 300.0 using Bitcoin.
  • 결제 방식 선택: get_payment_method() 함수는 문자열로 결제 방식을 입력받고, 해당하는 클래스의 인스턴스를 반환합니다.
  • 동적 인스턴스 생성: 사용자가 선택한 결제 방식에 따라 카드 결제, 페이팔 결제, 비트코인 결제 클래스 중 하나가 동적으로 선택됩니다.
  • 클라이언트 코드: 결제 방식을 동적으로 선택하고, 선택된 결제 방식으로 pay() 메서드를 호출하여 결제를 진행합니다.

팩토리 메서드 패턴을 사용하면 다양한 객체를 동적으로 생성할 수 있으며, 객체 생성 로직을 유연하게 관리할 수 있습니다.

3. 전략 패턴 (Strategy Pattern)

전략 패턴은 알고리즘을 객체로 캡슐화하여 실행 시점에 알고리즘을 교체할 수 있게 합니다. 여러 알고리즘이 필요하고, 이 중에서 하나를 선택해야 할 때 유용합니다.

전략 패턴은 다음과 같은 상황에서 유용합니다.

  • 알고리즘이 다양하거나 바뀔 가능성이 있는 경우
  • 여러 알고리즘을 사용할 수 있으며, 그 중 하나를 선택해야 할 경우
  • 런타임에 알고리즘을 변경해야 할 필요가 있을 때

전략 패턴의 구조

  • Context (컨텍스트): 특정 전략을 사용하는 클래스입니다. 이 클래스는 클라이언트 코드에서 호출되며, 내부에서 어떤 전략을 사용할지 결정합니다.
  • Strategy (전략 인터페이스): 알고리즘을 캡슐화한 공통 인터페이스입니다. 이를 통해 다양한 전략(알고리즘)이 동일한 방식으로 사용될 수 있습니다.
  • Concrete Strategy (구체적인 전략 클래스): 전략 인터페이스를 구현한 여러 알고리즘 클래스입니다. 각 클래스는 특정 알고리즘을 제공합니다.

전략 패턴의 장점

  • 유연성 증가: 런타임에 행위를 쉽게 변경할 수 있습니다.
  • 코드 재사용성: 알고리즘을 캡슐화하므로, 알고리즘이 서로 독립적으로 수정될 수 있습니다.
  • 응집력 증가: 하나의 클래스가 너무 많은 책임을 지지 않도록 행위를 별도의 클래스로 분리합니다.

사용 사례:

  • 정렬 알고리즘 선택
  • 결제 방식 선택

예제 코드:

from abc import ABC, abstractmethod

class Strategy(ABC):
    @abstractmethod
    def execute(self, data):
        pass

class ConcreteStrategyA(Strategy):
    def execute(self, data):
        return sorted(data)

class ConcreteStrategyB(Strategy):
    def execute(self, data):
        return sorted(data, reverse=True)

class Context:
    def __init__(self, strategy: Strategy):
        self._strategy = strategy

    def set_strategy(self, strategy: Strategy):
        self._strategy = strategy

    def do_some_business_logic(self, data):
        return self._strategy.execute(data)

# 사용 예시
data = [3, 1, 4, 1, 5]

context = Context(ConcreteStrategyA())
print(context.do_some_business_logic(data))  # [1, 1, 3, 4, 5]

context.set_strategy(ConcreteStrategyB())
print(context.do_some_business_logic(data))  # [5, 4, 3, 1, 1]
[1, 1, 3, 4, 5]
[5, 4, 3, 1, 1]
  • Strategy: 알고리즘의 인터페이스를 정의합니다.
  • ConcreteStrategyA/B: 각각 오름차순과 내림차순 정렬 알고리즘을 구현합니다.
  • Context: 전략을 설정하고 사용하여 데이터를 처리합니다.

전략 패턴의 주요 장점은 알고리즘을 클라이언트 코드와 독립적으로 정의하고 교환할 수 있게 함으로써, 유지 보수성을 높이고 확장성을 제공합니다. 이를 통해 알고리즘이 변경되더라도 기존 코드의 수정 없이 쉽게 새로운 알고리즘으로 교체할 수 있습니다.

4. 옵저버 패턴 (Observer Pattern)

옵저버 패턴은 객체의 상태 변화에 따라 다른 객체들에게 자동으로 통보하는 방식입니다. 이벤트 기반 시스템에서 사용됩니다.

옵저버 패턴은 한 객체(주제 또는 주관자)의 상태 변화에 따라 여러 객체(옵저버)가 자동으로 업데이트되도록 하는 행동 패턴입니다. 즉, 주관자 객체가 상태를 변경하면 그와 연결된 옵저버들이 이를 감지하고 자신을 업데이트하게 됩니다.

옵저버 패턴의 구성 요소

옵저버 패턴은 주로 다음과 같은 세 가지 구성 요소로 이루어져 있습니다:

  • 주관자 (Subject): 상태를 관리하고 옵저버를 등록 및 해제하며, 상태 변경 시 등록된 모든 옵저버에게 알립니다.
  • 옵저버 (Observer): 주관자의 상태 변화를 감지하고 이에 대응하는 객체입니다. 상태 변화에 따라 자신의 상태를 업데이트하는 메서드를 구현해야 합니다.
  • 구체적인 주관자 (Concrete Subject): 주관자 인터페이스를 구현한 클래스이며, 실제 상태를 저장하고 이를 변경할 수 있는 메서드를 가집니다.
  • 구체적인 옵저버 (Concrete Observer): 옵저버 인터페이스를 구현한 클래스이며, 주관자의 상태 변화에 따라 자신의 상태를 업데이트하는 방법을 정의합니다.

사용 사례:

  • 이벤트 처리 시스템
  • 실시간 데이터 갱신

예제 코드:

class Subject:
    def __init__(self):
        self._observers = []

    def add_observer(self, observer):
        self._observers.append(observer)

    def remove_observer(self, observer):
        self._observers.remove(observer)

    def notify_observers(self, message):
        for observer in self._observers:
            observer.update(message)

class Observer:
    def update(self, message):
        pass

class ConcreteObserverA(Observer):
    def update(self, message):
        print(f"Observer A received: {message}")

class ConcreteObserverB(Observer):
    def update(self, message):
        print(f"Observer B received: {message}")

# 사용 예시
subject = Subject()

observer_a = ConcreteObserverA()
observer_b = ConcreteObserverB()

subject.add_observer(observer_a)
subject.add_observer(observer_b)

subject.notify_observers("Event 1")
# Observer A received: Event 1
# Observer B received: Event 1
Observer A received: Event 1
Observer B received: Event 1

옵저버 패턴은 객체 간의 의존성을 최소화하면서, 상태 변화를 다른 객체에게 통보할 때 매우 유용합니다.

  • Subject 클래스는 옵저버를 관리하는 역할을 합니다. 주관자는 옵저버를 추가하고 제거하며, 상태 변화가 있을 때 모든 옵저버에게 알리는 기능을 갖추고 있습니다.
  • Observer 클래스는 옵저버의 기본 인터페이스로, update 메서드를 정의합니다. 실제 옵저버 클래스는 이 메서드를 구현해야 합니다.
  • ConcreteObserverA와 ConcreteObserverB는 Observer 클래스를 구현한 구체적인 옵저버입니다. 각 옵저버는 update 메서드를 통해 주관자에게서 전달받은 메시지를 출력합니다.

결론

디자인 패턴을 적절히 활용하면 코드의 유지보수성과 확장성을 높일 수 있으며, 성능 및 최적화 측면에서도 큰 이점을 가져올 수 있습니다. 이 글에서 다룬 패턴들인 싱글톤 패턴, 팩토리 메서드 패턴, 전략 패턴, 옵저버 패턴은 자주 사용되는 패턴들로, 다양한 프로젝트에서 유용하게 활용될 수 있습니다.