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

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

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

gabriel yang

Published

September 25, 2024


디자인 패턴은 소프트웨어 개발에서 자주 반복되는 문제를 해결하기 위한 일반적인 방법입니다. 앞서 설명한 패턴 외에도 실무에서 자주 사용되는 패턴들이 있습니다. 이번에는 그 외에 자주 사용하는 구조 패턴행위 패턴에 대해 설명하겠습니다.

1. 데코레이터 패턴 (Decorator Pattern)

데코레이터 패턴은 객체의 기능을 동적으로 확장할 때 사용됩니다. 상속을 사용하지 않고도 객체의 기능을 쉽게 확장할 수 있으며, 객체의 개별 책임을 추가하거나 변경할 수 있습니다.

데코레이터 패턴은 객체의 기능을 동적으로 추가할 수 있는 구조적 디자인 패턴입니다. 이 패턴을 사용하면 기존 객체를 변경하지 않고도 새로운 기능을 추가할 수 있습니다. 주로 기능을 추가하거나 수정하는 데 유용하며, 여러 개의 데코레이터를 조합하여 복잡한 동작을 구현할 수 있습니다.

이 패턴은 다음과 같은 상황에서 유용합니다:

  • 기능을 유연하게 추가하거나 변경해야 할 때.
  • 클래스 상속을 통해 기능을 추가하는 것이 비효율적일 때.
  • 여러 개의 기능 조합이 필요할 때.

데코레이터 패턴의 구성 요소

데코레이터 패턴은 다음과 같은 주요 구성 요소로 이루어져 있습니다:

  • 컴포넌트 (Component): 데코레이터가 추가될 수 있는 기본 인터페이스를 정의합니다.
  • 구체적인 컴포넌트 (Concrete Component): 컴포넌트를 구현하는 클래스이며, 기본 기능을 제공하는 객체입니다.
  • 데코레이터 (Decorator): 컴포넌트 인터페이스를 구현하고, 참조할 컴포넌트를 포함하여 기능을 추가합니다.
  • 구체적인 데코레이터 (Concrete Decorators): 데코레이터를 확장하여 특정 기능을 추가하는 클래스입니다.

사용 사례:

  • 새로운 기능을 객체에 추가해야 하지만 상속이 적합하지 않은 경우
  • 객체의 책임을 런타임에 변경하고 싶을 때

예제 코드:

from abc import ABC, abstractmethod

# 컴포넌트 인터페이스
class Coffee(ABC):
    @abstractmethod
    def cost(self) -> float:
        pass

# 구체적인 컴포넌트
class SimpleCoffee(Coffee):
    def cost(self) -> float:
        return 2.0  # 기본 커피 가격

# 데코레이터
class CoffeeDecorator(Coffee):
    def __init__(self, coffee: Coffee):
        self._coffee = coffee

    @abstractmethod
    def cost(self) -> float:
        pass

# 구체적인 데코레이터
class MilkDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.5  # 우유 추가 가격

class SugarDecorator(CoffeeDecorator):
    def cost(self) -> float:
        return self._coffee.cost() + 0.3  # 설탕 추가 가격

# 사용 예시
coffee = SimpleCoffee()
print(f"Basic coffee cost: ${coffee.cost():.2f}")

# 우유와 설탕을 추가한 커피
milk_coffee = MilkDecorator(coffee)
print(f"Coffee with milk cost: ${milk_coffee.cost():.2f}")

