[디자인 패턴] 공부합시다

Junkyu_Kang·2024년 12월 12일

디자인 패턴은 소프트웨어 디자인 과정에서 자주 발생하는 문제들에 대한 전형적인 해결책이다!

이는 뭐.. 라이브러리나 함수처럼 붙여넣기는 불가능해요!
하지만 전형적인 패턴 방식을 사용함으로서 문제 해결을 쉽게 하기 위함이라 말할 수 있다!

뭐 알고리즘이랑 비슷한가 했지만 그건 아니다!
오히려 더 넓은 청사진? 느낌이랄까

패턴의 종류는 주요하게 3가지를 다룬다!

  1. 생성 패턴 : 기존 코드의 재사용과 유연성을 증가시키는 객체 생성 메커니즘 제공!
  2. 구조 패턴 : 구조를 유연하고 효율적으로 유지하면서 객체와 클래스를 더 큰 구조로 조합하는 방법!
  3. 행동 패턴 : 객체간의 효과적인 소통화 책임을 할당!

AI가 만들어준 코드를 같이 봐보자

생성패턴

  1. 팩토리 메서드 (Factory Method)

객체 생성을 서브클래스에 위임하는 패턴
클라이언트 코드가 구체적인 클래스 대신 인터페이스와 상호작용
예: 다양한 타입의 문서를 생성하는 문서 작성 애플리케이션

from abc import ABC, abstractmethod

class Product(ABC):
    @abstractmethod
    def operation(self):
        pass

class ConcreteProductA(Product):
    def operation(self):
        return "Product A 작동"

class ConcreteProductB(Product):
    def operation(self):
        return "Product B 작동"

class Creator(ABC):
    @abstractmethod
    def factory_method(self):
        pass

    def some_operation(self):
        product = self.factory_method()
        return f"Creator: {product.operation()}"

class ConcreteCreatorA(Creator):
    def factory_method(self):
        return ConcreteProductA()

class ConcreteCreatorB(Creator):
    def factory_method(self):
        return ConcreteProductB()
  1. 추상 팩토리 (Abstract Factory)

관련된 객체들의 집합을 생성하기 위한 인터페이스 제공
구체적인 클래스를 지정하지 않고 객체들의 집합을 생성
예: GUI 테마 생성 (윈도우, 버튼, 체크박스 등)

class AbstractFactory(ABC):
    @abstractmethod
    def create_product_a(self):
        pass

    @abstractmethod
    def create_product_b(self):
        pass

class ConcreteFactory1(AbstractFactory):
    def create_product_a(self):
        return ConcreteProductA1()
    
    def create_product_b(self):
        return ConcreteProductB1()

class ConcreteFactory2(AbstractFactory):
    def create_product_a(self):
        return ConcreteProductA2()
    
    def create_product_b(self):
        return ConcreteProductB2()
  1. 빌더 (Builder)

복잡한 객체의 생성 과정을 단계별로 분리
같은 생성 과정으로 다양한 표현의 객체 생성 가능
예: 복잡한 문서나 XML 생성

class Product:
    def __init__(self):
        self.parts = []

    def add(self, part):
        self.parts.append(part)

    def list_parts(self):
        return f"제품 부품: {', '.join(self.parts)}"

class Builder(ABC):
    @abstractmethod
    def build_part_a(self):
        pass

    @abstractmethod
    def build_part_b(self):
        pass

    @abstractmethod
    def get_result(self):
        pass

class ConcreteBuilder(Builder):
    def __init__(self):
        self.product = Product()

    def build_part_a(self):
        self.product.add("부품 A")

    def build_part_b(self):
        self.product.add("부품 B")

    def get_result(self):
        return self.product

class Director:
    def __init__(self, builder):
        self._builder = builder

    def build_minimal_product(self):
        self._builder.build_part_a()

    def build_full_product(self):
        self._builder.build_part_a()
        self._builder.build_part_b()
  1. 프로토타입 (Prototype)

기존 객체를 복제하여 새로운 객체 생성
객체 생성 비용이 높을 때 유용
깊은 복사(deep copy)와 얕은 복사(shallow copy) 지원

import copy

class Prototype(ABC):
    @abstractmethod
    def clone(self):
        pass

class ConcretePrototype(Prototype):
    def __init__(self, value):
        self.value = value
        self.complex_obj = [1, 2, 3]

    def clone(self):
        # 깊은 복사를 위해 copy.deepcopy() 사용
        return copy.deepcopy(self)
  1. 싱글톤 (Singleton)

