# 소프트웨어 디자인 패턴
소프트웨어 디자인 패턴은 소프트웨어 시스템의 구조와 동작에서 반복적으로 등장하는 문제에 대해 재사용 가능한 해결책을 제공하는 일반화된 설계 템닉들이다. 디자인 패턴은 특정한 코드 구현이 아니라 문제 상황에 대한 고수준의 설계 가이드라인으로서, 요소 간의 의존성을 줄이고, 확장성, 유지보수성, 재사용성을 높이는 데 중점을 둔다. 패턴은 독립적으로 작동하는 고유한 해결책이라기보다, 특정 맥락에서 의도된 구조를 이루는 데 도움을 주는 템플릿으로 이해된다.
이 문서는 대표적인 디자인 패턴들을 분류별로 정리하고, 각 패턴의 의도, 문제 상황, 구조적 구성요소(참여자), 해결 방법, 장단점 및 간단한 구현 예제를 제시한다. 용어는 영어로 표기되더라도 한국어 설명을 통해 개념의 이해를 돕는다.
---
## 패턴의 분류
디자인 패턴은 대개 다음의 세 가지 범주로 분류된다.
- Creational 패턴 (생성 패턴): 객체 생성의 과정을 관리하고, 객체 생성의 유연성과 캡슐화를 강화한다.
- 예: Singleton, Factory Method, Abstract Factory, Builder, Prototype
- Structural 패턴 (구조 패턴): 클래스와 객체의 구성으로 시스템의 구조를 단순화하고 확장성을 높인다.
- 예: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
- Behavioral 패턴 (행위 패턴): 클래스와 객체 간의 상호작용과 책임 분배를 명확히 하여 동작의 흐름을 제어한다.
- 예: Strategy, Observer, Command, Iterator, Visitor, State, Template Method, Mediator, Memento
각 패턴은 특정 상황에서의 문제를 다루고, 그 문제를 해결하기 위한 구조적 원칙과 참여자 간의 상호작용 규칙을 제시한다.
---
## 주요 디자인 패턴
### 1) Singleton (Creational)
- 의도(Intent): 클래스의 인스턴스가 하나만 존재하도록 보장하고, 전역적인 접근점을 제공한다.
- 문제 상황: 전역적으로 공유되는 하나의 자원(예: 설정, 로깅 인스턴스)이 필요하지만 중복된 인스턴스가 생성되는 것을 원치 않는 경우.
- 구조(참여자): Singleton (유일한 인스턴스) + Client
- 해결 방법: 클래스 내부에 유일한 인스턴스를 보관하고, 인스턴스에 대한 접근점을 제공한다.
- 장단점:
- 장점: 인스턴스 관리의 일관성, 전역 접근성.
- 단점: 테스트 어려움, 의존성 관리의 어려움, 다중 쓰레드 환경에서의 안전성 필요.
- 구현 예시 (Python):
```python
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
s1 = Singleton()
s2 = Singleton()
assert s1 is s2
```
- 적용 주의: 다중 인스턴스가 필요하거나 테스트를 쉽게 하기 위해서는 피하는 것이 바람직하다.
---
### 2) Factory Method (Creational)
- 의도: 객체 생성 로직을 서브클래스에 위임하여 구체적인 클래스에 대한 의존성을 제거한다.
- 문제 상황: 상위 레벨의 코드가 구체적인 구현 클래스에 의존하지 않도록 하고, 필요 시 생성 로직을 확장하고 싶을 때.
- 구조(참여자): Creator, ConcreteCreator, Product, ConcreteProduct
- 해결 방법: 팩토리 메서드를 통해 객체 생성을 위임하고, 필요한 경우 서로 다른 구체 클래스를 확장에 따라 반환한다.
- 장단점:
- 장점: 확장성 증가, 생성 로직의 분리로 결합도 감소.
- 단점: 코드 구조가 복잡해질 수 있음.
- 구현 예시 (Python):
```python
class Product:
def operation(self) -> str:
raise NotImplementedError
class ConcreteProductA(Product):
def operation(self) -> str:
return "Result of ConcreteProductA"
class Creator:
def factory_method(self) -> Product:
raise NotImplementedError
def some_operation(self) -> str:
product = self.factory_method()
return f"Creator: The same old code has just worked with {product.operation()}"
class ConcreteCreatorA(Creator):
def factory_method(self) -> Product:
return ConcreteProductA()
c = ConcreteCreatorA()
print(c.some_operation())
```
---
### 3) Abstract Factory (Creational)
- 의도: 관련된 객체들의 가족(family)을 생성하기 위한 인터페이스를 정의하고, 구체적인 팩토리를 바꿔 다양한 제품군을 쉽게 교체할 수 있게 한다.
- 문제 상황: 특정한 시점에 서로 호환되는 객체들의 패밀리로 구성된 계열을 생성해야 할 때.
- 구조(참여자): AbstractFactory, ConcreteFactory, AbstractProductA/B, ConcreteProductA/B
- 해결 방법: 서로 연관된 객체들의 패밀리를 생성하는 팩토리를 제공하고, 서로 호환되는 객체로 구성된 집합을 만든다.
- 장단점:
- 장점: 제품군 간의 호환성 보장, 구현의 독립성 강화.
- 단점: 새로운 제품군 추가 시 시스템의 많은 변경 필요.
- 구현 예시 (Python):
```python
class AbstractProductA:
def useful_function_a(self) -> str:
raise NotImplementedError
class AbstractProductB:
def useful_function_b(self) -> str:
raise NotImplementedError
class ConcreteProductA1(AbstractProductA):
def useful_function_a(self) -> str:
return "ProductA1"
class ConcreteProductB1(AbstractProductB):
def useful_function_b(self) -> str:
return "ProductB1"
class AbstractFactory:
def create_product_a(self) -> AbstractProductA:
raise NotImplementedError
def create_product_b(self) -> AbstractProductB:
raise NotImplementedError
class ConcreteFactory1(AbstractFactory):
def create_product_a(self) -> AbstractProductA:
return ConcreteProductA1()
def create_product_b(self) -> AbstractProductB:
return ConcreteProductB1()
```
---
### 4) Builder (Creational)
- 의도: 복잡한 객체의 생성 과정을 동일한 생성 과정에서 다르게 구성하고, 객체의 표현을 분리한다.
- 문제 상황: 객체의 구성 요소가 많고, 단계별 구축이 필요하거나, 다양한 표현의 객체를 생성하고자 할 때.
- 구조(참여자): Builder, ConcreteBuilder, Director, Product
- 해결 방법: 구성 요소를 하나씩 구축하는 Builder를 분리하고, Director가 구축 순서를 제시한다.
- 장단점:
- 장점: 객체 구성의 복잡도 관리, 표현의 독립성.
- 단점: 추가 클래스가 필요하여 코드 가독성 저하 가능.
- 구현 예시 (Python):
```python
class Product:
def __init__(self):
self.parts = []
def add(self, part):
self.parts.append(part)
class Builder:
def __init__(self):
self.product = Product()
def add_header(self): self.product.add("Header")
def add_body(self): self.product.add("Body")
def add_footer(self): self.product.add("Footer")
def build(self) -> Product: return self.product
builder = Builder()
builder.add_header()
builder.add_body()
product = builder.build()
print(product.parts)
```
---
### 5) Prototype (Creational)
- 의도: 기존 객체의 복제를 통해 새로운 객체를 생성하는 패턴으로, 생성 비용이 큰 객체의 복제에 적합하다.
- 문제 상황: 동일한 초기 상태의 객체를 다수 생성하지만, 각 객체의 초기화가 비용이 많이 들 때.
- 구조(참여자): Prototype, ConcretePrototype
- 해결 방법: 원형 객체(프로토타입)를 복제(clone)하여 새로운 객체를 생성한다.
- 장단점:
- 장점: 생성 비용 절감, 객체 상태의 손실 최소화.
- 단점: 깊은 복제 필요 시 복제 로직이 복잡해질 수 있음.
- 구현 예시 (Python):
```python
import copy
class Prototype:
def __init__(self, value):
self.value = value
def clone(self):
return copy.deepcopy(self)
p1 = Prototype([1, 2, 3])
p2 = p1.clone()
p2.value.append(4)
print(p1.value, p2.value)
```
---
### 6) Adapter (Structural)
- 의도(Intent): 기존 인터페이스를 그대로 사용해야 하지만, 기대하는 인터페이스가 다를 때 호환시키기 위해 중간 다리(Adapter)를 제공한다.
- 문제 상황: 서로 호환되지 않는 두 인터페이스를 함께 사용해야 할 때.
- 구조(참여자): Target, Adapter, Adaptee, Client
- 해결 방법: Adapter가 Adaptee의 인터페이스를 Target 인터페이스로 매핑한다.
- 장단점:
- 장점: 기존 코드의 재사용성 증가, 인터페이스 호환성 확보.
- 단점: 추가 계층으로 인한 성능/복잡도 증가 가능.
- 구현 예시 (Python):
```python
class Target:
def request(self) -> str:
raise NotImplementedError
class Adaptee:
def specific_request(self) -> str:
return "Adaptee response"
class Adapter(Target):
def __init__(self, adaptee):
self.adaptee = adaptee
def request(self) -> str:
return f"Adapter: {self.adaptee.specific_request()}"
adaptee = Adaptee()
adapter = Adapter(adaptee)
print(adapter.request())
```
---
### 7) Decorator (Structural)
- 의도(Intent): 객체의 기능을 동적으로 확장하거나 변경할 수 있도록, 원래 객체에 래퍼를 붙여 추가 책임을 부여한다.
- 문제 상황: 서브클래스 확장을 통해 기능을 확장하는 대신, 실행 시점에 유연하게 기능을 붙이고 제거하고 싶을 때.
- 구조(참여자): Component, ConcreteComponent, Decorator, ConcreteDecorator
- 해결 방법: Decorator가 Component의 인터페이스를 확장하고, 원래 객체를 래핑하여 추가 기능을 구현한다.
- 장단점:
- 장점: 동적 기능 확장, 여러 조합의 기능 조합 가능.
- 단점: 많은 래퍼로 인한 복잡도 증가 가능.
- 구현 예시 (Python):
```python
class Component:
def operation(self) -> str:
raise NotImplementedError
class ConcreteComponent(Component):
def operation(self) -> str:
return "ConcreteComponent"
class Decorator(Component):
def __init__(self, component: Component):
self._component = component
def operation(self) -> str:
return self._component.operation()
class ConcreteDecorator(Decorator):
def operation(self) -> str:
return f"Decorated({self._component.operation()})"
component = ConcreteComponent()
decorated = ConcreteDecorator(component)
print(decorated.operation())
```
---
### 8) Facade (Structural)
- 의도(Intent): 복잡한 시스템에 단순하고 일관된 인터페이스를 제공하여 사용 편의성과 결합도를 낮춘다.
- 문제 상황: 서브시스템이 다수의 인터페이스로 구성되어 있어 외부에서의 사용이 어렵고 결합도가 높은 경우.
- 구조(참여자): Facade, Subsystem, Client
- 해결 방법: Facade가 서브시스템의 복잡한 인터페이스를 감싸고, 간단한 인터페이스를 제공한다.
- 장단점:
- 장점: 사용 편의성 증대, 시스템의 의존성 감소.
- 단점: 숨겨진 서브시스템의 기능 접근이 필요할 때 한계가 있을 수 있음.
- 구현 예시 (Python):
```python
class SubsystemA:
def operation_a(self): return "A"
class SubsystemB:
def operation_b(self): return "B"
class Facade:
def __init__(self):
self._a = SubsystemA()
self._b = SubsystemB()
def simple_operation(self):
return f"Facade: [{self._a.operation_a()}] + [{self._b.operation_b()}]"
facade = Facade()
print(facade.simple_operation())
```
---
### 9) Observer (Behavioral)
- 의도(Intent): 객체의 상태 변화에 따라 의존하는 다른 객체들에게 자동으로 알림을 보내고 업데이트를 수행한다.
- 문제 상황: 한 객체의 상태 변경에 따라 다른 다수 객체가 동기화되어야 하는 경우.
- 구조(참여자): Subject, Observer, ConcreteSubject, ConcreteObserver
- 해결 방법: Subject가 상태 변화 시 Observer들에게 알림(통지)을 보내고, Observer가 상태를 업데이트한다.
- 장단점:
- 장점: 느슨한 결합, 확장성 좋은 이벤트 기반 모델.
- 단점: 알림의 남발로 인한 성능 이슈 가능성, 순서 보장 필요 시 복잡도 증가.
- 구현 예시 (Python):
```python
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 o in self._observers:
o.update(self._state)
def set_state(self, state):
self._state = state
self.notify()
class Observer:
def update(self, state):
raise NotImplementedError
class ConcreteObserver(Observer):
def update(self, state):
print(f"Observer received state: {state}")
subject = Subject()
obs1 = ConcreteObserver()
subject.attach(obs1)
subject.set_state("ON")
```
---
### 10) Strategy (Behavioral)
- 의도(Intent): 알고리즘 군을 정의하고, 런타임에 해당 알고리즘을 바꿔서 사용하도록 한다.
- 문제 상황: 서로 다른 알고리즘을 동일한 인터페이스로 쉽게 교체하고 싶은 경우.
- 구조(참여자): Context, Strategy, ConcreteStrategy
- 해결 방법: Context가 Strategy 인터페이스를 통해 알고리즘을 사용하고, 필요에 따라 ConcreteStrategy를 교체한다.
- 장단점:
- 장점: 알고리즘 독립성, 런타임 교체 가능.
- 단점: 전략의 수가 증가하면 코드가 복잡해질 수 있음.
- 구현 예시 (Python):
```python
class Strategy:
def do_operation(self, a, b):
raise NotImplementedError
class AddStrategy(Strategy):
def do_operation(self, a, b):
return a + b
class MultiplyStrategy(Strategy):
def do_operation(self, a, b):
return a * b
class Context:
def __init__(self, strategy: Strategy):
self._strategy = strategy
def execute(self, a, b):
return self._strategy.do_operation(a, b)
ctx = Context(AddStrategy())
print(ctx.execute(3, 5))
ctx = Context(MultiplyStrategy())
print(ctx.execute(3, 5))
```
---
## 디자인 패턴의 적용 가이드라인
- 상황 파악: 문제 상황의 맥락과 요구사항을 명확히 정의한다.
- 적합한 패턴 선택: 문제의 성격(구성, 생성, 행위)에 맞는 패턴을 선택한다.
- 결합도 관리: 가능하면 구현 세부를 추상화하고, 패턴을 통해 느슨한 결합을 달성한다.
- 확장성과 유지보수: 향후 변경 가능성을 고려하여 패턴의 확장성을 평가한다.
- 문서화와 예제: 선택 이유, 의도, 참여자, 상호작용을 문서화하고 예제를 제공한다.
---
관련 문서: [[디자인 패턴 비교]], [[소프트웨어 아키텍처 패턴]], [[생성 패턴 요약]], [[구조 패턴 요약]], [[행위 패턴 요약]]