sugar_milk_coffee = SugarDecorator(milk_coffee)
print(f"Coffee with milk and sugar cost: ${sugar_milk_coffee.cost():.2f}")
Basic coffee cost: $2.00
Coffee with milk cost: $2.50
Coffee with milk and sugar cost: $2.80
  • ABC를 상속받는 이유는 추상 클래스(Abstract Class)를 만들기 위함입니다. 추상 클래스는 객체를 직접 생성할 수 없는 클래스이며, 일반적으로 공통된 인터페이스를 정의하고, 이를 상속받는 자식 클래스에서 그 인터페이스를 구체적으로 구현하도록 강제합니다.
  • Coffee 클래스는 커피의 가격을 반환하는 cost() 메서드를 정의한 추상 클래스입니다.
  • SimpleCoffee 클래스는 기본 커피를 구현한 클래스입니다. 가격은 2.0으로 설정되어 있습니다.
  • CoffeeDecorator 클래스는 Coffee 인터페이스를 구현하며, 다른 커피 객체를 포함할 수 있도록 하는 기본 데코레이터 클래스입니다. 추가 기능을 구현하기 위해 cost() 메서드를 추상 메서드로 정의합니다.
  • MilkDecorator와 SugarDecorator 클래스는 각각 우유와 설탕을 추가하는 데코레이터입니다. 이들은 cost() 메서드를 구현하여 기본 커피 가격에 추가 가격을 더합니다.
  • 기본 커피를 생성하고, 우유와 설탕을 추가한 커피를 생성하여 각각의 가격을 출력합니다.

데코레이터 패턴은 객체에 동적으로 기능을 추가할 수 있어, 상속보다 유연하고 직관적인 방법을 제공합니다.

2. 어댑터 패턴 (Adapter Pattern)

어댑터 패턴은 서로 다른 인터페이스를 사용하는 클래스들이 함께 동작할 수 있도록 변환해주는 역할을 합니다. 기존 코드를 변경하지 않고 새로운 기능을 추가할 때 유용합니다.

서로 호환되지 않는 인터페이스를 가진 두 객체 간의 통신을 가능하게 하는 구조적 디자인 패턴입니다. 이 패턴은 기존의 클래스를 재사용하면서도 새로운 인터페이스에 맞게 변환할 수 있도록 도와줍니다. 어댑터 패턴을 사용하면 코드의 유연성을 높이고, 시스템의 확장성을 향상시킬 수 있습니다.

주요 구성 요소

  • 타겟(Target):클라이언트가 기대하는 인터페이스를 정의합니다. 어댑터가 변환할 수 있는 표준 인터페이스입니다.
  • 어댑터(Adapter): 타겟 인터페이스를 구현하고, 클라이언트의 요청을 실제 서비스나 클래스에 전달합니다. 즉, 호환되지 않는 인터페이스를 연결해주는 역할을 합니다.
  • 어댑티(Adaptee): 기존의 클래스로, 클라이언트가 직접 사용할 수 없는 인터페이스를 가진 클래스입니다. 어댑터를 통해 이 클래스를 사용할 수 있게 됩니다.
  • 클라이언트(Client): 타겟 인터페이스를 사용하여 어댑터를 통해 어댑티와 상호작용하는 코드입니다

어댑터 패턴의 동작 과정

  • 클라이언트는 타겟 인터페이스에 정의된 메서드를 호출합니다.
  • 어댑터는 클라이언트의 호출을 받아서 어댑티의 메서드를 호출합니다.
  • 어댑티는 자신이 구현한 로직을 수행하여 결과를 반환하고, 어댑터는 이 결과를 클라이언트에 전달합니다.

사용 사례:

  • 호환되지 않는 인터페이스를 가진 객체들을 함께 사용할 때
  • 외부 라이브러리나 시스템과 통합해야 할 때

예제 코드:

# 타겟 인터페이스
class Target:
    def request(self) -> str:
        return "Default Target"

# 어댑티
class Adaptee:
    def specific_request(self) -> str:
        return "Specific Request"

# 어댑터
class Adapter(Target):
    def __init__(self, adaptee: Adaptee):
        self._adaptee = adaptee

    def request(self) -> str:
        return self._adaptee.specific_request()

# 클라이언트
def client_code(target: Target):
    print(target.request())

