- Published on
디자인 패턴 2025 실전 가이드: GoF 23개 패턴을 현대 언어(TypeScript/Python/Go)로 재해석
- Authors

- Name
- Youngju Kim
- @fjvbn20031
목차
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