객체지향 설계의 핵심—생성, 구조, 행동 패턴을 이해하면 코드 유지보수성과 확장성이 크게 좋아집니다. 이 가이드는 TypeScript 실전 예제와 리팩터링 팁으로 실무 적용을 돕습니다.
December 20, 2025 (4mo ago) — last updated March 11, 2026 (1mo ago)
OOP 디자인 패턴 실용 가이드 — TypeScript 예제
생성·구조·행동 패턴을 TypeScript 실전 코드로 배우고 설계 품질과 유지보수성을 개선하세요. 핵심 예제와 리팩터링 팁 포함.
← Back to blog
OOP 디자인 패턴 실용 가이드
디자인 패턴 OOP를 마스터하세요. 생성(Creational), 구조(Structural), 행동(Behavioral) 패턴을 명확하고 실용적인 코드 예제로 배우며 실력을 끌어올리세요.
소개
객체지향 프로그래밍의 디자인 패턴은 반복되는 설계 문제를 해결하기 위한 검증된 재사용 가능한 청사진입니다. 이들은 그대로 복사해 붙여넣는 코드가 아니라, 클래스와 객체를 구조화해 코드의 유지보수성, 확장성, 테스트 용이성을 높이는 적응 가능한 템플릿입니다. 핵심 23가지 패턴은 여전히 실무에서 널리 참고됩니다1.
일반적인 패턴인 생성, 구조, 행동 패턴을 탄탄히 이해하면 팀원들과 설계 의도를 명확히 소통하고, 레거시 코드를 안전하게 리팩터링하며, 각 아키텍처 문제에 적합한 도구를 빠르게 선택할 수 있습니다. 초기 학술 연구는 패턴 추상화와 재사용이 설계 생산성에 긍정적 영향을 준다고 보고했습니다2.
객체지향 프로그래밍에서의 디자인 패턴이란
계획 없이 복잡한 것을 만들어본 적 있나요? 디자인 패턴 없이 코딩하면 확장과 유지보수가 어려운 뒤엉킨 코드베이스가 될 수 있습니다. 디자인 패턴은 마스터 셰프의 요리책과 같습니다: 일관되고 유지보수 가능한 결과를 내기 위해 응용하는 검증된 레시피입니다.

개발자들의 공용 언어
패턴의 가장 큰 장점 중 하나는 공통의 어휘입니다. “Factory”나 “Singleton”이라고 하면 숙련된 개발자들은 즉시 의도와 고수준 구조를 이해해 협업과 아키텍처 결정을 빠르게 진행할 수 있습니다.
“Design Patterns: Elements of Reusable Object-Oriented Software”는 23가지 기본 패턴을 정리한 고전으로, 오늘날에도 많은 설계 결정의 기준이 됩니다1.
디자인 패턴은 곧바로 코드로 변환할 수 있는 완성된 설계가 아니다. 이는 여러 상황에서 사용할 수 있는 문제 해결 방법의 설명 또는 템플릿이다.
다형성(polymorphism)과 상속(inheritance) 같은 핵심 OOP 원칙에 익숙해지면 패턴을 효과적으로 적용할 수 있습니다. 실용적인 복습이 필요하면 다형성 vs 상속을 참고하세요.
디자인 패턴의 세 가지 핵심 분류
Gang of Four는 패턴을 생성, 구조, 행동의 세 가지 범주로 정리했습니다. 이 범주들을 알면 문제에 맞는 접근법을 빠르게 선택할 수 있습니다.

OOP 디자인 패턴 분류 개요
| Pattern Category | Core Purpose | Common Examples |
|---|---|---|
| Creational | 객체 생성 추상화 및 관리 | Factory, Builder, Singleton, Prototype |
| Structural | 클래스와 객체를 유연하게 조합 | Adapter, Decorator, Facade, Composite |
| Behavioral | 객체 상호작용과 책임 관리 | Observer, Strategy, Command, Iterator |
생성 패턴 (Creational Patterns): 객체 생성의 제어
생성 패턴은 객체가 생성되는 방식을 제어해 클라이언트 코드가 구체 클래스에 강하게 결합되지 않도록 합니다. 생성 로직을 숨기고 유연성을 높이며 인스턴스 생성 관리를 가능하게 합니다.