# 사용 예시
adaptee = Adaptee()
adapter = Adapter(adaptee)
client_code(adapter)  # 출력: Specific Request
Specific Request
  • 타겟 클래스: Target 클래스는 클라이언트가 사용할 수 있는 메서드 request()를 정의합니다.
  • 어댑티 클래스: Adaptee 클래스는 specific_request() 메서드를 가지고 있으며, 클라이언트가 직접 사용할 수 없습니다.
  • 어댑터 클래스: Adapter 클래스는 Target을 상속받고, Adaptee의 인스턴스를 내부에서 관리합니다. request() 메서드를 재정의(오버라이딩)하여 specific_request() 메서드를 호출합니다.
  • 클라이언트 함수: client_code()는 Target 인터페이스를 통해 어댑터의 메서드를 호출합니다.

어댑터 패턴은 기존 시스템을 변경하지 않고도 새 시스템에 맞게 객체를 변환해주는 역할을 합니다.

3. 프록시 패턴 (Proxy Pattern)

프록시 패턴은 객체에 대한 접근을 제어하거나 객체 생성 비용을 줄이기 위해 대리 객체를 사용하는 패턴입니다. 원래 객체에 대한 직접적인 접근을 제어하면서, 지연 로딩이나 권한 관리를 할 수 있습니다.

이 패턴은 다른 객체에 대한 접근을 제어하는 객체를 제공하는 구조적 디자인 패턴입니다. 프록시 객체는 원래 객체를 대리하여, 원본 객체에 대한 작업을 수행하거나, 그에 대한 추가적인 기능(예: 접근 제어, 로깅, 캐싱 등)을 제공합니다. 이 패턴은 클라이언트와 실제 객체 간의 인터페이스를 중재하여 다양한 기능을 추가하거나 성능을 향상시키는 데 유용합니다.

주요 구성 요소

  • 인터페이스 (Subject): 클라이언트가 사용하는 공통 인터페이스를 정의합니다. 프록시 객체와 실제 객체 모두 이 인터페이스를 구현합니다.
  • 리얼 서브젝트 (RealSubject): 실제 작업을 수행하는 객체입니다. 프록시가 참조하는 객체로, 프록시 패턴의 핵심 기능을 구현합니다.
  • 프록시 (Proxy): 클라이언트의 요청을 처리하고, 필요한 경우 리얼 서브젝트에 위임합니다. 프록시는 접근 제어, 로깅, 캐싱 등의 추가적인 작업을 수행할 수 있습니다.

사용 사례:

  • 원격 객체에 접근할 때
  • 무거운 리소스를 필요로 하는 객체의 지연 로딩
  • 권한 관리 또는 접근 제어

프록시 패턴의 동작 과정

  • 클라이언트는 프록시 객체를 통해 요청을 보냅니다.
  • 프록시는 요청을 받고, 필요한 경우 추가 작업을 수행한 후, 리얼 서브젝트에 요청을 위임합니다.
  • 리얼 서브젝트는 실제 작업을 수행하고 결과를 반환합니다.
  • 프록시는 그 결과를 클라이언트에게 전달합니다.

예제 코드:

class RealSubject:
    def request(self):
        return "RealSubject: Handling request."

class Proxy:
    def __init__(self):
        self._real_subject = None

    def request(self):
        if self._real_subject is None:
            self._real_subject = RealSubject()
        return self._real_subject.request()

# 사용 예시
proxy = Proxy()
print(proxy.request())  # RealSubject: Handling request.
RealSubject: Handling request.

프록시 패턴은 객체를 바로 생성하지 않고, 필요할 때만 생성함으로써 성능을 최적화할 수 있습니다.

4. 컴포지트 패턴 (Composite Pattern)

컴포지트 패턴은 객체를 트리 구조로 구성하여 부분-전체 관계를 나타냅니다. 클라이언트는 개별 객체와 객체의 그룹을 동일하게 처리할 수 있습니다.

이 패턴은 객체를 트리 구조로 구성하여 부분-전체 관계를 표현하는 구조적 디자인 패턴입니다. 이 패턴은 클라이언트가 단일 객체와 복합 객체를 동일하게 다룰 수 있도록 하여, 복잡한 구조를 단순하게 사용할 수 있게 해줍니다.