클래스의 인스턴스를 하나만 생성하도록 보장
전역 상태 관리, 리소스 공유에 사용
스레드 세이프한 구현 필요

class Singleton:
    _instance = None

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

구조 패턴 (Structural Patterns)

  1. 어댑터 (Adapter)

호환되지 않는 인터페이스를 클라이언트가 기대하는 인터페이스로 변환
기존 코드의 수정 없이 새로운 인터페이스 적용 가능
예: 서드파티 라이브러리 통합

class Target:
    def request(self):
        return "표준 요청"

class Adaptee:
    def specific_request(self):
        return "특정 요청"

class Adapter(Target):
    def __init__(self, adaptee):
        self._adaptee = adaptee

    def request(self):
        return f"어댑터: {self._adaptee.specific_request()}"
  1. 브릿지 (Bridge)

추상화와 구현을 분리하여 독립적으로 변경 가능하게 함
복잡한 계층 구조 간소화
예: 다양한 플랫폼에서 작동하는 그래픽 드로잉 라이브러리

class Implementor(ABC):
    @abstractmethod
    def operation_implementation(self):
        pass

class ConcreteImplementorA(Implementor):
    def operation_implementation(self):
        return "구현체 A"

class ConcreteImplementorB(Implementor):
    def operation_implementation(self):
        return "구현체 B"

class Abstraction:
    def __init__(self, implementor):
        self._implementor = implementor

    def operation(self):
        return f"추상화: {self._implementor.operation_implementation()}"
        
  1. 복합체 (Composite)

객체들을 트리 구조로 구성
개별 객체와 복합 객체를 동일하게 다룰 수 있음
예: 파일 시스템, UI 컴포넌트 트리

class Component(ABC):
    @abstractmethod
    def operation(self):
        pass

    def add(self, component):
        pass

    def remove(self, component):
        pass

class Leaf(Component):
    def operation(self):
        return "리프"

class Composite(Component):
    def __init__(self):
        self._children = []

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

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

    def operation(self):
        return f"복합체: {[child.operation() for child in self._children]}"
  1. 데코레이터 (Decorator)

객체에 동적으로 새로운 기능 추가
상속 대신 래퍼(wrapper) 사용
예: 입출력 스트림 확장, UI 컴포넌트 스타일링

class BaseComponent:
    def operation(self):
        return "기본 컴포넌트"

class Decorator(BaseComponent):
    def __init__(self, component):
        self._component = component

    def operation(self):
        return self._component.operation()

class ConcreteDecoratorA(Decorator):
    def operation(self):
        return f"데코레이터 A({self._component.operation()})"

class ConcreteDecoratorB(Decorator):
    def operation(self):
        return f"데코레이터 B({self._component.operation()})"
        
  1. 퍼사드 (Facade)

복잡한 서브시스템에 대한 단순화된 인터페이스 제공
클라이언트와 서브시스템 간 결합도 감소
예: 복잡한 라이브러리 사용 단순화

class SubsystemA:
    def operation_a(self):
        return "서브시스템 A 작동"

class SubsystemB:
    def operation_b(self):
        return "서브시스템 B 작동"

class Facade:
    def __init__(self):
        self._subsystem_a = SubsystemA()
        self._subsystem_b = SubsystemB()

    def operation(self):
        return f"{self._subsystem_a.operation_a()} + {self._subsystem_b.operation_b()}"
  1. 플라이웨이트 (Flyweight)

메모리 사용을 최적화하기 위해 객체를 공유
대량의 유사 객체 생성 시 메모리 절약
예: 그래픽 에디터의 문자 렌더링

class Flyweight:
    def __init__(self, shared_state):
        self._shared_state = shared_state

    def operation(self, unique_state):
        return f"공유 상태: {self._shared_state}, 고유 상태: {unique_state}"

class FlyweightFactory:
    def __init__(self):
        self._flyweights = {}

    def get_flyweight(self, key):
        if key not in self._flyweights:
            self._flyweights[key] = Flyweight(key)
        return self._flyweights[key]
  1. 프록시 (Proxy)

다른 객체에 대한 접근을 제어하는 대리 객체
지연 로딩, 접근 제어, 로깅 등에 사용
예: 원격 리소스 접근, 대용량 이미지 로딩

