디자인 패턴
디자인 패턴¶
주제: Programming 레슨: 7 of 16 선수지식: OOP 원칙(캡슐화, 상속, 다형성) 이해, 기본 프로그래밍 경험 목표: 일반적인 디자인 패턴(Gang of Four 및 그 이상)을 마스터하고, 언제 적용할지 인식하며, 트레이드오프 이해
소개¶
디자인 패턴(Design Pattern)은 소프트웨어 설계에서 일반적으로 발생하는 문제에 대한 재사용 가능한 솔루션입니다. 개발자를 위한 공유 어휘를 제공하고 수십 년간 다듬어진 모범 사례를 캡슐화합니다. 이 레슨에서는 고전적인 Gang of Four(GoF) 패턴, 언제 사용할지, 그리고 언제 과도할 수 있는지를 다룹니다.
디자인 패턴이란?¶
디자인 패턴 = 이름 + 문제 + 솔루션 + 결과
- 이름: 공유 어휘 (예: "여기서 Factory 패턴을 사용하세요")
- 문제: 패턴을 적용할 때
- 솔루션: 문제를 해결하는 일반적인 설계
- 결과: 트레이드오프, 비용, 이점
역사: Gang of Four (1994)¶
Gamma, Helm, Johnson, Vlissides의 책 Design Patterns: Elements of Reusable Object-Oriented Software는 23개의 패턴을 소개했으며, 세 가지 범주로 구성됩니다:
- 생성(Creational): 객체 생성 메커니즘
- 구조(Structural): 클래스와 객체의 조합
- 행동(Behavioral): 객체 간 통신
패턴을 사용해야 할 때¶
좋은 이유: - 문제가 패턴의 의도와 일치 - 패턴이 설계를 단순화 - 팀이 패턴을 이해
나쁜 이유 (안티패턴): - "더 많은 디자인 패턴이 필요하다" (패턴 열병) - 이력서 채우기 위한 패턴 사용 - 단순한 문제를 과도하게 엔지니어링 - 맞지 않는 곳에 패턴 강제
기억하세요: 패턴은 도구이지 목표가 아닙니다. 단순한 코드 > 불필요하게 패턴화된 코드.
생성 패턴(Creational Patterns)¶
이 패턴들은 객체 생성을 다루며, 인스턴스화 프로세스를 추상화합니다.
싱글턴(Singleton)¶
의도: 클래스가 하나의 인스턴스만 갖도록 보장하고 전역 접근 지점 제공.
사용 시기: - 공유 리소스 관리 (데이터베이스 연결 풀, 로거) - 시스템 전체 작업 조정
Java (스레드 안전):
public class DatabaseConnection {
private static volatile DatabaseConnection instance;
private Connection connection;
// Private constructor prevents external instantiation
private DatabaseConnection() {
// Initialize connection
connection = DriverManager.getConnection("jdbc:...");
}
// Double-checked locking for thread safety
public static DatabaseConnection getInstance() {
if (instance == null) {
synchronized (DatabaseConnection.class) {
if (instance == null) {
instance = new DatabaseConnection();
}
}
}
return instance;
}
public Connection getConnection() {
return connection;
}
}
// Usage
DatabaseConnection db = DatabaseConnection.getInstance();
Python:
class Logger:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.initialized = False
return cls._instance
def __init__(self):
if not self.initialized:
self.log_file = open('app.log', 'a')
self.initialized = True
def log(self, message):
self.log_file.write(f"{message}\n")
self.log_file.flush()
# Usage
logger1 = Logger()
logger2 = Logger()
assert logger1 is logger2 # Same instance
C++ (Meyer's Singleton):
class Config {
public:
static Config& getInstance() {
static Config instance; // Thread-safe in C++11+
return instance;
}
// Delete copy constructor and assignment
Config(const Config&) = delete;
Config& operator=(const Config&) = delete;
void setValue(const std::string& key, const std::string& value) {
settings[key] = value;
}
std::string getValue(const std::string& key) {
return settings[key];
}
private:
Config() {} // Private constructor
std::map<std::string, std::string> settings;
};
// Usage
Config::getInstance().setValue("timeout", "30");
왜 종종 안티패턴인가: - 전역 상태가 테스트를 어렵게 만듦 - 강한 결합 - 단일 책임 원칙 위반 - 모킹이나 대체가 어려움
더 나은 대안: 의존성 주입(Dependency Injection)
// Instead of Singleton
public class UserService {
private final Database database;
public UserService(Database database) { // Injected dependency
this.database = database;
}
}
// Easy to test with mock
Database mockDb = new MockDatabase();
UserService service = new UserService(mockDb);
팩토리 메서드(Factory Method)¶
의도: 객체 생성을 위한 인터페이스를 정의하되, 서브클래스가 인스턴스화할 클래스를 결정하도록 함.
JavaScript:
// Product interface
class Button {
render() {
throw new Error('Must implement render()');
}
}
class WindowsButton extends Button {
render() {
return '<button class="windows">Click me</button>';
}
}
class MacButton extends Button {
render() {
return '<button class="mac">Click me</button>';
}
}
// Creator
class Dialog {
render() {
const button = this.createButton(); // Factory method
return `<div>${button.render()}</div>`;
}
createButton() {
throw new Error('Must implement createButton()');
}
}
class WindowsDialog extends Dialog {
createButton() {
return new WindowsButton();
}
}
class MacDialog extends Dialog {
createButton() {
return new MacButton();
}
}
// Usage
const os = detectOS();
let dialog;
if (os === 'Windows') {
dialog = new WindowsDialog();
} else if (os === 'Mac') {
dialog = new MacDialog();
}
console.log(dialog.render());
Python (Simple Factory vs Factory Method):
from abc import ABC, abstractmethod
# Simple Factory (not GoF pattern, but useful)
class ShapeFactory:
@staticmethod
def create_shape(shape_type):
if shape_type == 'circle':
return Circle()
elif shape_type == 'rectangle':
return Rectangle()
else:
raise ValueError(f"Unknown shape: {shape_type}")
# Factory Method (GoF pattern)
class Document(ABC):
@abstractmethod
def create_page(self):
pass
def print_document(self):
page = self.create_page() # Factory method
print(f"Printing: {page.render()}")
class PDFDocument(Document):
def create_page(self):
return PDFPage()
class WordDocument(Document):
def create_page(self):
return WordPage()
class Page(ABC):
@abstractmethod
def render(self):
pass
class PDFPage(Page):
def render(self):
return "PDF page content"
class WordPage(Page):
def render(self):
return "Word page content"
빌더(Builder)¶
의도: 복잡한 객체의 생성을 표현과 분리하여 단계별 생성 허용.
Java (플루언트 인터페이스):
public class HttpRequest {
private String method;
private String url;
private Map<String, String> headers;
private String body;
private HttpRequest() {
headers = new HashMap<>();
}
public static class Builder {
private HttpRequest request;
public Builder() {
request = new HttpRequest();
}
public Builder method(String method) {
request.method = method;
return this;
}
public Builder url(String url) {
request.url = url;
return this;
}
public Builder header(String key, String value) {
request.headers.put(key, value);
return this;
}
public Builder body(String body) {
request.body = body;
return this;
}
public HttpRequest build() {
// Validation
if (request.method == null || request.url == null) {
throw new IllegalStateException("method and url are required");
}
return request;
}
}
// Getters...
}
// Usage
HttpRequest request = new HttpRequest.Builder()
.method("POST")
.url("https://api.example.com/users")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body("{\"name\": \"Alice\"}")
.build();
C++ (SQL 쿼리 빌더):
class SQLQuery {
private:
std::string table;
std::vector<std::string> columns;
std::string whereClause;
std::string orderBy;
int limitValue = -1;
public:
class Builder {
private:
SQLQuery query;
public:
Builder& select(const std::vector<std::string>& cols) {
query.columns = cols;
return *this;
}
Builder& from(const std::string& tbl) {
query.table = tbl;
return *this;
}
Builder& where(const std::string& condition) {
query.whereClause = condition;
return *this;
}
Builder& orderBy(const std::string& column) {
query.orderBy = column;
return *this;
}
Builder& limit(int n) {
query.limitValue = n;
return *this;
}
SQLQuery build() {
if (query.table.empty()) {
throw std::runtime_error("table is required");
}
return query;
}
};
std::string toSQL() const {
std::string sql = "SELECT ";
if (columns.empty()) {
sql += "*";
} else {
for (size_t i = 0; i < columns.size(); i++) {
if (i > 0) sql += ", ";
sql += columns[i];
}
}
sql += " FROM " + table;
if (!whereClause.empty()) {
sql += " WHERE " + whereClause;
}
if (!orderBy.empty()) {
sql += " ORDER BY " + orderBy;
}
if (limitValue > 0) {
sql += " LIMIT " + std::to_string(limitValue);
}
return sql;
}
};
// Usage
SQLQuery query = SQLQuery::Builder()
.select({"id", "name", "email"})
.from("users")
.where("age > 18")
.orderBy("name")
.limit(10)
.build();
std::cout << query.toSQL() << "\n";
프로토타입(Prototype)¶
의도: 기존 객체(프로토타입)를 복사하여 새 객체 생성.
JavaScript (프로토타입 상속):
const carPrototype = {
drive() {
console.log(`Driving a ${this.make} ${this.model}`);
},
clone() {
return Object.create(Object.getPrototypeOf(this),
Object.getOwnPropertyDescriptors(this));
}
};
function createCar(make, model, year) {
const car = Object.create(carPrototype);
car.make = make;
car.model = model;
car.year = year;
return car;
}
const tesla = createCar('Tesla', 'Model 3', 2024);
const teslaClone = tesla.clone();
teslaClone.year = 2025;
tesla.drive(); // Driving a Tesla Model 3
teslaClone.drive(); // Driving a Tesla Model 3
구조 패턴(Structural Patterns)¶
이 패턴들은 객체 조합을 다루며, 유연성을 유지하면서 더 큰 구조를 형성합니다.
어댑터(Adapter)¶
의도: 클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환.
Python:
# Legacy system
class OldPaymentProcessor:
def process_payment(self, amount):
print(f"Old system processing ${amount}")
# New interface
class PaymentProcessor:
def pay(self, amount):
raise NotImplementedError
# Adapter
class PaymentAdapter(PaymentProcessor):
def __init__(self, old_processor):
self.old_processor = old_processor
def pay(self, amount):
# Adapt new interface to old interface
self.old_processor.process_payment(amount)
# Client code expects new interface
def checkout(payment_processor: PaymentProcessor, amount):
payment_processor.pay(amount)
# Usage
old_system = OldPaymentProcessor()
adapter = PaymentAdapter(old_system)
checkout(adapter, 99.99) # Works with new interface
Java (객체 어댑터):
// Target interface
interface MediaPlayer {
void play(String filename);
}
// Adaptee (incompatible interface)
class AdvancedMediaPlayer {
public void playVlc(String filename) {
System.out.println("Playing VLC file: " + filename);
}
public void playMp4(String filename) {
System.out.println("Playing MP4 file: " + filename);
}
}
// Adapter
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedPlayer;
public MediaAdapter() {
advancedPlayer = new AdvancedMediaPlayer();
}
@Override
public void play(String filename) {
if (filename.endsWith(".vlc")) {
advancedPlayer.playVlc(filename);
} else if (filename.endsWith(".mp4")) {
advancedPlayer.playMp4(filename);
}
}
}
// Usage
MediaPlayer player = new MediaAdapter();
player.play("video.mp4");
데코레이터(Decorator)¶
의도: 객체에 동적으로 추가 책임을 부여하여 서브클래싱의 유연한 대안 제공.
Java (I/O 스트림):
// Classic example: Java I/O
InputStream fileStream = new FileInputStream("data.txt");
InputStream bufferedStream = new BufferedInputStream(fileStream);
InputStream compressedStream = new GZIPInputStream(bufferedStream);
// Each decorator adds functionality
Python (함수 데코레이터):
import time
from functools import wraps
# Timing decorator
def timing(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f}s")
return result
return wrapper
# Logging decorator
def logging(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
# Stack decorators
@timing
@logging
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
fibonacci(10)
C++ (커피숍 예제):
// Component
class Coffee {
public:
virtual ~Coffee() = default;
virtual std::string getDescription() const = 0;
virtual double cost() const = 0;
};
// Concrete Component
class SimpleCoffee : public Coffee {
public:
std::string getDescription() const override {
return "Simple coffee";
}
double cost() const override {
return 2.0;
}
};
// Decorator
class CoffeeDecorator : public Coffee {
protected:
Coffee* coffee;
public:
CoffeeDecorator(Coffee* c) : coffee(c) {}
virtual ~CoffeeDecorator() { delete coffee; }
};
// Concrete Decorators
class Milk : public CoffeeDecorator {
public:
Milk(Coffee* c) : CoffeeDecorator(c) {}
std::string getDescription() const override {
return coffee->getDescription() + ", milk";
}
double cost() const override {
return coffee->cost() + 0.5;
}
};
class Sugar : public CoffeeDecorator {
public:
Sugar(Coffee* c) : CoffeeDecorator(c) {}
std::string getDescription() const override {
return coffee->getDescription() + ", sugar";
}
double cost() const override {
return coffee->cost() + 0.2;
}
};
// Usage
Coffee* myCoffee = new SimpleCoffee();
myCoffee = new Milk(myCoffee);
myCoffee = new Sugar(myCoffee);
std::cout << myCoffee->getDescription() << " costs $" << myCoffee->cost() << "\n";
// Output: Simple coffee, milk, sugar costs $2.7
delete myCoffee;
퍼사드(Facade)¶
의도: 복잡한 하위 시스템에 통합되고 단순화된 인터페이스 제공.
JavaScript:
// Complex subsystem
class CPU {
freeze() { console.log('CPU: Freezing...'); }
jump(position) { console.log(`CPU: Jumping to ${position}`); }
execute() { console.log('CPU: Executing...'); }
}
class Memory {
load(position, data) {
console.log(`Memory: Loading ${data} at ${position}`);
}
}
class HardDrive {
read(sector, size) {
console.log(`HDD: Reading ${size} bytes from sector ${sector}`);
return 'boot data';
}
}
// Facade
class ComputerFacade {
constructor() {
this.cpu = new CPU();
this.memory = new Memory();
this.hdd = new HardDrive();
}
start() {
this.cpu.freeze();
const bootData = this.hdd.read(0, 1024);
this.memory.load(0, bootData);
this.cpu.jump(0);
this.cpu.execute();
}
}
// Client code (simple!)
const computer = new ComputerFacade();
computer.start(); // Hides complex subsystem
프록시(Proxy)¶
의도: 다른 객체에 대한 대리자 또는 자리 표시자를 제공하여 접근 제어.
유형: - 가상 프록시(Virtual Proxy): 지연 초기화 (필요할 때만 비싼 객체 생성) - 보호 프록시(Protection Proxy): 접근 제어 - 원격 프록시(Remote Proxy): 다른 주소 공간의 객체 표현 - 캐시 프록시(Cache Proxy): 결과 캐싱
Python (캐싱을 포함한 가상 프록시):
class Image:
def __init__(self, filename):
self.filename = filename
self._load()
def _load(self):
print(f"Loading image from {self.filename}")
# Expensive operation
time.sleep(2)
self.data = f"Image data from {self.filename}"
def display(self):
print(f"Displaying {self.data}")
# Proxy with lazy loading and caching
class ImageProxy:
def __init__(self, filename):
self.filename = filename
self._image = None # Not loaded yet
def display(self):
if self._image is None:
self._image = Image(self.filename) # Lazy load
self._image.display()
# Usage
image = ImageProxy("photo.jpg")
# Image not loaded yet
image.display() # Loads now (2 second delay)
image.display() # Uses cached image (instant)
Java (보호 프록시):
interface Document {
void display();
void edit(String content);
}
class RealDocument implements Document {
private String content;
public void display() {
System.out.println("Displaying: " + content);
}
public void edit(String newContent) {
content = newContent;
System.out.println("Document edited");
}
}
class ProtectedDocument implements Document {
private RealDocument document;
private String userRole;
public ProtectedDocument(String role) {
document = new RealDocument();
userRole = role;
}
public void display() {
document.display(); // Anyone can view
}
public void edit(String content) {
if (userRole.equals("ADMIN")) {
document.edit(content);
} else {
System.out.println("Access denied: insufficient permissions");
}
}
}
// Usage
Document doc = new ProtectedDocument("USER");
doc.display(); // OK
doc.edit("new text"); // Denied
컴포지트(Composite)¶
의도: 객체들을 트리 구조로 조합하여 부분-전체 계층 구조를 표현하고, 개별 객체와 복합 객체를 동일하게 처리.
C++:
#include <vector>
#include <memory>
// Component
class Graphic {
public:
virtual ~Graphic() = default;
virtual void draw() const = 0;
virtual void add(std::shared_ptr<Graphic> g) {
throw std::runtime_error("Cannot add to leaf");
}
};
// Leaf
class Circle : public Graphic {
public:
void draw() const override {
std::cout << "Drawing circle\n";
}
};
class Rectangle : public Graphic {
public:
void draw() const override {
std::cout << "Drawing rectangle\n";
}
};
// Composite
class CompositeGraphic : public Graphic {
private:
std::vector<std::shared_ptr<Graphic>> children;
public:
void add(std::shared_ptr<Graphic> g) override {
children.push_back(g);
}
void draw() const override {
for (const auto& child : children) {
child->draw();
}
}
};
// Usage
auto circle1 = std::make_shared<Circle>();
auto circle2 = std::make_shared<Circle>();
auto rect = std::make_shared<Rectangle>();
auto group1 = std::make_shared<CompositeGraphic>();
group1->add(circle1);
group1->add(rect);
auto group2 = std::make_shared<CompositeGraphic>();
group2->add(circle2);
group2->add(group1); // Nested composite
group2->draw(); // Draws entire tree
행동 패턴(Behavioral Patterns)¶
이 패턴들은 객체 간 통신에 초점을 맞춥니다.
옵저버(Observer)¶
의도: 일대다 의존성을 정의하여 한 객체가 상태를 변경하면 모든 종속 객체에 알림.
JavaScript (이벤트 시스템):
class EventEmitter {
constructor() {
this.listeners = {};
}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
off(event, callback) {
if (this.listeners[event]) {
this.listeners[event] = this.listeners[event]
.filter(cb => cb !== callback);
}
}
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
}
// Subject
class Stock extends EventEmitter {
constructor(symbol) {
super();
this.symbol = symbol;
this.price = 0;
}
setPrice(price) {
this.price = price;
this.emit('priceChange', { symbol: this.symbol, price });
}
}
// Observers
class Investor {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} notified: ${data.symbol} is now $${data.price}`);
}
}
// Usage
const apple = new Stock('AAPL');
const investor1 = new Investor('Alice');
const investor2 = new Investor('Bob');
apple.on('priceChange', data => investor1.update(data));
apple.on('priceChange', data => investor2.update(data));
apple.setPrice(150); // Both investors notified
apple.setPrice(155); // Both investors notified
Python (속성 옵저버):
class Observable:
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def detach(self, observer):
self._observers.remove(observer)
def notify(self, data):
for observer in self._observers:
observer.update(data)
class Subject(Observable):
def __init__(self):
super().__init__()
self._state = None
@property
def state(self):
return self._state
@state.setter
def state(self, value):
self._state = value
self.notify(value) # Notify observers on change
class ConcreteObserver:
def __init__(self, name):
self.name = name
def update(self, data):
print(f"{self.name} received update: {data}")
# Usage
subject = Subject()
obs1 = ConcreteObserver("Observer 1")
obs2 = ConcreteObserver("Observer 2")
subject.attach(obs1)
subject.attach(obs2)
subject.state = "new state" # Both observers notified
전략(Strategy)¶
의도: 알고리즘 패밀리를 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만듦.
Java:
// Strategy interface
interface PaymentStrategy {
void pay(int amount);
}
// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println("Paid $" + amount + " with credit card " + cardNumber);
}
}
class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(int amount) {
System.out.println("Paid $" + amount + " via PayPal account " + email);
}
}
class CryptoPayment implements PaymentStrategy {
private String walletAddress;
public CryptoPayment(String wallet) {
this.walletAddress = wallet;
}
@Override
public void pay(int amount) {
System.out.println("Paid $" + amount + " to crypto wallet " + walletAddress);
}
}
// Context
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
// Usage
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("1234-5678"));
cart.checkout(100);
cart.setPaymentStrategy(new PayPalPayment("user@example.com"));
cart.checkout(50);
cart.setPaymentStrategy(new CryptoPayment("0x1234..."));
cart.checkout(200);
커맨드(Command)¶
의도: 요청을 객체로 캡슐화하여 매개변수화, 큐잉, 로깅, 실행 취소 작업 허용.
C++ (실행 취소/재실행이 있는 텍스트 편집기):
#include <stack>
#include <memory>
// Command interface
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0;
};
// Receiver
class TextEditor {
private:
std::string text;
public:
void insertText(const std::string& str, size_t pos) {
text.insert(pos, str);
}
void deleteText(size_t pos, size_t length) {
text.erase(pos, length);
}
std::string getText() const {
return text;
}
};
// Concrete Commands
class InsertCommand : public Command {
private:
TextEditor* editor;
std::string textToInsert;
size_t position;
public:
InsertCommand(TextEditor* ed, const std::string& text, size_t pos)
: editor(ed), textToInsert(text), position(pos) {}
void execute() override {
editor->insertText(textToInsert, position);
}
void undo() override {
editor->deleteText(position, textToInsert.length());
}
};
class DeleteCommand : public Command {
private:
TextEditor* editor;
std::string deletedText;
size_t position;
size_t length;
public:
DeleteCommand(TextEditor* ed, size_t pos, size_t len)
: editor(ed), position(pos), length(len) {
deletedText = editor->getText().substr(pos, len);
}
void execute() override {
editor->deleteText(position, length);
}
void undo() override {
editor->insertText(deletedText, position);
}
};
// Invoker
class CommandManager {
private:
std::stack<std::shared_ptr<Command>> undoStack;
std::stack<std::shared_ptr<Command>> redoStack;
public:
void executeCommand(std::shared_ptr<Command> cmd) {
cmd->execute();
undoStack.push(cmd);
// Clear redo stack on new command
while (!redoStack.empty()) {
redoStack.pop();
}
}
void undo() {
if (!undoStack.empty()) {
auto cmd = undoStack.top();
undoStack.pop();
cmd->undo();
redoStack.push(cmd);
}
}
void redo() {
if (!redoStack.empty()) {
auto cmd = redoStack.top();
redoStack.pop();
cmd->execute();
undoStack.push(cmd);
}
}
};
// Usage
TextEditor editor;
CommandManager manager;
auto insert1 = std::make_shared<InsertCommand>(&editor, "Hello", 0);
manager.executeCommand(insert1);
std::cout << editor.getText() << "\n"; // Hello
auto insert2 = std::make_shared<InsertCommand>(&editor, " World", 5);
manager.executeCommand(insert2);
std::cout << editor.getText() << "\n"; // Hello World
manager.undo();
std::cout << editor.getText() << "\n"; // Hello
manager.redo();
std::cout << editor.getText() << "\n"; // Hello World
반복자(Iterator)¶
의도: 기본 표현을 노출하지 않고 컬렉션의 요소에 순차적으로 접근하는 방법 제공.
Python (사용자 정의 반복자):
class LinkedListNode:
def __init__(self, value):
self.value = value
self.next = None
class LinkedList:
def __init__(self):
self.head = None
def append(self, value):
if not self.head:
self.head = LinkedListNode(value)
else:
current = self.head
while current.next:
current = current.next
current.next = LinkedListNode(value)
def __iter__(self):
return LinkedListIterator(self.head)
class LinkedListIterator:
def __init__(self, head):
self.current = head
def __iter__(self):
return self
def __next__(self):
if self.current is None:
raise StopIteration
value = self.current.value
self.current = self.current.next
return value
# Usage
linked_list = LinkedList()
linked_list.append(1)
linked_list.append(2)
linked_list.append(3)
for value in linked_list:
print(value) # 1, 2, 3
상태(State)¶
의도: 객체의 내부 상태가 변경될 때 객체의 동작을 변경할 수 있도록 함.
JavaScript (TCP 연결):
// State interface
class ConnectionState {
open(connection) { throw new Error('Not implemented'); }
close(connection) { throw new Error('Not implemented'); }
read(connection) { throw new Error('Not implemented'); }
write(connection, data) { throw new Error('Not implemented'); }
}
// Concrete States
class ClosedState extends ConnectionState {
open(connection) {
console.log('Opening connection...');
connection.setState(new OpenState());
}
close(connection) {
console.log('Already closed');
}
read(connection) {
console.log('Cannot read: connection closed');
}
write(connection, data) {
console.log('Cannot write: connection closed');
}
}
class OpenState extends ConnectionState {
open(connection) {
console.log('Already open');
}
close(connection) {
console.log('Closing connection...');
connection.setState(new ClosedState());
}
read(connection) {
console.log('Reading data...');
return 'data';
}
write(connection, data) {
console.log(`Writing: ${data}`);
}
}
// Context
class TCPConnection {
constructor() {
this.state = new ClosedState();
}
setState(state) {
this.state = state;
}
open() { this.state.open(this); }
close() { this.state.close(this); }
read() { return this.state.read(this); }
write(data) { this.state.write(this, data); }
}
// Usage
const conn = new TCPConnection();
conn.read(); // Cannot read: connection closed
conn.open(); // Opening connection...
conn.write('Hello'); // Writing: Hello
conn.close(); // Closing connection...
템플릿 메서드(Template Method)¶
의도: 메서드에서 알고리즘의 골격을 정의하고 일부 단계를 서브클래스에 위임.
Python:
from abc import ABC, abstractmethod
class DataParser(ABC):
# Template method
def parse(self, filename):
data = self.read_file(filename)
parsed = self.parse_data(data)
validated = self.validate(parsed)
self.use_data(validated)
def read_file(self, filename):
with open(filename, 'r') as f:
return f.read()
@abstractmethod
def parse_data(self, data):
pass
def validate(self, data):
# Default validation (can be overridden)
return data
@abstractmethod
def use_data(self, data):
pass
class CSVParser(DataParser):
def parse_data(self, data):
lines = data.strip().split('\n')
return [line.split(',') for line in lines]
def use_data(self, data):
print(f"Processing {len(data)} CSV rows")
class JSONParser(DataParser):
def parse_data(self, data):
import json
return json.loads(data)
def validate(self, data):
if not isinstance(data, list):
raise ValueError("Expected list")
return data
def use_data(self, data):
print(f"Processing {len(data)} JSON objects")
# Usage
csv_parser = CSVParser()
csv_parser.parse('data.csv')
json_parser = JSONParser()
json_parser.parse('data.json')
안티패턴(Anti-Patterns)¶
피해야 할 일반적인 안티패턴:
- 신 객체(God Object): 모든 것을 하는 하나의 클래스
- 스파게티 코드(Spaghetti Code): 얽힌 제어 흐름
- 황금 망치(Golden Hammer): 모든 문제에 하나의 패턴 사용
- 용암 흐름(Lava Flow): "혹시 몰라서" 유지하는 죽은 코드
- 복사-붙여넣기 프로그래밍: 추상화 대신 중복
현대적 관점¶
많은 GoF 패턴들은 현대 언어에서 덜 필요합니다:
- 전략/커맨드: 일급 함수가 클래스의 필요성 제거
- 반복자: 대부분의 언어에 내장 (Python 제너레이터, Java Streams)
- 싱글턴: 의존성 주입 선호
- 방문자: 함수형 언어의 패턴 매칭
예제 (Python에서 클래스 없는 전략):
# Old (class-based)
class Strategy:
def execute(self):
pass
# New (function-based)
def strategy_a(data):
return data * 2
def strategy_b(data):
return data ** 2
strategies = {'a': strategy_a, 'b': strategy_b}
result = strategies['a'](10)
요약¶
| 패턴 | 범주 | 목적 | 사용 시기 |
|---|---|---|---|
| 싱글턴(Singleton) | 생성 | 하나의 인스턴스 | 공유 리소스 (대신 DI 사용) |
| 팩토리 메서드(Factory Method) | 생성 | 인스턴스화 지연 | 객체 생성이 다양함 |
| 빌더(Builder) | 생성 | 복잡한 생성 | 많은 선택적 매개변수 |
| 어댑터(Adapter) | 구조 | 인터페이스 변환 | 비호환 코드 통합 |
| 데코레이터(Decorator) | 구조 | 동적으로 동작 추가 | 서브클래싱의 유연한 대안 |
| 퍼사드(Facade) | 구조 | 인터페이스 단순화 | 복잡한 하위 시스템 숨기기 |
| 프록시(Proxy) | 구조 | 접근 제어 | 지연 로딩, 접근 제어 |
| 옵저버(Observer) | 행동 | 일대다 알림 | 이벤트 시스템 |
| 전략(Strategy) | 행동 | 교환 가능한 알고리즘 | 런타임에 알고리즘 변경 |
| 커맨드(Command) | 행동 | 요청 캡슐화 | 실행 취소/재실행, 큐잉 |
연습 문제¶
연습 문제 1: 패턴 식별¶
각 시나리오에서 어떤 디자인 패턴이 사용되는지 식별하세요:
- Java의
InputStream계층 구조 (BufferedInputStream, GZIPInputStream) - Spring Framework의 의존성 주입
- GUI 이벤트 리스너 (button.onClick)
- Python의
@property데코레이터 - SQL 쿼리 빌더
연습 문제 2: 옵저버 구현¶
온도가 변경될 때 여러 디스플레이에 알리는 기상 관측소를 구현하세요. 포함할 것:
- WeatherStation (subject)
- 여러 디스플레이 유형 (현재 조건, 통계, 예보)
- 동적으로 디스플레이 추가/제거 기능
연습 문제 3: 전략으로 리팩토링¶
이 코드를 전략 패턴을 사용하도록 리팩토링하세요:
class Order {
public double calculateShipping(String method, double weight) {
if (method.equals("standard")) {
return weight * 0.5;
} else if (method.equals("express")) {
return weight * 1.5;
} else if (method.equals("overnight")) {
return weight * 3.0;
}
return 0;
}
}
연습 문제 4: 커맨드 시스템 구축¶
커맨드 패턴을 사용하여 스마트 홈 자동화 시스템을 만드세요: - 명령 지원: 조명 켜기/끄기, 온도 조절, 문 잠금/잠금 해제 - 매크로 명령 구현 (예: "외출" = 조명 끄기, 문 잠금, 온도 조절) - 모든 명령에 대한 실행 취소 지원
연습 문제 5: 패턴을 사용하지 말아야 할 때¶
각 시나리오에 대해 디자인 패턴 사용이 과도한 이유를 설명하세요:
- 파일을 읽고 내용을 출력하는 스크립트
- 더하기/빼기/곱하기/나누기가 있는 간단한 계산기
- 3개 화면이 있는 할 일 목록 앱
- 5개 설정이 있는 구성 파일