Split View: 디자인 패턴 2025 실전 가이드: GoF 23개 패턴을 현대 언어(TypeScript/Python/Go)로 재해석
디자인 패턴 2025 실전 가이드: GoF 23개 패턴을 현대 언어(TypeScript/Python/Go)로 재해석
목차
1. 왜 2025년에도 디자인 패턴이 중요한가
1994년 Gang of Four(GoF)가 발표한 23개 디자인 패턴은 30년이 지난 지금도 소프트웨어 엔지니어링의 공용어입니다. 하지만 세상은 변했습니다.
현대 개발 환경의 변화:
- OOP 중심에서 함수형 + 반응형 하이브리드로 전환
- 마이크로서비스, 서버리스, 이벤트 드리븐 아키텍처의 보편화
- TypeScript, Go, Rust 등 현대 언어의 특성이 패턴 구현 방식을 변화
- 프레임워크가 패턴을 내장하여 명시적 구현 빈도 감소
그럼에도 패턴을 알아야 하는 이유:
- 코드 리뷰 시 공통 언어 — "여기 Strategy 패턴 적용하면 어때?" 한마디로 통한다
- 프레임워크 내부 이해 — React, Spring, Express 모두 패턴의 조합
- 면접 필수 지식 — FAANG 면접에서 시스템 설계와 함께 자주 출제
- 아키텍처 판단력 — 언제 쓰고, 언제 쓰지 말아야 하는지 판단
패턴을 아는 것과 패턴을 남용하는 것은 다릅니다. 이 글에서는 언제 쓰고 언제 쓰지 말아야 하는지까지 다룹니다.
2. TOP 10 가장 많이 사용되는 패턴
현대 코드베이스에서의 사용 빈도 기준입니다:
| 순위 | 패턴 | 분류 | 사용 빈도 | 대표적 사용처 |
|---|---|---|---|---|
| 1 | Strategy | 행위 | 매우 높음 | 결제, 인증, 정렬 |
| 2 | Observer | 행위 | 매우 높음 | 이벤트 시스템, React |
| 3 | Factory Method | 생성 | 높음 | DI 컨테이너, ORM |
| 4 | Builder | 생성 | 높음 | 쿼리 빌더, 설정 |
| 5 | Decorator | 구조 | 높음 | 미들웨어, AOP |
| 6 | Adapter | 구조 | 높음 | API 래퍼, 레거시 통합 |
| 7 | Singleton | 생성 | 보통 | 설정, 로거, DB 연결 |
| 8 | Proxy | 구조 | 보통 | 캐싱, 로깅, 접근 제어 |
| 9 | Command | 행위 | 보통 | Undo/Redo, CQRS |
| 10 | State | 행위 | 보통 | FSM, UI 상태 관리 |
3. 생성 패턴 (Creational Patterns)
3.1 Singleton — 단일 인스턴스 보장
문제: 전역 상태(설정, 로거, DB 풀)가 여러 인스턴스로 생성되면 일관성 붕괴
해결: 클래스의 인스턴스를 하나로 제한하고 전역 접근점 제공
TypeScript — 현대적 구현:
class AppConfig {
private static instance: AppConfig;
private config: Map<string, string> = new Map();
private constructor() {
// 외부 생성 차단
}
static getInstance(): AppConfig {
if (!AppConfig.instance) {
AppConfig.instance = new AppConfig();
}
return AppConfig.instance;
}
get(key: string): string | undefined {
return this.config.get(key);
}
set(key: string, value: string): void {
this.config.set(key, value);
}
}
// 사용
const config = AppConfig.getInstance();
config.set('API_URL', 'https://api.example.com');
Python — 모듈 레벨 싱글턴 (Pythonic):
# config.py - Python에서는 모듈 자체가 싱글턴
class _AppConfig:
def __init__(self):
self._config: dict[str, str] = {}
def get(self, key: str) -> str | None:
return self._config.get(key)
def set(self, key: str, value: str) -> None:
self._config[key] = value
# 모듈 레벨 인스턴스 = 싱글턴
app_config = _AppConfig()
쓰지 말아야 할 때:
- 테스트가 어려워진다면 (의존성 주입으로 대체)
- 전역 상태가 필요 없는데 편의상 쓰는 경우
- 멀티스레드 환경에서 동기화를 고려하지 않은 경우
3.2 Factory Method — 객체 생성의 위임
문제: 객체 생성 로직이 클라이언트에 직접 결합되면 확장이 어렵다
해결: 객체 생성을 서브클래스 또는 팩토리 함수에 위임
// 알림 시스템 팩토리
interface Notification {
send(message: string): Promise<void>;
}
class EmailNotification implements Notification {
async send(message: string) {
console.log(`Email: ${message}`);
}
}
class SlackNotification implements Notification {
async send(message: string) {
console.log(`Slack: ${message}`);
}
}
class SMSNotification implements Notification {
async send(message: string) {
console.log(`SMS: ${message}`);
}
}
// 팩토리 함수 (현대적 접근)
type NotificationType = 'email' | 'slack' | 'sms';
function createNotification(type: NotificationType): Notification {
const notificationMap: Record<NotificationType, () => Notification> = {
email: () => new EmailNotification(),
slack: () => new SlackNotification(),
sms: () => new SMSNotification(),
};
const creator = notificationMap[type];
if (!creator) throw new Error(`Unknown notification type: ${type}`);
return creator();
}
// 사용
const notifier = createNotification('slack');
await notifier.send('배포 완료!');
from abc import ABC, abstractmethod
class Notification(ABC):
@abstractmethod
async def send(self, message: str) -> None: ...
class EmailNotification(Notification):
async def send(self, message: str) -> None:
print(f"Email: {message}")
class SlackNotification(Notification):
async def send(self, message: str) -> None:
print(f"Slack: {message}")
def create_notification(ntype: str) -> Notification:
factories = {
"email": EmailNotification,
"slack": SlackNotification,
}
cls = factories.get(ntype)
if not cls:
raise ValueError(f"Unknown type: {ntype}")
return cls()
3.3 Abstract Factory — 관련 객체 패밀리 생성
문제: 서로 관련된 객체 그룹(예: UI 테마)을 일관되게 생성해야 한다
// UI 테마 Abstract Factory
interface Button {
render(): string;
}
interface Input {
render(): string;
}
interface UIFactory {
createButton(): Button;
createInput(): Input;
}
class DarkButton implements Button {
render() { return '<button class="dark-btn">Click</button>'; }
}
class DarkInput implements Input {
render() { return '<input class="dark-input" />'; }
}
class DarkThemeFactory implements UIFactory {
createButton() { return new DarkButton(); }
createInput() { return new DarkInput(); }
}
class LightButton implements Button {
render() { return '<button class="light-btn">Click</button>'; }
}
class LightInput implements Input {
render() { return '<input class="light-input" />'; }
}
class LightThemeFactory implements UIFactory {
createButton() { return new LightButton(); }
createInput() { return new LightInput(); }
}
// 사용 — 팩토리만 바꾸면 전체 테마 변경
function buildUI(factory: UIFactory) {
const button = factory.createButton();
const input = factory.createInput();
return { button: button.render(), input: input.render() };
}
3.4 Builder — 복잡한 객체의 단계적 구성
문제: 생성자 매개변수가 10개 이상인 "텔레스코핑 생성자" 문제
// Fluent Builder 패턴
class QueryBuilder {
private table = '';
private conditions: string[] = [];
private orderByField = '';
private limitCount = 0;
from(table: string): this {
this.table = table;
return this;
}
where(condition: string): this {
this.conditions.push(condition);
return this;
}
orderBy(field: string): this {
this.orderByField = field;
return this;
}
limit(count: number): this {
this.limitCount = count;
return this;
}
build(): string {
let query = `SELECT * FROM ${this.table}`;
if (this.conditions.length > 0) {
query += ` WHERE ${this.conditions.join(' AND ')}`;
}
if (this.orderByField) {
query += ` ORDER BY ${this.orderByField}`;
}
if (this.limitCount > 0) {
query += ` LIMIT ${this.limitCount}`;
}
return query;
}
}
// 사용 — 가독성 극대화
const query = new QueryBuilder()
.from('users')
.where('age > 18')
.where('status = "active"')
.orderBy('created_at DESC')
.limit(10)
.build();
from dataclasses import dataclass, field
@dataclass
class HttpRequest:
method: str = "GET"
url: str = ""
headers: dict[str, str] = field(default_factory=dict)
body: str | None = None
timeout: int = 30
class Builder:
def __init__(self):
self._request = HttpRequest()
def method(self, m: str) -> "HttpRequest.Builder":
self._request.method = m
return self
def url(self, u: str) -> "HttpRequest.Builder":
self._request.url = u
return self
def header(self, k: str, v: str) -> "HttpRequest.Builder":
self._request.headers[k] = v
return self
def body(self, b: str) -> "HttpRequest.Builder":
self._request.body = b
return self
def build(self) -> "HttpRequest":
return self._request
# 사용
req = (HttpRequest.Builder()
.method("POST")
.url("https://api.example.com/users")
.header("Content-Type", "application/json")
.body('{"name": "Kim"}')
.build())
3.5 Prototype — 복제를 통한 생성
문제: 객체 생성 비용이 높거나 기존 객체를 기반으로 변형이 필요한 경우
interface Cloneable<T> {
clone(): T;
}
class GameCharacter implements Cloneable<GameCharacter> {
constructor(
public name: string,
public health: number,
public inventory: string[],
public position: { x: number; y: number }
) {}
clone(): GameCharacter {
return new GameCharacter(
this.name,
this.health,
[...this.inventory], // 깊은 복사
{ ...this.position } // 깊은 복사
);
}
}
// 프로토타입 레지스트리
const templates = {
warrior: new GameCharacter('Warrior', 100, ['sword', 'shield'], { x: 0, y: 0 }),
mage: new GameCharacter('Mage', 70, ['staff', 'potion'], { x: 0, y: 0 }),
};
const player = templates.warrior.clone();
player.name = 'Hero Kim';
4. 구조 패턴 (Structural Patterns)
4.1 Adapter — 호환되지 않는 인터페이스 연결
문제: 외부 API, 레거시 시스템의 인터페이스가 우리 코드와 맞지 않는 경우
// 기존 외부 라이브러리 (변경 불가)
class LegacyPaymentGateway {
processPaymentInCents(amountCents: number, curr: string): boolean {
console.log(`Processing ${amountCents} cents in ${curr}`);
return true;
}
}
// 우리가 원하는 인터페이스
interface PaymentProcessor {
pay(amount: number, currency: string): Promise<boolean>;
}
// Adapter
class PaymentAdapter implements PaymentProcessor {
constructor(private legacy: LegacyPaymentGateway) {}
async pay(amount: number, currency: string): Promise<boolean> {
const cents = Math.round(amount * 100);
return this.legacy.processPaymentInCents(cents, currency);
}
}
// 사용 — 클라이언트는 레거시를 모른다
const processor: PaymentProcessor = new PaymentAdapter(
new LegacyPaymentGateway()
);
await processor.pay(29.99, 'USD');
4.2 Decorator — 동적 기능 추가
문제: 기존 객체에 기능을 추가하고 싶지만 상속은 유연하지 않다
// 미들웨어 스타일 Decorator
interface HttpHandler {
handle(request: Request): Promise<Response>;
}
class BaseHandler implements HttpHandler {
async handle(req: Request): Promise<Response> {
return new Response('OK', { status: 200 });
}
}
// 로깅 데코레이터
class LoggingDecorator implements HttpHandler {
constructor(private inner: HttpHandler) {}
async handle(req: Request): Promise<Response> {
console.log(`[LOG] ${req.method} ${req.url}`);
const start = Date.now();
const response = await this.inner.handle(req);
console.log(`[LOG] Completed in ${Date.now() - start}ms`);
return response;
}
}
// 인증 데코레이터
class AuthDecorator implements HttpHandler {
constructor(private inner: HttpHandler) {}
async handle(req: Request): Promise<Response> {
const token = req.headers.get('Authorization');
if (!token) {
return new Response('Unauthorized', { status: 401 });
}
return this.inner.handle(req);
}
}
// 데코레이터 체이닝 — 순서 중요!
const handler = new LoggingDecorator(
new AuthDecorator(
new BaseHandler()
)
);
Python 데코레이터 (언어 내장 기능):
import functools
import time
def log_execution(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"[LOG] {func.__name__} took {elapsed:.3f}s")
return result
return wrapper
def retry(max_attempts: int = 3):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
print(f"Retry {attempt + 1}/{max_attempts}")
return wrapper
return decorator
@log_execution
@retry(max_attempts=3)
def fetch_data(url: str) -> dict:
# API 호출 로직
...
4.3 Proxy — 접근 제어와 추가 로직
문제: 객체에 대한 접근을 제어하거나 추가 동작(캐싱, 로깅, 지연 초기화)이 필요
// 캐싱 프록시
interface DataService {
fetchUser(id: string): Promise<User>;
}
class RealDataService implements DataService {
async fetchUser(id: string): Promise<User> {
// 실제 API 호출 (느림)
const response = await fetch(`/api/users/${id}`);
return response.json();
}
}
class CachingProxy implements DataService {
private cache = new Map<string, { data: User; expiry: number }>();
constructor(
private real: DataService,
private ttlMs: number = 60000
) {}
async fetchUser(id: string): Promise<User> {
const cached = this.cache.get(id);
if (cached && cached.expiry > Date.now()) {
console.log(`Cache HIT for user ${id}`);
return cached.data;
}
console.log(`Cache MISS for user ${id}`);
const data = await this.real.fetchUser(id);
this.cache.set(id, { data, expiry: Date.now() + this.ttlMs });
return data;
}
}
4.4 Facade — 복잡한 서브시스템의 단순 인터페이스
문제: 복잡한 서브시스템(라이브러리, API 조합)을 간단하게 사용하고 싶다
// 복잡한 서브시스템들
class VideoEncoder { encode(file: string) { /* ... */ } }
class AudioEncoder { encode(file: string) { /* ... */ } }
class Uploader { upload(file: string, dest: string) { /* ... */ } }
class Notifier { notify(userId: string, msg: string) { /* ... */ } }
// Facade — 단순한 인터페이스
class MediaPublisher {
private videoEncoder = new VideoEncoder();
private audioEncoder = new AudioEncoder();
private uploader = new Uploader();
private notifier = new Notifier();
async publish(videoFile: string, userId: string): Promise<string> {
const encoded = this.videoEncoder.encode(videoFile);
const audioProcessed = this.audioEncoder.encode(videoFile);
const url = this.uploader.upload(videoFile, 'cdn-bucket');
this.notifier.notify(userId, '영상 업로드 완료!');
return url;
}
}
// 클라이언트는 내부 복잡성을 모른다
const publisher = new MediaPublisher();
await publisher.publish('video.mp4', 'user-123');
4.5 Composite — 트리 구조의 균일 처리
문제: 트리 구조의 개별 객체와 복합 객체를 동일하게 다루어야 한다
// 파일 시스템 예시
interface FileSystemNode {
name: string;
getSize(): number;
print(indent?: string): void;
}
class File implements FileSystemNode {
constructor(public name: string, private size: number) {}
getSize(): number { return this.size; }
print(indent = ''): void {
console.log(`${indent}📄 ${this.name} (${this.size}B)`);
}
}
class Directory implements FileSystemNode {
private children: FileSystemNode[] = [];
constructor(public name: string) {}
add(node: FileSystemNode): void { this.children.push(node); }
getSize(): number {
return this.children.reduce((sum, child) => sum + child.getSize(), 0);
}
print(indent = ''): void {
console.log(`${indent}📁 ${this.name} (${this.getSize()}B)`);
this.children.forEach(child => child.print(indent + ' '));
}
}
// 사용
const root = new Directory('src');
const components = new Directory('components');
components.add(new File('Button.tsx', 1200));
components.add(new File('Modal.tsx', 2400));
root.add(components);
root.add(new File('index.ts', 500));
root.print();
// 📁 src (4100B)
// 📁 components (3600B)
// 📄 Button.tsx (1200B)
// 📄 Modal.tsx (2400B)
// 📄 index.ts (500B)
5. 행위 패턴 (Behavioral Patterns)
5.1 Strategy — 알고리즘의 교체 가능한 캡슐화
문제: 결제, 정렬, 인증 등에서 if-else 분기가 무한 증식
// 결제 전략
interface PaymentStrategy {
pay(amount: number): Promise<PaymentResult>;
validate(): boolean;
}
class CreditCardPayment implements PaymentStrategy {
constructor(private cardNumber: string) {}
validate() { return this.cardNumber.length === 16; }
async pay(amount: number) {
return { success: true, method: 'credit_card', amount };
}
}
class KakaoPayPayment implements PaymentStrategy {
constructor(private userId: string) {}
validate() { return this.userId.length > 0; }
async pay(amount: number) {
return { success: true, method: 'kakao_pay', amount };
}
}
class NaverPayPayment implements PaymentStrategy {
constructor(private token: string) {}
validate() { return this.token.length > 0; }
async pay(amount: number) {
return { success: true, method: 'naver_pay', amount };
}
}
// Context
class PaymentProcessor {
constructor(private strategy: PaymentStrategy) {}
setStrategy(strategy: PaymentStrategy) {
this.strategy = strategy;
}
async checkout(amount: number) {
if (!this.strategy.validate()) {
throw new Error('Invalid payment method');
}
return this.strategy.pay(amount);
}
}
// 런타임에 전략 교체
const processor = new PaymentProcessor(new CreditCardPayment('1234567890123456'));
await processor.checkout(50000);
processor.setStrategy(new KakaoPayPayment('user-kim'));
await processor.checkout(30000);
5.2 Observer — 상태 변화의 자동 전파
문제: 한 객체의 상태 변화를 여러 객체가 알아야 하지만, 강결합은 피하고 싶다
// 타입 안전한 이벤트 버스
type EventMap = {
'user:login': { userId: string; timestamp: Date };
'user:logout': { userId: string };
'order:created': { orderId: string; total: number };
'order:shipped': { orderId: string; trackingNumber: string };
};
class TypedEventBus {
private listeners = new Map<string, Set<Function>>();
on<K extends keyof EventMap>(
event: K,
callback: (data: EventMap[K]) => void
): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
// unsubscribe 함수 반환
return () => this.listeners.get(event)?.delete(callback);
}
emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
this.listeners.get(event)?.forEach(cb => cb(data));
}
}
// 사용
const bus = new TypedEventBus();
const unsubscribe = bus.on('order:created', (data) => {
console.log(`새 주문: ${data.orderId}, 금액: ${data.total}원`);
});
bus.on('order:created', (data) => {
// 재고 업데이트
console.log(`재고 업데이트 for 주문 ${data.orderId}`);
});
bus.emit('order:created', { orderId: 'ORD-001', total: 50000 });
// 구독 해제
unsubscribe();
5.3 Command — 요청의 객체화
문제: Undo/Redo, 매크로, 요청 큐잉이 필요한 경우
interface Command {
execute(): void;
undo(): void;
}
class TextEditor {
private content = '';
private history: Command[] = [];
private undone: Command[] = [];
getContent() { return this.content; }
executeCommand(command: Command) {
command.execute();
this.history.push(command);
this.undone = []; // redo 스택 초기화
}
undo() {
const command = this.history.pop();
if (command) {
command.undo();
this.undone.push(command);
}
}
redo() {
const command = this.undone.pop();
if (command) {
command.execute();
this.history.push(command);
}
}
}
class InsertTextCommand implements Command {
constructor(
private editor: { content: string },
private text: string,
private position: number
) {}
execute() {
const before = this.editor.content.slice(0, this.position);
const after = this.editor.content.slice(this.position);
this.editor.content = before + this.text + after;
}
undo() {
const before = this.editor.content.slice(0, this.position);
const after = this.editor.content.slice(this.position + this.text.length);
this.editor.content = before + after;
}
}
5.4 State — 상태에 따른 행동 변화 (FSM)
문제: 객체의 상태에 따라 동작이 달라지는데, 조건문이 복잡해진다
// 주문 상태 FSM
interface OrderState {
name: string;
confirm(order: Order): void;
ship(order: Order): void;
deliver(order: Order): void;
cancel(order: Order): void;
}
class PendingState implements OrderState {
name = 'PENDING';
confirm(order: Order) { order.setState(new ConfirmedState()); }
ship(_order: Order) { throw new Error('결제 전에는 배송 불가'); }
deliver(_order: Order) { throw new Error('결제 전에는 수령 불가'); }
cancel(order: Order) { order.setState(new CancelledState()); }
}
class ConfirmedState implements OrderState {
name = 'CONFIRMED';
confirm(_order: Order) { throw new Error('이미 결제 완료'); }
ship(order: Order) { order.setState(new ShippedState()); }
deliver(_order: Order) { throw new Error('배송 전에는 수령 불가'); }
cancel(order: Order) { order.setState(new CancelledState()); }
}
class ShippedState implements OrderState {
name = 'SHIPPED';
confirm(_order: Order) { throw new Error('이미 결제 완료'); }
ship(_order: Order) { throw new Error('이미 배송 중'); }
deliver(order: Order) { order.setState(new DeliveredState()); }
cancel(_order: Order) { throw new Error('배송 중에는 취소 불가'); }
}
class DeliveredState implements OrderState {
name = 'DELIVERED';
confirm() { throw new Error('완료된 주문'); }
ship() { throw new Error('완료된 주문'); }
deliver() { throw new Error('완료된 주문'); }
cancel() { throw new Error('완료된 주문'); }
}
class CancelledState implements OrderState {
name = 'CANCELLED';
confirm() { throw new Error('취소된 주문'); }
ship() { throw new Error('취소된 주문'); }
deliver() { throw new Error('취소된 주문'); }
cancel() { throw new Error('이미 취소됨'); }
}
class Order {
private state: OrderState = new PendingState();
setState(state: OrderState) { this.state = state; }
getStateName() { return this.state.name; }
confirm() { this.state.confirm(this); }
ship() { this.state.ship(this); }
deliver() { this.state.deliver(this); }
cancel() { this.state.cancel(this); }
}
5.5 Iterator — 컬렉션 순회의 추상화
// 커스텀 이터레이터 — 페이지네이션
class PaginatedIterator<T> {
private currentPage = 0;
private buffer: T[] = [];
private done = false;
constructor(
private fetchPage: (page: number) => Promise<T[]>,
private pageSize: number = 20
) {}
async *[Symbol.asyncIterator](): AsyncIterableIterator<T> {
while (!this.done) {
const items = await this.fetchPage(this.currentPage);
if (items.length < this.pageSize) {
this.done = true;
}
for (const item of items) {
yield item;
}
this.currentPage++;
}
}
}
// 사용
const userIterator = new PaginatedIterator<User>(
(page) => fetch(`/api/users?page=${page}`).then(r => r.json())
);
for await (const user of userIterator) {
console.log(user.name);
}
5.6 Template Method — 알고리즘의 뼈대 정의
abstract class DataProcessor {
// 템플릿 메서드 — 알고리즘 뼈대
async process(): Promise<void> {
const raw = await this.extract();
const transformed = this.transform(raw);
await this.validate(transformed);
await this.load(transformed);
}
abstract extract(): Promise<string[]>;
abstract transform(data: string[]): Record<string, unknown>[];
// 기본 구현 제공 (선택적 오버라이드)
async validate(data: Record<string, unknown>[]): Promise<void> {
if (data.length === 0) throw new Error('No data');
}
abstract load(data: Record<string, unknown>[]): Promise<void>;
}
class CSVProcessor extends DataProcessor {
async extract() { return ['name,age', 'Kim,30', 'Lee,25']; }
transform(data: string[]) {
const headers = data[0].split(',');
return data.slice(1).map(row => {
const values = row.split(',');
return Object.fromEntries(headers.map((h, i) => [h, values[i]]));
});
}
async load(data: Record<string, unknown>[]) {
console.log('DB 저장:', data);
}
}
5.7 Chain of Responsibility — 요청의 체인 처리
// Express 미들웨어 스타일
interface Middleware {
handle(req: Request, next: () => Promise<Response>): Promise<Response>;
}
class CorsMiddleware implements Middleware {
async handle(req: Request, next: () => Promise<Response>) {
const response = await next();
response.headers.set('Access-Control-Allow-Origin', '*');
return response;
}
}
class RateLimitMiddleware implements Middleware {
private requests = new Map<string, number[]>();
async handle(req: Request, next: () => Promise<Response>) {
const ip = req.headers.get('x-forwarded-for') || 'unknown';
const now = Date.now();
const windowMs = 60000;
const maxRequests = 100;
const timestamps = (this.requests.get(ip) || [])
.filter(t => t > now - windowMs);
if (timestamps.length >= maxRequests) {
return new Response('Too Many Requests', { status: 429 });
}
timestamps.push(now);
this.requests.set(ip, timestamps);
return next();
}
}
// 미들웨어 체인 빌더
class MiddlewareChain {
private middlewares: Middleware[] = [];
use(middleware: Middleware): this {
this.middlewares.push(middleware);
return this;
}
async execute(req: Request, finalHandler: () => Promise<Response>): Promise<Response> {
let index = 0;
const next = async (): Promise<Response> => {
if (index >= this.middlewares.length) {
return finalHandler();
}
const mw = this.middlewares[index++];
return mw.handle(req, next);
};
return next();
}
}
6. 현대적 대안 — 함수형 패턴
많은 GoF 패턴은 함수형 프로그래밍에서 더 간결하게 표현됩니다.
| GoF 패턴 | 함수형 대안 | 설명 |
|---|---|---|
| Strategy | 고차 함수 | 함수를 인자로 전달 |
| Observer | RxJS / Signals | 반응형 스트림 |
| Command | 클로저 + 함수 스택 | 함수 자체가 Command |
| Factory | 팩토리 함수 | 클래스 없이 함수로 생성 |
| Template Method | 함수 합성 | pipe / compose |
| Decorator | 함수 래핑 | 고차 함수로 래핑 |
| Singleton | 모듈 스코프 변수 | ES 모듈이 자체 싱글턴 |
// Strategy를 함수형으로
type SortStrategy<T> = (a: T, b: T) => number;
const byName: SortStrategy<User> = (a, b) => a.name.localeCompare(b.name);
const byAge: SortStrategy<User> = (a, b) => a.age - b.age;
const byCreatedAt: SortStrategy<User> = (a, b) =>
a.createdAt.getTime() - b.createdAt.getTime();
// 전략 교체 = 함수 인자 변경
const sorted = users.sort(byAge);
// Decorator를 함수형으로
const withLogging = <T extends (...args: any[]) => any>(fn: T): T => {
return ((...args: any[]) => {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
}) as T;
};
const add = (a: number, b: number) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(1, 2); // Calling add with [1, 2], Result: 3
의존성 주입 (DI) — 팩토리/싱글턴의 현대적 대안:
// 수동 DI (프레임워크 없이)
interface Logger { log(msg: string): void; }
interface Database { query(sql: string): Promise<any[]>; }
class UserService {
constructor(
private logger: Logger,
private db: Database
) {}
async getUser(id: string) {
this.logger.log(`Fetching user ${id}`);
return this.db.query(`SELECT * FROM users WHERE id = '${id}'`);
}
}
// 테스트에서 Mock 주입
const mockLogger: Logger = { log: jest.fn() };
const mockDb: Database = { query: jest.fn().mockResolvedValue([{ id: '1', name: 'Kim' }]) };
const service = new UserService(mockLogger, mockDb);
7. 프레임워크 속 디자인 패턴
React의 패턴
| 패턴 | React에서의 활용 |
|---|---|
| Observer | useState, useEffect, 상태 구독 |
| Composite | 컴포넌트 트리 (JSX) |
| Strategy | 렌더 프롭, 커스텀 훅 |
| HOC (Decorator) | withAuth, withTheme |
| Factory | createElement |
| Proxy | React.lazy (지연 로딩) |
Express/Koa의 패턴
| 패턴 | Express에서의 활용 |
|---|---|
| Chain of Responsibility | 미들웨어 체인 |
| Decorator | app.use() 미들웨어 래핑 |
| Strategy | 라우트 핸들러 |
| Adapter | body-parser, cors 등 |
Spring의 패턴
| 패턴 | Spring에서의 활용 |
|---|---|
| Factory | ApplicationContext (Bean Factory) |
| Proxy | AOP, @Transactional |
| Template Method | JdbcTemplate, RestTemplate |
| Singleton | Bean 기본 스코프 |
| Observer | ApplicationEvent |
8. 안티패턴 — 이것만은 피하자
8.1 God Object (신 객체)
모든 로직을 하나의 클래스에 몰아넣는 안티패턴입니다.
// BAD - 신 객체
class AppManager {
// 사용자 관리
createUser() { /* ... */ }
deleteUser() { /* ... */ }
// 결제 관리
processPayment() { /* ... */ }
refund() { /* ... */ }
// 이메일 관리
sendEmail() { /* ... */ }
// 로깅
log() { /* ... */ }
// 설정
getConfig() { /* ... */ }
// ... 500줄 이상
}
// GOOD - 단일 책임 원칙
class UserService { /* 사용자만 */ }
class PaymentService { /* 결제만 */ }
class EmailService { /* 이메일만 */ }
8.2 Singleton 남용
모든 것을 Singleton으로 만드는 실수입니다.
// BAD - 테스트 불가능한 싱글턴 의존
class OrderService {
processOrder(orderId: string) {
const db = DatabaseConnection.getInstance(); // 하드코딩된 의존
const logger = Logger.getInstance(); // 테스트에서 교체 불가
// ...
}
}
// GOOD - 의존성 주입
class OrderService {
constructor(
private db: DatabaseConnection,
private logger: Logger
) {}
processOrder(orderId: string) {
// db와 logger는 외부에서 주입
}
}
8.3 과도한 추상화 (Over-Engineering)
코드가 3줄이면 되는데 5개 클래스와 3개 인터페이스를 만드는 경우입니다.
// BAD - 과도한 추상화 (YAGNI 위반)
interface IStringFormatter { format(s: string): string; }
class UpperCaseFormatter implements IStringFormatter {
format(s: string) { return s.toUpperCase(); }
}
class FormatterFactory {
static create(type: string): IStringFormatter { /* ... */ }
}
// GOOD - KISS 원칙
const toUpper = (s: string) => s.toUpperCase();
9. 면접 TOP 10 패턴 질문과 답변
Q1: Singleton의 문제점과 대안은?
답변: Singleton은 전역 상태로 인한 테스트 어려움, 숨겨진 의존성, 멀티스레드 동기화 문제가 있습니다. 대안으로 의존성 주입(DI)을 사용합니다. DI 컨테이너가 생명주기를 관리하면 Singleton의 이점은 유지하면서 테스트 가능성을 확보할 수 있습니다.
Q2: Strategy vs State 패턴의 차이?
답변: Strategy는 알고리즘 교체가 목적이고 클라이언트가 전략을 선택합니다. State는 상태에 따른 행동 변화가 목적이고 상태 객체가 스스로 전이를 결정합니다. Strategy는 독립적 알고리즘, State는 상태 머신에 적합합니다.
Q3: Observer 패턴에서 메모리 릭을 방지하려면?
답변: 구독 해제(unsubscribe)를 반드시 구현하고, WeakRef 또는 WeakMap을 사용하여 가비지 컬렉션을 허용합니다. React에서는 useEffect의 cleanup 함수에서 구독 해제합니다.
Q4: Adapter vs Facade 차이?
답변: Adapter는 기존 인터페이스를 다른 인터페이스로 변환하는 것이고, Facade는 복잡한 서브시스템에 단순한 인터페이스를 제공하는 것입니다. Adapter는 1:1 변환, Facade는 N:1 단순화입니다.
Q5: 디자인 패턴을 쓰지 말아야 할 때는?
답변: 문제가 단순할 때, 팀원이 이해하기 어려운 과도한 추상화가 될 때, YAGNI 원칙에 위배될 때입니다. 패턴은 실제 문제를 해결할 때만 도입해야 합니다.
Q6: Factory Method vs Abstract Factory?
답변: Factory Method는 하나의 객체 생성을 서브클래스에 위임하고, Abstract Factory는 관련 객체 패밀리 전체를 생성합니다. UI 테마처럼 여러 관련 객체가 함께 변경되어야 할 때 Abstract Factory를 씁니다.
Q7: Decorator와 Proxy의 차이?
답변: Decorator는 기능 추가가 목적(여러 겹 쌓기 가능), Proxy는 접근 제어가 목적(캐싱, 로깅, 지연 초기화). Decorator는 런타임에 동적으로 기능을 합성하고, Proxy는 보통 하나의 래퍼입니다.
Q8: Command 패턴을 CQRS에 어떻게 적용하나?
답변: CQRS에서 Command는 상태 변경 요청을 객체화합니다. CreateOrderCommand, UpdateUserCommand 등으로 쓰기 작업을 캡슐화하고, 이벤트 소싱과 결합하면 모든 변경 이력을 추적할 수 있습니다.
Q9: Template Method vs Strategy?
답변: Template Method는 상속 기반으로 알고리즘 뼈대는 고정하고 일부 단계만 재정의합니다. Strategy는 조합 기반으로 전체 알고리즘을 교체합니다. 현대 개발에서는 Strategy(조합)를 더 선호합니다.
Q10: 마이크로서비스에서 많이 쓰이는 패턴은?
답변: Circuit Breaker (장애 전파 방지), Saga (분산 트랜잭션), API Gateway (Facade), Service Discovery (Observer 변형), Sidecar (Proxy), Event Sourcing (Command) 등입니다. GoF 패턴이 아키텍처 수준으로 확장된 형태입니다.
10. 실전 퀴즈
Q1: 다음 코드에서 어떤 디자인 패턴이 사용되었나요?
const handler = new LoggingMiddleware(
new AuthMiddleware(
new RateLimitMiddleware(
new ApiHandler()
)
)
);
정답: Decorator 패턴 + Chain of Responsibility
각 미들웨어가 내부 핸들러를 감싸며(Decorator), 요청이 체인을 따라 전달됩니다(Chain of Responsibility). Express 미들웨어가 대표적인 예입니다.
Q2: React의 useState는 어떤 패턴에 해당하나요?
정답: Observer 패턴
useState가 반환하는 setter 함수를 호출하면, React가 해당 상태를 구독하는 컴포넌트를 자동으로 리렌더링합니다. 이것이 Observer 패턴의 핵심 — 상태 변화의 자동 전파입니다.
Q3: 왜 Singleton보다 의존성 주입(DI)을 선호하나요?
정답:
- 테스트 용이성 — DI는 Mock 객체를 쉽게 주입할 수 있다
- 명시적 의존성 — 생성자에서 의존성이 드러난다
- 유연한 생명주기 — 요청 범위, 세션 범위 등 다양한 스코프 가능
- SOLID 원칙 준수 — 의존성 역전 원칙(DIP) 지원
Q4: Builder 패턴을 써야 할 때와 쓰지 말아야 할 때는?
써야 할 때:
- 생성자 매개변수가 4개 이상일 때
- 선택적 매개변수가 많을 때
- 불변 객체를 단계적으로 구성할 때
- 같은 구성 과정으로 다른 표현을 만들 때
쓰지 말아야 할 때:
- 매개변수가 2-3개 이하일 때 (과도한 추상화)
- 단순 DTO나 데이터 클래스일 때
- 언어에 named parameters가 있을 때 (Python kwargs, Kotlin named args)
Q5: 다음 상황에 적합한 패턴을 선택하세요.
"EC2 인스턴스의 상태(running, stopped, terminated)에 따라 start, stop, terminate 메서드의 동작이 달라야 합니다."
정답: State 패턴
각 상태를 클래스로 표현하고, 현재 상태 객체가 동작을 결정합니다. if-else로 상태를 확인하는 대신, 상태 객체에 동작을 위임합니다.
11. 참고 자료
도서
- "Design Patterns: Elements of Reusable Object-Oriented Software" — GoF (1994)
- "Head First Design Patterns" — Freeman & Robson (2020 2판)
- "Patterns of Enterprise Application Architecture" — Martin Fowler
- "Refactoring to Patterns" — Joshua Kerievsky
- "A Philosophy of Software Design" — John Ousterhout
온라인 리소스
- Refactoring.Guru — Design Patterns — 시각적 패턴 카탈로그
- Source Making — Design Patterns
- TypeScript Design Patterns
- Python Design Patterns
- Go Design Patterns
강의 및 영상
아키텍처 패턴
Design Patterns 2025 Practical Guide: Reinterpreting GoF 23 Patterns in Modern Languages (TypeScript/Python/Go)
Table of Contents
1. Why Design Patterns Still Matter in 2025
The 23 design patterns published by the Gang of Four (GoF) in 1994 remain the lingua franca of software engineering 30 years later. But the world has changed.
How the modern development landscape has evolved:
- Shift from OOP-centric to functional + reactive hybrid approaches
- Widespread adoption of microservices, serverless, and event-driven architectures
- Modern languages like TypeScript, Go, and Rust have changed how patterns are implemented
- Frameworks embed patterns internally, reducing the need for explicit implementation
Why you still need to know patterns:
- Shared vocabulary in code reviews — "How about applying Strategy pattern here?" communicates instantly
- Understanding framework internals — React, Spring, Express are all combinations of patterns
- Essential interview knowledge — Frequently tested alongside system design at FAANG
- Architectural judgment — Knowing when to use and when NOT to use them
Knowing patterns and overusing patterns are different things. This guide covers when to use and when to avoid each pattern.
2. TOP 10 Most Used Patterns
Ranked by frequency in modern codebases:
| Rank | Pattern | Category | Frequency | Common Use Cases |
|---|---|---|---|---|
| 1 | Strategy | Behavioral | Very High | Payments, auth, sorting |
| 2 | Observer | Behavioral | Very High | Event systems, React |
| 3 | Factory Method | Creational | High | DI containers, ORMs |
| 4 | Builder | Creational | High | Query builders, configs |
| 5 | Decorator | Structural | High | Middleware, AOP |
| 6 | Adapter | Structural | High | API wrappers, legacy integration |
| 7 | Singleton | Creational | Medium | Config, logger, DB pools |
| 8 | Proxy | Structural | Medium | Caching, logging, access control |
| 9 | Command | Behavioral | Medium | Undo/Redo, CQRS |
| 10 | State | Behavioral | Medium | FSM, UI state management |
3. Creational Patterns
3.1 Singleton — Ensuring a Single Instance
Problem: Global state (config, logger, DB pool) loses consistency if multiple instances are created
Solution: Restrict a class to a single instance with a global access point
TypeScript — Modern Implementation:
class AppConfig {
private static instance: AppConfig;
private config: Map<string, string> = new Map();
private constructor() {
// Block external construction
}
static getInstance(): AppConfig {
if (!AppConfig.instance) {
AppConfig.instance = new AppConfig();
}
return AppConfig.instance;
}
get(key: string): string | undefined {
return this.config.get(key);
}
set(key: string, value: string): void {
this.config.set(key, value);
}
}
// Usage
const config = AppConfig.getInstance();
config.set('API_URL', 'https://api.example.com');
Python — Module-level Singleton (Pythonic):
# config.py - In Python, the module itself is a singleton
class _AppConfig:
def __init__(self):
self._config: dict[str, str] = {}
def get(self, key: str) -> str | None:
return self._config.get(key)
def set(self, key: str, value: str) -> None:
self._config[key] = value
# Module-level instance = singleton
app_config = _AppConfig()
When NOT to use:
- When it makes testing difficult (replace with dependency injection)
- When using it for convenience without needing global state
- In multithreaded environments without considering synchronization
3.2 Factory Method — Delegating Object Creation
Problem: Object creation logic tightly coupled to client code makes extension difficult
Solution: Delegate object creation to subclasses or factory functions
// Notification system factory
interface Notification {
send(message: string): Promise<void>;
}
class EmailNotification implements Notification {
async send(message: string) {
console.log(`Email: ${message}`);
}
}
class SlackNotification implements Notification {
async send(message: string) {
console.log(`Slack: ${message}`);
}
}
class SMSNotification implements Notification {
async send(message: string) {
console.log(`SMS: ${message}`);
}
}
// Factory function (modern approach)
type NotificationType = 'email' | 'slack' | 'sms';
function createNotification(type: NotificationType): Notification {
const notificationMap: Record<NotificationType, () => Notification> = {
email: () => new EmailNotification(),
slack: () => new SlackNotification(),
sms: () => new SMSNotification(),
};
const creator = notificationMap[type];
if (!creator) throw new Error(`Unknown notification type: ${type}`);
return creator();
}
// Usage
const notifier = createNotification('slack');
await notifier.send('Deployment complete!');
3.3 Abstract Factory — Creating Families of Related Objects
Problem: Groups of related objects (e.g., UI themes) need to be created consistently
// UI Theme Abstract Factory
interface Button { render(): string; }
interface Input { render(): string; }
interface UIFactory {
createButton(): Button;
createInput(): Input;
}
class DarkThemeFactory implements UIFactory {
createButton() { return { render: () => '<button class="dark-btn">Click</button>' }; }
createInput() { return { render: () => '<input class="dark-input" />' }; }
}
class LightThemeFactory implements UIFactory {
createButton() { return { render: () => '<button class="light-btn">Click</button>' }; }
createInput() { return { render: () => '<input class="light-input" />' }; }
}
// Usage — just swap the factory to change the entire theme
function buildUI(factory: UIFactory) {
const button = factory.createButton();
const input = factory.createInput();
return { button: button.render(), input: input.render() };
}
3.4 Builder — Step-by-Step Construction of Complex Objects
Problem: "Telescoping constructor" with 10+ parameters
// Fluent Builder pattern
class QueryBuilder {
private table = '';
private conditions: string[] = [];
private orderByField = '';
private limitCount = 0;
from(table: string): this {
this.table = table;
return this;
}
where(condition: string): this {
this.conditions.push(condition);
return this;
}
orderBy(field: string): this {
this.orderByField = field;
return this;
}
limit(count: number): this {
this.limitCount = count;
return this;
}
build(): string {
let query = `SELECT * FROM ${this.table}`;
if (this.conditions.length > 0) {
query += ` WHERE ${this.conditions.join(' AND ')}`;
}
if (this.orderByField) {
query += ` ORDER BY ${this.orderByField}`;
}
if (this.limitCount > 0) {
query += ` LIMIT ${this.limitCount}`;
}
return query;
}
}
// Usage — maximum readability
const query = new QueryBuilder()
.from('users')
.where('age > 18')
.where('status = "active"')
.orderBy('created_at DESC')
.limit(10)
.build();
3.5 Prototype — Creation Through Cloning
Problem: Object creation is expensive, or you need to create variations based on existing objects
interface Cloneable<T> {
clone(): T;
}
class GameCharacter implements Cloneable<GameCharacter> {
constructor(
public name: string,
public health: number,
public inventory: string[],
public position: { x: number; y: number }
) {}
clone(): GameCharacter {
return new GameCharacter(
this.name,
this.health,
[...this.inventory], // deep copy
{ ...this.position } // deep copy
);
}
}
// Prototype registry
const templates = {
warrior: new GameCharacter('Warrior', 100, ['sword', 'shield'], { x: 0, y: 0 }),
mage: new GameCharacter('Mage', 70, ['staff', 'potion'], { x: 0, y: 0 }),
};
const player = templates.warrior.clone();
player.name = 'Hero Kim';
4. Structural Patterns
4.1 Adapter — Bridging Incompatible Interfaces
Problem: External API or legacy system interfaces don't match your codebase
// Existing external library (cannot modify)
class LegacyPaymentGateway {
processPaymentInCents(amountCents: number, curr: string): boolean {
console.log(`Processing ${amountCents} cents in ${curr}`);
return true;
}
}
// Our desired interface
interface PaymentProcessor {
pay(amount: number, currency: string): Promise<boolean>;
}
// Adapter
class PaymentAdapter implements PaymentProcessor {
constructor(private legacy: LegacyPaymentGateway) {}
async pay(amount: number, currency: string): Promise<boolean> {
const cents = Math.round(amount * 100);
return this.legacy.processPaymentInCents(cents, currency);
}
}
// Usage — client is unaware of the legacy system
const processor: PaymentProcessor = new PaymentAdapter(
new LegacyPaymentGateway()
);
await processor.pay(29.99, 'USD');
4.2 Decorator — Dynamic Feature Addition
Problem: You want to add features to existing objects but inheritance is inflexible
// Middleware-style Decorator
interface HttpHandler {
handle(request: Request): Promise<Response>;
}
class BaseHandler implements HttpHandler {
async handle(req: Request): Promise<Response> {
return new Response('OK', { status: 200 });
}
}
class LoggingDecorator implements HttpHandler {
constructor(private inner: HttpHandler) {}
async handle(req: Request): Promise<Response> {
console.log(`[LOG] ${req.method} ${req.url}`);
const start = Date.now();
const response = await this.inner.handle(req);
console.log(`[LOG] Completed in ${Date.now() - start}ms`);
return response;
}
}
class AuthDecorator implements HttpHandler {
constructor(private inner: HttpHandler) {}
async handle(req: Request): Promise<Response> {
const token = req.headers.get('Authorization');
if (!token) return new Response('Unauthorized', { status: 401 });
return this.inner.handle(req);
}
}
// Decorator chaining — order matters!
const handler = new LoggingDecorator(
new AuthDecorator(
new BaseHandler()
)
);
Python decorators (language built-in):
import functools
import time
def log_execution(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"[LOG] {func.__name__} took {elapsed:.3f}s")
return result
return wrapper
def retry(max_attempts: int = 3):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
print(f"Retry {attempt + 1}/{max_attempts}")
return wrapper
return decorator
@log_execution
@retry(max_attempts=3)
def fetch_data(url: str) -> dict:
...
4.3 Proxy — Access Control and Additional Logic
Problem: You need to control access to an object or add behavior (caching, logging, lazy init)
// Caching Proxy
interface DataService {
fetchUser(id: string): Promise<User>;
}
class RealDataService implements DataService {
async fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
}
class CachingProxy implements DataService {
private cache = new Map<string, { data: User; expiry: number }>();
constructor(
private real: DataService,
private ttlMs: number = 60000
) {}
async fetchUser(id: string): Promise<User> {
const cached = this.cache.get(id);
if (cached && cached.expiry > Date.now()) {
console.log(`Cache HIT for user ${id}`);
return cached.data;
}
console.log(`Cache MISS for user ${id}`);
const data = await this.real.fetchUser(id);
this.cache.set(id, { data, expiry: Date.now() + this.ttlMs });
return data;
}
}
4.4 Facade — Simple Interface for Complex Subsystems
// Complex subsystems
class VideoEncoder { encode(file: string) { /* ... */ } }
class AudioEncoder { encode(file: string) { /* ... */ } }
class Uploader { upload(file: string, dest: string) { /* ... */ } }
class Notifier { notify(userId: string, msg: string) { /* ... */ } }
// Facade — simple interface
class MediaPublisher {
private videoEncoder = new VideoEncoder();
private audioEncoder = new AudioEncoder();
private uploader = new Uploader();
private notifier = new Notifier();
async publish(videoFile: string, userId: string): Promise<string> {
this.videoEncoder.encode(videoFile);
this.audioEncoder.encode(videoFile);
const url = this.uploader.upload(videoFile, 'cdn-bucket');
this.notifier.notify(userId, 'Video upload complete!');
return url;
}
}
4.5 Composite — Uniform Treatment of Tree Structures
interface FileSystemNode {
name: string;
getSize(): number;
print(indent?: string): void;
}
class File implements FileSystemNode {
constructor(public name: string, private size: number) {}
getSize(): number { return this.size; }
print(indent = ''): void {
console.log(`${indent}FILE ${this.name} (${this.size}B)`);
}
}
class Directory implements FileSystemNode {
private children: FileSystemNode[] = [];
constructor(public name: string) {}
add(node: FileSystemNode): void { this.children.push(node); }
getSize(): number {
return this.children.reduce((sum, child) => sum + child.getSize(), 0);
}
print(indent = ''): void {
console.log(`${indent}DIR ${this.name} (${this.getSize()}B)`);
this.children.forEach(child => child.print(indent + ' '));
}
}
5. Behavioral Patterns
5.1 Strategy — Interchangeable Algorithm Encapsulation
Problem: Infinite if-else branching for payments, sorting, authentication
// Payment strategies
interface PaymentStrategy {
pay(amount: number): Promise<PaymentResult>;
validate(): boolean;
}
class CreditCardPayment implements PaymentStrategy {
constructor(private cardNumber: string) {}
validate() { return this.cardNumber.length === 16; }
async pay(amount: number) {
return { success: true, method: 'credit_card', amount };
}
}
class PayPalPayment implements PaymentStrategy {
constructor(private email: string) {}
validate() { return this.email.includes('@'); }
async pay(amount: number) {
return { success: true, method: 'paypal', amount };
}
}
class StripePayment implements PaymentStrategy {
constructor(private token: string) {}
validate() { return this.token.length > 0; }
async pay(amount: number) {
return { success: true, method: 'stripe', amount };
}
}
// Context
class PaymentProcessor {
constructor(private strategy: PaymentStrategy) {}
setStrategy(strategy: PaymentStrategy) {
this.strategy = strategy;
}
async checkout(amount: number) {
if (!this.strategy.validate()) {
throw new Error('Invalid payment method');
}
return this.strategy.pay(amount);
}
}
5.2 Observer — Automatic State Change Propagation
Problem: Multiple objects need to react to state changes without tight coupling
// Type-safe event bus
type EventMap = {
'user:login': { userId: string; timestamp: Date };
'user:logout': { userId: string };
'order:created': { orderId: string; total: number };
'order:shipped': { orderId: string; trackingNumber: string };
};
class TypedEventBus {
private listeners = new Map<string, Set<Function>>();
on<K extends keyof EventMap>(
event: K,
callback: (data: EventMap[K]) => void
): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
return () => this.listeners.get(event)?.delete(callback);
}
emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
this.listeners.get(event)?.forEach(cb => cb(data));
}
}
// Usage
const bus = new TypedEventBus();
const unsubscribe = bus.on('order:created', (data) => {
console.log(`New order: ${data.orderId}, amount: ${data.total}`);
});
bus.emit('order:created', { orderId: 'ORD-001', total: 50000 });
unsubscribe(); // Clean up
5.3 Command — Objectifying Requests
Problem: You need Undo/Redo, macros, or request queuing
interface Command {
execute(): void;
undo(): void;
}
class TextEditor {
content = '';
private history: Command[] = [];
private undone: Command[] = [];
executeCommand(command: Command) {
command.execute();
this.history.push(command);
this.undone = [];
}
undo() {
const command = this.history.pop();
if (command) {
command.undo();
this.undone.push(command);
}
}
redo() {
const command = this.undone.pop();
if (command) {
command.execute();
this.history.push(command);
}
}
}
class InsertTextCommand implements Command {
constructor(
private editor: TextEditor,
private text: string,
private position: number
) {}
execute() {
const before = this.editor.content.slice(0, this.position);
const after = this.editor.content.slice(this.position);
this.editor.content = before + this.text + after;
}
undo() {
const before = this.editor.content.slice(0, this.position);
const after = this.editor.content.slice(this.position + this.text.length);
this.editor.content = before + after;
}
}
5.4 State — Behavior Changes Based on State (FSM)
// Order state FSM
interface OrderState {
name: string;
confirm(order: Order): void;
ship(order: Order): void;
deliver(order: Order): void;
cancel(order: Order): void;
}
class PendingState implements OrderState {
name = 'PENDING';
confirm(order: Order) { order.setState(new ConfirmedState()); }
ship() { throw new Error('Cannot ship before payment'); }
deliver() { throw new Error('Cannot deliver before shipping'); }
cancel(order: Order) { order.setState(new CancelledState()); }
}
class ConfirmedState implements OrderState {
name = 'CONFIRMED';
confirm() { throw new Error('Already confirmed'); }
ship(order: Order) { order.setState(new ShippedState()); }
deliver() { throw new Error('Cannot deliver before shipping'); }
cancel(order: Order) { order.setState(new CancelledState()); }
}
class ShippedState implements OrderState {
name = 'SHIPPED';
confirm() { throw new Error('Already confirmed'); }
ship() { throw new Error('Already shipping'); }
deliver(order: Order) { order.setState(new DeliveredState()); }
cancel() { throw new Error('Cannot cancel while shipping'); }
}
class DeliveredState implements OrderState {
name = 'DELIVERED';
confirm() { throw new Error('Order completed'); }
ship() { throw new Error('Order completed'); }
deliver() { throw new Error('Order completed'); }
cancel() { throw new Error('Order completed'); }
}
class CancelledState implements OrderState {
name = 'CANCELLED';
confirm() { throw new Error('Order cancelled'); }
ship() { throw new Error('Order cancelled'); }
deliver() { throw new Error('Order cancelled'); }
cancel() { throw new Error('Already cancelled'); }
}
class Order {
private state: OrderState = new PendingState();
setState(state: OrderState) { this.state = state; }
getStateName() { return this.state.name; }
confirm() { this.state.confirm(this); }
ship() { this.state.ship(this); }
deliver() { this.state.deliver(this); }
cancel() { this.state.cancel(this); }
}
5.5 Chain of Responsibility — Chain Processing of Requests
// Express middleware-style
interface Middleware {
handle(req: Request, next: () => Promise<Response>): Promise<Response>;
}
class CorsMiddleware implements Middleware {
async handle(req: Request, next: () => Promise<Response>) {
const response = await next();
response.headers.set('Access-Control-Allow-Origin', '*');
return response;
}
}
class RateLimitMiddleware implements Middleware {
private requests = new Map<string, number[]>();
async handle(req: Request, next: () => Promise<Response>) {
const ip = req.headers.get('x-forwarded-for') || 'unknown';
const now = Date.now();
const timestamps = (this.requests.get(ip) || []).filter(t => t > now - 60000);
if (timestamps.length >= 100) {
return new Response('Too Many Requests', { status: 429 });
}
timestamps.push(now);
this.requests.set(ip, timestamps);
return next();
}
}
class MiddlewareChain {
private middlewares: Middleware[] = [];
use(middleware: Middleware): this {
this.middlewares.push(middleware);
return this;
}
async execute(req: Request, finalHandler: () => Promise<Response>): Promise<Response> {
let index = 0;
const next = async (): Promise<Response> => {
if (index >= this.middlewares.length) return finalHandler();
return this.middlewares[index++].handle(req, next);
};
return next();
}
}
5.6 Template Method — Defining Algorithm Skeletons
abstract class DataProcessor {
// Template method — algorithm skeleton
async process(): Promise<void> {
const raw = await this.extract();
const transformed = this.transform(raw);
await this.validate(transformed);
await this.load(transformed);
}
abstract extract(): Promise<string[]>;
abstract transform(data: string[]): Record<string, unknown>[];
async validate(data: Record<string, unknown>[]): Promise<void> {
if (data.length === 0) throw new Error('No data');
}
abstract load(data: Record<string, unknown>[]): Promise<void>;
}
class CSVProcessor extends DataProcessor {
async extract() { return ['name,age', 'Kim,30', 'Lee,25']; }
transform(data: string[]) {
const headers = data[0].split(',');
return data.slice(1).map(row => {
const values = row.split(',');
return Object.fromEntries(headers.map((h, i) => [h, values[i]]));
});
}
async load(data: Record<string, unknown>[]) {
console.log('Saving to DB:', data);
}
}
6. Modern Alternatives — Functional Patterns
Many GoF patterns can be expressed more concisely with functional programming.
| GoF Pattern | Functional Alternative | Description |
|---|---|---|
| Strategy | Higher-order functions | Pass functions as arguments |
| Observer | RxJS / Signals | Reactive streams |
| Command | Closures + function stacks | The function itself IS the command |
| Factory | Factory functions | Create without classes |
| Template Method | Function composition | pipe / compose |
| Decorator | Function wrapping | Higher-order function wrapping |
| Singleton | Module-scoped variables | ES modules are singletons |
// Strategy as functional
type SortStrategy<T> = (a: T, b: T) => number;
const byName: SortStrategy<User> = (a, b) => a.name.localeCompare(b.name);
const byAge: SortStrategy<User> = (a, b) => a.age - b.age;
// Swapping strategy = changing function argument
const sorted = users.sort(byAge);
// Decorator as functional
const withLogging = <T extends (...args: any[]) => any>(fn: T): T => {
return ((...args: any[]) => {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
}) as T;
};
const add = (a: number, b: number) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(1, 2);
Dependency Injection — Modern alternative to Factory/Singleton:
// Manual DI (no framework)
interface Logger { log(msg: string): void; }
interface Database { query(sql: string): Promise<any[]>; }
class UserService {
constructor(
private logger: Logger,
private db: Database
) {}
async getUser(id: string) {
this.logger.log(`Fetching user ${id}`);
return this.db.query(`SELECT * FROM users WHERE id = '${id}'`);
}
}
// Inject mocks in tests
const mockLogger: Logger = { log: jest.fn() };
const mockDb: Database = { query: jest.fn().mockResolvedValue([]) };
const service = new UserService(mockLogger, mockDb);
7. Design Patterns Inside Frameworks
React Patterns
| Pattern | Usage in React |
|---|---|
| Observer | useState, useEffect, state subscriptions |
| Composite | Component tree (JSX) |
| Strategy | Render props, custom hooks |
| HOC (Decorator) | withAuth, withTheme |
| Factory | createElement |
| Proxy | React.lazy (lazy loading) |
Express/Koa Patterns
| Pattern | Usage in Express |
|---|---|
| Chain of Responsibility | Middleware chain |
| Decorator | app.use() middleware wrapping |
| Strategy | Route handlers |
| Adapter | body-parser, cors, etc. |
Spring Patterns
| Pattern | Usage in Spring |
|---|---|
| Factory | ApplicationContext (Bean Factory) |
| Proxy | AOP, @Transactional |
| Template Method | JdbcTemplate, RestTemplate |
| Singleton | Default bean scope |
| Observer | ApplicationEvent |
8. Anti-Patterns — What to Avoid
8.1 God Object
Cramming all logic into a single class.
// BAD - God Object
class AppManager {
createUser() { /* ... */ }
deleteUser() { /* ... */ }
processPayment() { /* ... */ }
sendEmail() { /* ... */ }
log() { /* ... */ }
getConfig() { /* ... */ }
// ... 500+ lines
}
// GOOD - Single Responsibility Principle
class UserService { /* users only */ }
class PaymentService { /* payments only */ }
class EmailService { /* emails only */ }
8.2 Singleton Abuse
// BAD - untestable singleton dependency
class OrderService {
processOrder(orderId: string) {
const db = DatabaseConnection.getInstance(); // hardcoded dependency
const logger = Logger.getInstance(); // cannot replace in tests
}
}
// GOOD - dependency injection
class OrderService {
constructor(
private db: DatabaseConnection,
private logger: Logger
) {}
}
8.3 Over-Engineering
// BAD - excessive abstraction (YAGNI violation)
interface IStringFormatter { format(s: string): string; }
class UpperCaseFormatter implements IStringFormatter {
format(s: string) { return s.toUpperCase(); }
}
// GOOD - KISS principle
const toUpper = (s: string) => s.toUpperCase();
9. Interview TOP 10 Pattern Questions
Q1: What are the problems with Singleton and its alternatives?
Answer: Singleton causes testing difficulties due to global state, hidden dependencies, and multithreading synchronization issues. The alternative is Dependency Injection (DI). When a DI container manages lifecycle, you maintain Singleton benefits while ensuring testability.
Q2: Strategy vs State pattern differences?
Answer: Strategy is for algorithm replacement where the client selects the strategy. State is for behavior changes based on state where the state object determines transitions. Strategy suits independent algorithms; State suits state machines.
Q3: How to prevent memory leaks in Observer pattern?
Answer: Always implement unsubscribe, use WeakRef or WeakMap to allow garbage collection. In React, unsubscribe in useEffect cleanup functions.
Q4: Adapter vs Facade differences?
Answer: Adapter converts one interface to another (1:1 conversion). Facade provides a simple interface to a complex subsystem (N:1 simplification).
Q5: When should you NOT use design patterns?
Answer: When the problem is simple, when it creates excessive abstraction that teammates cannot understand, and when it violates YAGNI. Patterns should only be introduced when solving real problems.
Q6: Factory Method vs Abstract Factory?
Answer: Factory Method delegates single object creation to subclasses. Abstract Factory creates an entire family of related objects. Use Abstract Factory when multiple related objects need to change together, like UI themes.
Q7: Decorator vs Proxy differences?
Answer: Decorator is for adding features (can stack multiple layers). Proxy is for access control (caching, logging, lazy init). Decorators dynamically compose at runtime; Proxy is typically a single wrapper.
Q8: How to apply Command pattern in CQRS?
Answer: In CQRS, Command objectifies state change requests. Write operations are encapsulated as CreateOrderCommand, UpdateUserCommand, etc. Combined with event sourcing, it enables tracking all change history.
Q9: Template Method vs Strategy?
Answer: Template Method is inheritance-based — fixes the algorithm skeleton, overrides specific steps. Strategy is composition-based — replaces the entire algorithm. Modern development prefers Strategy (composition).
Q10: What patterns are commonly used in microservices?
Answer: Circuit Breaker (fault isolation), Saga (distributed transactions), API Gateway (Facade), Service Discovery (Observer variant), Sidecar (Proxy), Event Sourcing (Command). These are GoF patterns extended to architecture level.
10. Practice Quiz
Q1: What design patterns are used in this code?
const handler = new LoggingMiddleware(
new AuthMiddleware(
new RateLimitMiddleware(
new ApiHandler()
)
)
);
Answer: Decorator + Chain of Responsibility
Each middleware wraps the inner handler (Decorator), and requests propagate through the chain (Chain of Responsibility). Express middleware is a classic example.
Q2: What pattern does React useState represent?
Answer: Observer Pattern
When you call the setter function returned by useState, React automatically re-renders components subscribed to that state. This is the core of the Observer pattern: automatic propagation of state changes.
Q3: Why prefer DI over Singleton?
Answer:
- Testability — DI allows easy mock injection
- Explicit dependencies — Dependencies are visible in constructor
- Flexible lifecycle — Request-scoped, session-scoped, etc.
- SOLID compliance — Supports Dependency Inversion Principle
Q4: When should you use / not use Builder?
Use when:
- Constructor has 4+ parameters
- Many optional parameters
- Building immutable objects step-by-step
- Same construction process for different representations
Do NOT use when:
- 2-3 parameters or fewer (over-engineering)
- Simple DTOs or data classes
- Language has named parameters (Python kwargs, Kotlin named args)
Q5: Pick the right pattern for this scenario.
"EC2 instance behavior of start, stop, terminate must change based on state (running, stopped, terminated)."
Answer: State Pattern
Represent each state as a class, and let the current state object determine behavior. Instead of checking state with if-else, delegate behavior to state objects.
11. References
Books
- "Design Patterns: Elements of Reusable Object-Oriented Software" — GoF (1994)
- "Head First Design Patterns" — Freeman & Robson (2020, 2nd ed.)
- "Patterns of Enterprise Application Architecture" — Martin Fowler
- "Refactoring to Patterns" — Joshua Kerievsky
- "A Philosophy of Software Design" — John Ousterhout
Online Resources
- Refactoring.Guru — Design Patterns — Visual pattern catalog
- Source Making — Design Patterns
- TypeScript Design Patterns
- Python Design Patterns
- Go Design Patterns