주요 구성 요소

  • 컴포넌트(Component): 공통 인터페이스를 정의하며, 단일 객체와 복합 객체 모두가 이 인터페이스를 구현합니다. 일반적으로 공통 메서드를 정의합니다.
  • 리프(Leaf): 컴포넌트 인터페이스를 구현하는 단일 객체입니다. 더 이상 자식 객체를 가지지 않는 기본 객체입니다.
  • 복합체(Composite): 컴포넌트 인터페이스를 구현하며, 자식 컴포넌트를 포함할 수 있는 복합 객체입니다. 다른 컴포넌트(리프 또는 다른 복합체)를 포함할 수 있습니다.

컴포지트 패턴의 동작 과정

  • 클라이언트는 컴포넌트 인터페이스를 통해 리프 객체나 복합체를 생성합니다.
  • 복합체는 자식 컴포넌트를 추가하거나 제거할 수 있으며, 자식 컴포넌트의 메서드를 호출할 수 있습니다.
  • 클라이언트는 모든 컴포넌트를 동일한 방식으로 다룰 수 있습니다.

사용 사례:

  • 트리 구조를 형성하여 계층적으로 객체를 관리할 때
  • 개별 객체와 객체 그룹을 동일하게 처리해야 할 때
  • 이 패턴은 GUI 시스템, 파일 시스템, 문서 구조 등 다양한 분야에서 널리 사용됩니다.

예제 코드:

from abc import ABC, abstractmethod

# 컴포넌트 인터페이스
class Component(ABC):
    @abstractmethod
    def operation(self) -> str:
        pass

# 리프
class Leaf(Component):
    def __init__(self, name: str):
        self._name = name

    def operation(self) -> str:
        return f"Leaf: {self._name}"

# 복합체
class Composite(Component):
    def __init__(self):
        self._children = []

    def add(self, component: Component):
        self._children.append(component)

    def remove(self, component: Component):
        self._children.remove(component)

    def operation(self) -> str:
        results = [child.operation() for child in self._children]
        return f"Composite:\n" + "\n".join(results)

# 클라이언트
def client_code(component: Component):
    print(component.operation())

# 사용 예시
leaf1 = Leaf("Leaf 1")
leaf2 = Leaf("Leaf 2")
composite = Composite()

composite.add(leaf1)
composite.add(leaf2)

client_code(composite)  # Composite:\nLeaf: Leaf 1\nLeaf: Leaf 2
client_code(leaf1)      # Leaf: Leaf 1
Composite:
Leaf: Leaf 1
Leaf: Leaf 2
Leaf: Leaf 1
  • 컴포넌트 인터페이스: Component는 공통 인터페이스로, 리프와 복합체가 이 인터페이스를 구현합니다.
  • 리프: Leaf 클래스는 단일 객체를 나타내며, 자신만의 이름을 가지고 operation 메서드를 구현합니다.
  • 복합체: Composite 클래스는 다른 컴포넌트를 포함할 수 있는 복합 객체입니다. add 및 remove 메서드를 통해 자식 컴포넌트를 관리합니다.
  • 클라이언트: client_code 함수는 컴포넌트의 operation 메서드를 호출하여 결과를 출력합니다. 클라이언트는 리프 객체와 복합체를 동일하게 다룰 수 있습니다.

컴포지트 패턴은 개별 객체와 그룹 객체를 동일하게 취급할 수 있어, 복잡한 트리 구조를 쉽게 관리할 수 있게 해줍니다.

5. 명령 패턴 (Command Pattern)

명령 패턴은 요청을 객체로 캡슐화하여, 명령의 실행을 큐에 저장하거나 나중에 실행할 수 있도록 합니다. 이 패턴은 명령의 실행과 요청자를 분리하여 유연성을 높입니다.

이 패턴은 요청을 객체로 캡슐화하여, 요청을 호출하는 발신자와 요청을 처리하는 수신자 간의 결합도를 줄이는 구조적 디자인 패턴입니다. 이 패턴은 요청을 객체로 변환하여, 요청의 저장, 큐잉, 로그 기록 및 요청 실행 취소와 같은 기능을 지원할 수 있게 합니다.