class RealSubject:
    def request(self):
        return "실제 주체 요청"

class Proxy:
    def __init__(self, real_subject):
        self._real_subject = real_subject

    def request(self):
        print("프록시: 접근 제어")
        return self._real_subject.request()
        

행동 패턴 (Behavioral Patterns)

  1. 책임 연쇄 (Chain of Responsibility)

요청을 처리할 수 있는 객체를 동적으로 찾아 전달
요청 처리 객체 간 느슨한 결합
예: 로깅, 이벤트 핸들링 시스템

class Handler(ABC):
    def __init__(self, next_handler=None):
        self._next_handler = next_handler

    @abstractmethod
    def handle(self, request):
        pass

class ConcreteHandlerA(Handler):
    def handle(self, request):
        if request == "A":
            return "핸들러 A 처리"
        elif self._next_handler:
            return self._next_handler.handle(request)
        return "요청 처리 불가"

class ConcreteHandlerB(Handler):
    def handle(self, request):
        if request == "B":
            return "핸들러 B 처리"
        elif self._next_handler:
            return self._next_handler.handle(request)
        return "요청 처리 불가"
  1. 커맨드 (Command)

요청을 객체로 캡슐화
요청 실행, 취소, 재실행 지원
예: 메뉴 항목, 매크로, 실행 취소/다시 실행

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

class Receiver:
    def action_a(self):
        return "액션 A 수행"

    def action_b(self):
        return "액션 B 수행"

class ConcreteCommand(Command):
    def __init__(self, receiver):
        self._receiver = receiver

    def execute(self):
        return self._receiver.action_a()
  1. 반복자 (Iterator)

컬렉션 내부 표현을 노출하지 않고 순회 기능 제공
다양한 컬렉션 타입에 대해 일관된 순회 방식
예: 리스트, 트리, 그래프 순회

class Iterator(ABC):
    @abstractmethod
    def has_next(self):
        pass

    @abstractmethod
    def next(self):
        pass

class Collection(ABC):
    @abstractmethod
    def create_iterator(self):
        pass

class ConcreteCollection(Collection):
    def __init__(self):
        self._items = []

    def add_item(self, item):
        self._items.append(item)

    def create_iterator(self):
        return ConcreteIterator(self)

class ConcreteIterator(Iterator):
    def __init__(self, collection):
        self._collection = collection
        self._index = 0

    def has_next(self):
        return self._index < len(self._collection._items)

    def next(self):
        item = self._collection._items[self._index]
        self._index += 1
        return item
        
  1. 중재자 (Mediator)

객체 간 직접 통신 대신 중앙 중재자를 통해 통신
객체 간 결합도 감소
예: 대화상자, 채팅 애플리케이션

class Mediator(ABC):
    @abstractmethod
    def send(self, message, colleague):
        pass

class ConcreteMediator(Mediator):
    def __init__(self):
        self._colleague1 = None
        self._colleague2 = None

    def set_colleague1(self, colleague):
        self._colleague1 = colleague

    def set_colleague2(self, colleague):
        self._colleague2 = colleague

    def send(self, message, colleague):
        if colleague == self._colleague1:
            self._colleague2.notify(message)
        else:
            self._colleague1.notify(message)

class Colleague:
    def __init__(self, mediator):
        self._mediator = mediator

class ConcreteColleague1(Colleague):
    def send(self, message):
        self._mediator.send(message, self)

    def notify(self, message):
        print(f"동료 1 수신: {message}")

class ConcreteColleague2(Colleague):
    def send(self, message):
        self._mediator.send(message, self)

    def notify(self, message):
        print(f"동료 2 수신: {message}")
  1. 메멘토 (Memento)

객체의 상태를 저장하고 복원
되돌리기(Undo) 기능 구현
예: 문서 편집기의 실행 취소 기능

class Memento:
    def __init__(self, state):
        self._state = state

    def get_state(self):
        return self._state

class Originator:
    def __init__(self):
        self._state = None

    def set_state(self, state):
        self._state = state

    def save_to_memento(self):
        return Memento(self._state)

    def restore_from_memento(self, memento):
        self._state = memento.get_state()

class Caretaker:
    def __init__(self):
        self._mementos = []

    def add_memento(self, memento):
        self._mementos.append(memento)

    def get_memento(self, index):
        return self._mementos[index]
  1. 옵저버 (Observer)