싱글톤 패턴: 하나의 인스턴스 보장
Singleton은 클래스가 오직 하나의 인스턴스만 가지도록 보장하고 전역 접근점을 제공합니다. 데이터베이스 연결이나 로거처럼 공유 자원에 유용하지만, 전역 상태를 도입해 테스트와 의존성 관리를 복잡하게 만들 수 있습니다3.
예시: 데이터베이스 연결을 위한 TypeScript Singleton.
class DatabaseConnection {
private static instance: DatabaseConnection;
private constructor() {
// A private constructor prevents external 'new' calls
console.log("Connecting to the database...");
}
public static getInstance(): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection();
}
return DatabaseConnection.instance;
}
public query(sql: string): void {
console.log(`Executing query: ${sql}`);
}
}
// Usage
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
db1.query("SELECT * FROM users");
console.log(db1 === db2); // true
싱글톤의 트레이드오프: 진정한 전역 자원에만 싱글톤을 사용하고, 테스트 용이성과 모듈화를 위해서는 의존성 주입을 선호하세요3.
팩토리 메서드: 어떤 것을 생성할지 하위 클래스가 결정하게 하기
Factory Method는 객체를 생성하기 위한 인터페이스를 정의하고, 어떤 구체 제품을 인스턴스화할지는 하위 클래스가 결정하게 합니다. 이는 클라이언트 코드를 구체 클래스에서 분리해 시스템 확장을 더 쉽게 만듭니다.
예시: OS별 버튼을 렌더링하는 TypeScript 코드.
interface Button {
render(): void;
onClick(f: () => void): void;
}
class WindowsButton implements Button {
render() { console.log("Rendering a button in Windows style."); }
onClick(f: () => void) { console.log("Windows button click event."); f(); }
}
class MacButton implements Button {
render() { console.log("Rendering a button in macOS style."); }
onClick(f: () => void) { console.log("Mac button click event."); f(); }
}
abstract class Dialog {
abstract createButton(): Button;
render() {
const okButton = this.createButton();
okButton.render();
}
}
class WindowsDialog extends Dialog {
createButton(): Button { return new WindowsButton(); }
}
class MacDialog extends Dialog {
createButton(): Button { return new MacButton(); }
}
// Client
const os: string = "windows";
let dialog: Dialog;
if (os === "windows") dialog = new WindowsDialog(); else dialog = new MacDialog();
dialog.render();
Factory Method는 생성자가 구체 제품을 알지 못하게 하여 추가 플랫폼 지원을 간단하게 만듭니다.
구조 패턴 (Structural Patterns): 유연한 조립
구조 패턴은 객체들을 더 큰 시스템으로 조립하는 데 도움을 줍니다. 구성 요소 간의 관계를 단순화해 부분을 변경해도 전체 시스템이 깨지지 않도록 합니다.