주요 구성 요소

  • 명령(Command): 실행할 작업을 정의하는 인터페이스입니다. 구체적인 명령 객체는 이 인터페이스를 구현하여 특정 작업을 수행합니다.
  • 구체적인 명령(ConcreteCommand): 명령 인터페이스를 구현하고, 수신자 객체의 특정 작업을 호출하는 클래스입니다. 실행할 작업의 정보를 포함합니다.
  • 수신자(Receiver): 실제 작업을 수행하는 객체입니다. 명령이 실행될 때 호출되는 메서드를 정의합니다.
  • 발신자(Invoker): 명령 객체를 저장하고 실행하는 역할을 하는 객체입니다. 클라이언트는 발신자를 통해 명령을 요청합니다.
  • 클라이언트(Client): 명령 객체와 수신자를 생성하고 발신자에게 명령을 설정하는 객체입니다.

명령 패턴의 동작 과정

  • 클라이언트는 구체적인 명령 객체를 생성하고, 수신자 객체를 해당 명령 객체에 설정합니다.
  • 클라이언트는 발신자에게 명령을 설정합니다.
  • 발신자는 명령을 실행합니다. 이때 명령 객체는 수신자의 메서드를 호출하여 실제 작업을 수행합니다.

사용 사례:

  • 작업 실행을 큐에 저장하고 나중에 실행할 때
  • 실행 취소 기능을 구현할 때

예제 코드:

# 명령 인터페이스
class Command:
    def execute(self):
        pass

# 수신자
class Light:
    def turn_on(self):
        print("The light is on.")

    def turn_off(self):
        print("The light is off.")

# 구체적인 명령
class TurnOnCommand(Command):
    def __init__(self, light: Light):
        self._light = light

    def execute(self):
        self._light.turn_on()

class TurnOffCommand(Command):
    def __init__(self, light: Light):
        self._light = light

    def execute(self):
        self._light.turn_off()

# 발신자
class RemoteControl:
    def __init__(self):
        self._command = None

    def set_command(self, command: Command):
        self._command = command

    def press_button(self):
        self._command.execute()

# 클라이언트
def client_code():
    light = Light()
    turn_on = TurnOnCommand(light)
    turn_off = TurnOffCommand(light)

    remote = RemoteControl()

    remote.set_command(turn_on)
    remote.press_button()  # The light is on.

    remote.set_command(turn_off)
    remote.press_button()  # The light is off.

# 사용 예시
client_code()
The light is on.
The light is off.
  • 명령 인터페이스: Command는 모든 명령의 공통 인터페이스입니다.
  • 수신자: Light 클래스는 실제 작업(조명 켜기 및 끄기)을 수행하는 수신자입니다.
  • 구체적인 명령: TurnOnCommand와 TurnOffCommand 클래스는 각각 조명을 켜고 끄는 작업을 수행하는 구체적인 명령입니다. 이들은 Light 객체를 통해 수신자의 메서드를 호출합니다.
  • 발신자: RemoteControl 클래스는 명령 객체를 설정하고, 버튼을 눌러 명령을 실행하는 역할을 합니다.
  • 클라이언트: client_code 함수에서 클라이언트는 명령 객체와 수신자를 설정하고 발신자를 통해 명령을 실행합니다.

명령 패턴은 요청을 객체로 캡슐화하여 나중에 실행할 수 있으며, 작업 취소 및 반복 실행이 필요한 상황에서 유용하게 사용됩니다.

결론

자주 사용되는 디자인 패턴들은 각각의 목적에 따라 소프트웨어의 유연성을 높이고, 성능을 최적화하며, 유지보수성을 개선할 수 있습니다. 이번 글에서는 데코레이터 패턴, 어댑터 패턴, 프록시 패턴, 컴포지트 패턴, 명령 패턴과 같은 구조적, 행위적 패턴들을 살펴보았습니다. 이 패턴들을 이해하고 실무에서 적절하게 적용하면, 코드의 품질과 효율성이 크게 향상될 수 있습니다.