객체 상태 변경 시 의존 객체들에 자동으로 알림
발행-구독(Publish-Subscribe) 모델
예: 이벤트 처리, UI 업데이트

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

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

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

    def notify(self):
        for observer in self._observers:
            observer.update(self._state)

    def set_state(self, state):
        self._state = state
        self.notify()

class Observer(ABC):
    @abstractmethod
    def update(self, state):
        pass

class ConcreteObserver(Observer):
    def update(self, state):
        print(f"옵저버 상태 업데이트: {state}")
  1. 상태 (State)

객체의 내부 상태에 따라 행동을 동적으로 변경
상태 전환 로직을 객체로 캡슐화
예: 상태 머신, 게임 캐릭터 상태

class State(ABC):
    @abstractmethod
    def handle(self):
        pass

class ConcreteStateA(State):
    def handle(self):
        return "상태 A 처리"

class ConcreteStateB(State):
    def handle(self):
        return "상태 B 처리"

class Context:
    def __init__(self, state):
        self._state = state

    def set_state(self, state):
        self._state = state

    def request(self):
        return self._state.handle()
  1. 전략 (Strategy)

런타임에 알고리즘 선택 가능
알고리즘 계열을 정의하고 상호 교환 가능하게 함
예: 정렬 알고리즘, 결제 방식

class Strategy(ABC):
    @abstractmethod
    def do_algorithm(self):
        pass

class ConcreteStrategyA(Strategy):
    def do_algorithm(self):
        return "전략 A 알고리즘"

class ConcreteStrategyB(Strategy):
    def do_algorithm(self):
        return "전략 B 알고리즘"

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

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

    def execute_strategy(self):
        return self._strategy.do_algorithm()
        
  1. 템플릿 메서드 (Template Method)

알고리즘의 골격을 정의하고 일부 단계를 서브클래스에 위임
코드 재사용성 증가
예: 문서 생성 프레임워크

class AbstractClass(ABC):
    def template_method(self):
        self.base_operation1()
        self.required_operation1()
        self.base_operation2()
        self.hook1()
        self.required_operation2()
        self.base_operation3()
        self.hook2()

    def base_operation1(self):
        print("기본 작업 1")

    def base_operation2(self):
        print("기본 작업 2")

    def base_operation3(self):
        print("기본 작업 3")

    def hook1(self):
        pass

    def hook2(self):
        pass

    @abstractmethod
    def required_operation1(self):
        pass

    @abstractmethod
    def required_operation2(self):
        pass

class ConcreteClass(AbstractClass):
    def required_operation1(self):
        print("구체적인 작업 1")

    def required_operation2(self):
        print("구체적인 작업 2")

    def hook1(self):
        print("구체적인 후크 1")
        
  1. 비지터 (Visitor)

알고리즘을 객체 구조에서 분리
새로운 연산을 기존 객체 구조 변경 없이 추가
예: 컴파일러의 AST(Abstract Syntax Tree) 방문


# 10. 비지터 (Visitor)
class Element(ABC):
    @abstractmethod
    

느낀 점

이번에 프로젝트 코드리뷰를 하며 배운 점은 디자인 패턴을 잘 쓰는 것도 중요하다이다
중복을 제거하되 코드를 보는 사람이 명확하고 쉽게 알 수 있어야 하더라

그렇게 abstract method로 class를 구상하고 그걸 상속받게 하니 자녀클래스에서 기능을 만들 때
일관성을 유지할 수 있었고 각 기능에 대한 설명이 들어가있으니 사용자도 쉽게 알 수 있었다.

그 후 싱글톤을 통해 DB 연결을 1회만 인스턴스화로 연결하여 사용하기도 했으며,

커맨드 패턴으로 요청을 클래스로 따로 빼고 캡슐화하여 내부 정보에 대해 알 수 없게 하였다!

게다가 load든 sgen이든 다양한 알고리즘을 2~3개씩 써야했던 터라 전략적으로 선택 가능하게끔 수정하였다

  • 대신 비동기로 진행을 하였고, 해당 내용을 pynng로 pub/sub 방식의 통신을 구현했기 때문에 port가 겹치지 않도록 할 필요가 있다.

이런 이유로 패턴을 쓰는구나를 알게 되었고.. 아무래도 프로젝트하다가 주먹구구식으로 길게 쓴 내 코드들은 문제가 많았구나 생각하게 된..?

profile
강준규

0개의 댓글