어댑터 패턴: 호환되지 않는 인터페이스 연결하기
Adapter는 호환되지 않는 인터페이스를 래핑해 시스템에서 기대하는 형태로 맞춥니다. 이는 레거시 코드에 침습적인 변경을 피하게 해줍니다.
예시: ModernLogger를 기존의 ILogger 인터페이스에 맞추는 어댑터.
class ModernLogger {
public logInfo(message: string): void {
console.log(`[INFO]: ${message}`);
}
}
interface ILogger { log(message: string): void; }
class LoggerAdapter implements ILogger {
private modernLogger: ModernLogger;
constructor() { this.modernLogger = new ModernLogger(); }
public log(message: string): void { this.modernLogger.logInfo(message); }
}
const logger: ILogger = new LoggerAdapter();
logger.log("User logged in successfully.");
데코레이터 패턴: 기능을 동적으로 추가하기
Decorator는 객체를 런타임에 래핑해 책임을 추가합니다. 이는 서브클래싱보다 유연하고 단일 책임 원칙을 따릅니다.
예시: 선택적 애드온이 있는 구독(composition) 구성.
interface Subscription { getDescription(): string; getCost(): number; }
class BasicSubscription implements Subscription {
getDescription(): string { return "Basic Plan"; }
getCost(): number { return 10; }
}
abstract class SubscriptionDecorator implements Subscription {
protected subscription: Subscription;
constructor(subscription: Subscription) { this.subscription = subscription; }
abstract getDescription(): string;
abstract getCost(): number;
}
class PremiumSupportDecorator extends SubscriptionDecorator {
getDescription(): string { return `${this.subscription.getDescription()}, Premium Support`; }
getCost(): number { return this.subscription.getCost() + 5; }
}
class CloudStorageDecorator extends SubscriptionDecorator {
getDescription(): string { return `${this.subscription.getDescription()}, 1TB Cloud Storage`; }
getCost(): number { return this.subscription.getCost() + 7; }
}
let mySubscription: Subscription = new BasicSubscription();
mySubscription = new PremiumSupportDecorator(mySubscription);
mySubscription = new CloudStorageDecorator(mySubscription);
console.log(mySubscription.getDescription());
console.log(mySubscription.getCost());
이러한 구성적 접근은 기능을 모듈화하고 테스트하기 쉽게 유지합니다.
행동 패턴 (Behavioral Patterns): 상호작용과 책임 관리
행동 패턴은 객체 통신을 조직화해 시스템을 유연하고 유지보수하기 쉽게 만듭니다. 아래 두 가지는 실제로 자주 사용하는 핵심 패턴입니다.
옵저버 패턴: 관심 있는 당사자에게 알리기
Observer는 일대다 관계를 설정해 Subject가 상태를 변경하면 옵저버들이 자동으로 통지받게 합니다. 이벤트 기반 시스템에 이상적입니다.
예시: 간단한 알림 서비스.
interface Subject { attach(observer: Observer): void; detach(observer: Observer): void; notify(): void; }
interface Observer { update(subject: Subject): void; }
class NotificationService implements Subject {
public state: string = '';
private observers: Observer[] = [];
attach(observer: Observer): void { this.observers.push(observer); }
detach(observer: Observer): void { const i = this.observers.indexOf(observer); if (i !== -1) this.observers.splice(i, 1); }
notify(): void { for (const o of this.observers) o.update(this); }
public createNewPost(title: string): void { this.state = `New Post: ${title}`; console.log(`\nNotificationService: A new post was created.`); this.notify(); }
}
class EmailNotifier implements Observer { public update(subject: Subject): void { if (subject instanceof NotificationService) console.log(`EmailNotifier: Sending email about "${subject.state}"`); } }
class PushNotifier implements Observer { public update(subject: Subject): void { if (subject instanceof NotificationService) console.log(`PushNotifier: Sending push notification for "${subject.state}"`); } }
const notificationService = new NotificationService();
const emailer = new EmailNotifier();
const pusher = new PushNotifier();
notificationService.attach(emailer);
notificationService.attach(pusher);
notificationService.createNewPost("Understanding Observer Pattern");
notificationService.detach(pusher);
notificationService.createNewPost("Why Strategy is Awesome");
Observer는 느슨하게 결합된 설계를 만들어 Subjects가 통지할 구체 옵저버들을 알 필요가 없게 합니다.
전략 패턴: 알고리즘을 캡슐화하기
Strategy는 런타임에 알고리즘을 교체할 수 있게 해 큰 조건문 블록을 피합니다. 이는 기존 코드를 수정하지 않고 새로운 전략을 추가할 수 있게 해 Open/Closed 원칙에 부합합니다4.
예시: 쇼핑 카트의 결제 전략들.
interface PaymentStrategy { pay(amount: number): void; }
class CreditCardStrategy implements PaymentStrategy { pay(amount: number): void { console.log(`Paying $${amount} with Credit Card.`); } }
class PayPalStrategy implements PaymentStrategy { pay(amount: number): void { console.log(`Paying $${amount} via PayPal.`); } }
class ShoppingCart {
private paymentStrategy: PaymentStrategy;
constructor(strategy: PaymentStrategy) { this.paymentStrategy = strategy; }
public setPaymentStrategy(strategy: PaymentStrategy) { this.paymentStrategy = strategy; }
public checkout(amount: number): void { this.paymentStrategy.pay(amount); }
}
const cart = new ShoppingCart(new CreditCardStrategy());
cart.checkout(150);
cart.setPaymentStrategy(new PayPalStrategy());
cart.checkout(150);
Strategy는 조건문 복잡성을 제거하고 시스템 확장을 더 쉽게 만듭니다.
흔한 설계 함정과 리팩터링 전략
패턴을 선택하는 것만으로는 충분하지 않습니다. 책임을 독점하고 단일 책임 원칙을 위반하는 God Object 같은 안티패턴을 피하세요. 긴 메서드, 지나친 의존성, 거대 클래스와 같은 코드 스멜을 찾아 규율 있는 리팩터링으로 해결하세요.
리팩터링은 외부 동작을 바꾸지 않으면서 내부 구조를 개선하는 것입니다. 이것이 레거시 시스템을 안전하게 다루는 방법입니다. 예를 들어 긴 switch나 if/else 체인은 Strategy로 추출해 코드 가독성과 테스트 용이성을 높일 수 있습니다4.
OOP 디자인 패턴 관련 자주 묻는 질문
얼마나 많은 디자인 패턴을 배워야 하나요?
우선 5~7개의 핵심 패턴에 집중하세요: Singleton, Factory, Adapter, Decorator, Observer, Strategy. 각 패턴이 어떤 문제를 해결하는지 이해하면 추가 패턴을 익히기가 훨씬 쉬워집니다.
언제 디자인 패턴 사용을 피해야 하나요?
실질적인 문제를 해결하지 못하면서 복잡성만 추가하는 패턴은 피하세요. ‘혹시 몰라’라는 이유로 패턴을 도입하지 마세요. YAGNI 원칙을 따르고, 실제 문제가 발생했을 때 패턴을 도입하세요.
함수형 프로그래밍에서도 디자인 패턴을 사용할 수 있나요?
네. 많은 패턴이 함수형 기법과 자연스럽게 매핑됩니다. Strategy는 함수 파라미터가 될 수 있고, Decorator는 고차 함수로 표현할 수 있습니다. 핵심은 OOP 형식을 엄격히 모방하기보다는 설계 원칙을 적용하는 것입니다.
Clean Code Guy에서는 기본 원칙을 워크플로에 녹여 팀이 오래가는 소프트웨어를 구축하도록 돕습니다. 우리의 코드 감사와 AI 준비 리팩터링이 팀이 자신 있게 배포하도록 어떻게 도울 수 있는지 알아보세요: https://cleancodeguy.com.5
AI가 코드를 작성합니다.당신이 그것을 지속시킵니다.
AI 가속 시대에 클린 코드는 단순히 좋은 관행이 아닙니다 — 확장되는 시스템과 자체 무게로 붕괴되는 코드베이스의 차이입니다.