┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Factory │ │ Strategy │ │ Observer │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ Product │ │Algorithm│ │ Subject │
└─────────┘ └─────────┘ └─────────┘
"또 if-else 지옥이야..." 주니어 시절, 기능 추가할 때마다 조건문이 늘어나는 코드를 보며 한숨 쉬던 기억이 있으신가요? 저도 결제 모듈에 새 PG사를 추가할 때마다 switch문이 길어지는 걸 보며 막막했습니다.
그런데 시니어 개발자가 "여기 팩토리 패턴 쓰면 되겠네요"라고 하더니 30분 만에 리팩토링을 끝내더군요. 그때부터 디자인 패턴의 매력에 빠졌습니다.
⚡ TL;DR
- 실무에서 가장 많이 쓰는 디자인 패턴 3개 완벽 정리
- 복붙 가능한 TypeScript 예제 코드와 실제 적용 사례
목차
- 배경 - 왜 디자인 패턴인가?
- 핵심 개념 정리
- 실습 - 3대 패턴 구현하기
- 모범 사례·베스트 프랙티스
- 마치며 & 참고자료
1. 배경 - 왜 디자인 패턴인가?
디자인 패턴은 반복되는 설계 문제에 대한 재사용 가능한 해결책입니다. 1994년 GoF(Gang of Four)가 정리한 23개 패턴 중, 실무에서는 몇 개만 알아도 충분합니다.
디자인 패턴을 쓰면 좋은 점
✅ 코드 재사용성 - 검증된 구조로 안정성 확보
✅ 의사소통 효율 - "여기 옵저버 패턴 쓰죠"로 설명 끝
✅ 유지보수 용이 - 구조가 명확해 수정이 쉬움
주요 용어 정리
용어 | 설명 | 예시 |
---|---|---|
팩토리(Factory) | 객체 생성을 담당하는 클래스 | PaymentFactory.create('카카오페이') |
전략(Strategy) | 알고리즘을 캡슐화하여 교체 가능하게 | 배송비 계산 방식 변경 |
옵저버(Observer) | 상태 변화를 여러 객체에 알림 | 재고 변경 시 UI 업데이트 |
2. 핵심 개념
디자인 패턴 = 개발자들의 공통 언어
복잡한 설계를 간단한 이름으로 소통할 수 있게 해주는 도구
오늘 다룰 3대 패턴
- Factory Pattern - "뭘 만들지는 나중에 결정해"
- Strategy Pattern - "방법은 여러 개, 선택은 실행 시점에"
- Observer Pattern - "변경사항 있으면 다 알려줘"
3. 실습 - 3대 패턴 구현하기
① Factory Pattern - 결제 수단 추가가 쉬워진다
Before: if-else 지옥
// ❌ 새로운 결제 수단 추가할 때마다 코드 수정 필요
function processPayment(type: string, amount: number) {
if (type === 'card') {
// 카드 결제 로직
} else if (type === 'kakao') {
// 카카오페이 로직
} else if (type === 'naver') {
// 네이버페이 로직
}
// 새로운 결제 수단은...? 😱
}
After: Factory Pattern 적용
// 결제 인터페이스 정의
interface Payment {
pay(amount: number): Promise<boolean>;
getCommission(): number;
}
// 각 결제 수단 구현
class CardPayment implements Payment {
async pay(amount: number): Promise<boolean> {
console.log(`카드로 ${amount}원 결제`);
// 실제 카드 결제 API 호출
return true;
}
getCommission(): number {
return 0.03; // 3% 수수료
}
}
class KakaoPayment implements Payment {
async pay(amount: number): Promise<boolean> {
console.log(`카카오페이로 ${amount}원 결제`);
// 카카오페이 API 호출
return true;
}
getCommission(): number {
return 0.02; // 2% 수수료
}
}
// 팩토리 클래스
class PaymentFactory {
private static payments: Map<string, Payment> = new Map([
['card', new CardPayment()],
['kakao', new KakaoPayment()],
]);
// 새로운 결제 수단 등록
static register(type: string, payment: Payment): void {
this.payments.set(type, payment);
}
// 결제 객체 생성
static create(type: string): Payment {
const payment = this.payments.get(type);
if (!payment) {
throw new Error(`지원하지 않는 결제 수단: ${type}`);
}
return payment;
}
}
// 사용 예시
async function checkout(paymentType: string, amount: number) {
try {
const payment = PaymentFactory.create(paymentType);
const commission = amount * payment.getCommission();
const success = await payment.pay(amount + commission);
if (success) {
console.log(`결제 성공! 수수료: ${commission}원`);
}
} catch (error) {
console.error(error.message);
}
}
// 실행
checkout('kakao', 10000); // 카카오페이로 10200원 결제
② Strategy Pattern - 배송비 계산이 유연해진다
// 배송비 계산 전략 인터페이스
interface ShippingStrategy {
calculate(weight: number, distance: number): number;
}
// 각 배송 전략 구현
class StandardShipping implements ShippingStrategy {
calculate(weight: number, distance: number): number {
return weight * 1000 + distance * 100; // 기본 배송비
}
}
class PremiumShipping implements ShippingStrategy {
calculate(weight: number, distance: number): number {
return 5000; // 프리미엄은 고정 5000원
}
}
class FreeShipping implements ShippingStrategy {
calculate(weight: number, distance: number): number {
return 0; // 무료 배송
}
}
// 배송비 계산기 (Context)
class ShippingCalculator {
private strategy: ShippingStrategy;
constructor(strategy: ShippingStrategy) {
this.strategy = strategy;
}
// 전략 변경 가능
setStrategy(strategy: ShippingStrategy): void {
this.strategy = strategy;
}
calculateCost(weight: number, distance: number): number {
return this.strategy.calculate(weight, distance);
}
}
// 사용 예시
const calculator = new ShippingCalculator(new StandardShipping());
// 일반 배송
console.log(calculator.calculateCost(2, 10)); // 2100원
// VIP 고객은 프리미엄 배송으로 변경
calculator.setStrategy(new PremiumShipping());
console.log(calculator.calculateCost(2, 10)); // 5000원
// 이벤트 기간엔 무료 배송
calculator.setStrategy(new FreeShipping());
console.log(calculator.calculateCost(2, 10)); // 0원
③ Observer Pattern - 재고 변경을 실시간으로
// 옵저버 인터페이스
interface Observer {
update(product: string, stock: number): void;
}
// 주제(Subject) 인터페이스
interface Subject {
attach(observer: Observer): void;
detach(observer: Observer): void;
notify(): void;
}
// 재고 관리 시스템 (Subject)
class Inventory implements Subject {
private observers: Observer[] = [];
private products: Map<string, number> = new Map();
// 옵저버 등록
attach(observer: Observer): void {
this.observers.push(observer);
}
// 옵저버 제거
detach(observer: Observer): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
// 모든 옵저버에게 알림
notify(): void {
this.products.forEach((stock, product) => {
this.observers.forEach(observer => {
observer.update(product, stock);
});
});
}
// 재고 업데이트
updateStock(product: string, stock: number): void {
this.products.set(product, stock);
console.log(`📦 ${product} 재고 변경: ${stock}개`);
this.notify();
}
getStock(product: string): number {
return this.products.get(product) || 0;
}
}
// 구체적인 옵저버들
class WebsiteDisplay implements Observer {
update(product: string, stock: number): void {
console.log(`🖥️ 웹사이트 표시: ${product} - ${stock > 0 ? `재고 ${stock}개` : '품절'}`);
}
}
class MobileApp implements Observer {
update(product: string, stock: number): void {
if (stock < 5) {
console.log(`📱 모바일 알림: ${product} 품절 임박! (${stock}개 남음)`);
}
}
}
class AnalyticsSystem implements Observer {
update(product: string, stock: number): void {
console.log(`📊 분석 시스템: ${product} 재고 데이터 수집 - ${stock}개`);
}
}
// 사용 예시
const inventory = new Inventory();
// 옵저버 등록
const website = new WebsiteDisplay();
const mobile = new MobileApp();
const analytics = new AnalyticsSystem();
inventory.attach(website);
inventory.attach(mobile);
inventory.attach(analytics);
// 재고 변경 시 자동으로 모든 시스템 업데이트
inventory.updateStock('아이폰 15', 10);
console.log('---');
inventory.updateStock('아이폰 15', 3);
console.log('---');
inventory.updateStock('아이폰 15', 0);
/* 출력 결과:
📦 아이폰 15 재고 변경: 10개
🖥️ 웹사이트 표시: 아이폰 15 - 재고 10개
📊 분석 시스템: 아이폰 15 재고 데이터 수집 - 10개
---
📦 아이폰 15 재고 변경: 3개
🖥️ 웹사이트 표시: 아이폰 15 - 재고 3개
📱 모바일 알림: 아이폰 15 품절 임박! (3개 남음)
📊 분석 시스템: 아이폰 15 재고 데이터 수집 - 3개
---
📦 아이폰 15 재고 변경: 0개
🖥️ 웹사이트 표시: 아이폰 15 - 품절
📱 모바일 알림: 아이폰 15 품절 임박! (0개 남음)
📊 분석 시스템: 아이폰 15 재고 데이터 수집 - 0개
*/
4. 베스트 프랙티스
패턴 | 사용하면 좋을 때 | 주의점 |
---|---|---|
Factory | • 객체 생성 로직이 복잡할 때 • 런타임에 객체 타입 결정 |
• 너무 많은 팩토리 클래스 생성 주의 • 단순한 객체는 직접 생성이 나음 |
Strategy | • 알고리즘이 자주 변경될 때 • 조건문이 복잡해질 때 |
• 전략 객체가 너무 많아지면 관리 어려움 • 상태를 가지면 안 됨 |
Observer | • 1:N 의존 관계가 있을 때 • 느슨한 결합이 필요할 때 |
• 순환 참조 주의 • 메모리 누수 방지 위해 detach 필수 |
💡 실무 적용 팁
- Factory Pattern
- DB 커넥션, API 클라이언트 생성에 활용
- 테스트 시 Mock 객체 주입이 쉬워짐
- Strategy Pattern
- 가격 정책, 할인 규칙, 정렬 알고리즘에 적합
- if-else가 3개 이상이면 고려해볼 것
- Observer Pattern
- 이벤트 시스템, 상태 관리 라이브러리의 기본
- React의 useState, Vue의 반응형 시스템도 이 패턴
5. 마치며
오늘 배운 3가지 패턴만 제대로 활용해도 코드 품질이 확 달라집니다. 특히 Factory 패턴은 정말 자주 쓰이니 꼭 익혀두세요.
실제 프로젝트에 적용할 때는 YAGNI(You Aren't Gonna Need It) 원칙을 기억하세요. 당장 필요하지 않은 패턴을 미리 적용하면 오히려 복잡도만 증가합니다.
다음엔 어떤 패턴을 다뤄볼까요? Singleton, Decorator, Adapter 패턴도 실무에서 유용합니다!
❤️ 도움이 되셨다면 하트 한 번, 궁금한 점은 댓글로 남겨주세요!
참고자료
'800===Dev Concepts and License > Design Pattern' 카테고리의 다른 글
더 나은 Java 코드 리팩토링 가이드 🛠️ (0) | 2024.11.13 |
---|---|
Observer Pattern with Java (0) | 2024.05.30 |
Clean Architecture (0) | 2024.05.28 |
Singleton Pattern with Java (0) | 2024.05.28 |
Factory Pattern with Java (0) | 2024.05.27 |