800===Dev Concepts and License/Design Pattern

🎯 디자인 패턴 3대장, 이것만 알면 코드가 달라진다

블로글러 2024. 5. 28. 12:27
    ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
    │   Factory   │     │  Strategy   │     │  Observer   │
    └──────┬──────┘     └──────┬──────┘     └──────┬──────┘
           │                   │                   │
      ┌────┴────┐         ┌────┴────┐         ┌────┴────┐
      │ Product │         │Algorithm│         │ Subject │
      └─────────┘         └─────────┘         └─────────┘

 

"또 if-else 지옥이야..." 주니어 시절, 기능 추가할 때마다 조건문이 늘어나는 코드를 보며 한숨 쉬던 기억이 있으신가요? 저도 결제 모듈에 새 PG사를 추가할 때마다 switch문이 길어지는 걸 보며 막막했습니다.

그런데 시니어 개발자가 "여기 팩토리 패턴 쓰면 되겠네요"라고 하더니 30분 만에 리팩토링을 끝내더군요. 그때부터 디자인 패턴의 매력에 빠졌습니다.

 

TL;DR

  • 실무에서 가장 많이 쓰는 디자인 패턴 3개 완벽 정리
  • 복붙 가능한 TypeScript 예제 코드와 실제 적용 사례

목차

  1. 배경 - 왜 디자인 패턴인가?
  2. 핵심 개념 정리
  3. 실습 - 3대 패턴 구현하기
  4. 모범 사례·베스트 프랙티스
  5. 마치며 & 참고자료

1. 배경 - 왜 디자인 패턴인가?

디자인 패턴은 반복되는 설계 문제에 대한 재사용 가능한 해결책입니다. 1994년 GoF(Gang of Four)가 정리한 23개 패턴 중, 실무에서는 몇 개만 알아도 충분합니다.

디자인 패턴을 쓰면 좋은 점

코드 재사용성 - 검증된 구조로 안정성 확보
의사소통 효율 - "여기 옵저버 패턴 쓰죠"로 설명 끝
유지보수 용이 - 구조가 명확해 수정이 쉬움

주요 용어 정리

용어 설명 예시
팩토리(Factory) 객체 생성을 담당하는 클래스 PaymentFactory.create('카카오페이')
전략(Strategy) 알고리즘을 캡슐화하여 교체 가능하게 배송비 계산 방식 변경
옵저버(Observer) 상태 변화를 여러 객체에 알림 재고 변경 시 UI 업데이트

2. 핵심 개념

디자인 패턴 = 개발자들의 공통 언어
복잡한 설계를 간단한 이름으로 소통할 수 있게 해주는 도구

오늘 다룰 3대 패턴

  1. Factory Pattern - "뭘 만들지는 나중에 결정해"
  2. Strategy Pattern - "방법은 여러 개, 선택은 실행 시점에"
  3. 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 필수

💡 실무 적용 팁

  1. Factory Pattern
    • DB 커넥션, API 클라이언트 생성에 활용
    • 테스트 시 Mock 객체 주입이 쉬워짐
  2. Strategy Pattern
    • 가격 정책, 할인 규칙, 정렬 알고리즘에 적합
    • if-else가 3개 이상이면 고려해볼 것
  3. Observer Pattern
    • 이벤트 시스템, 상태 관리 라이브러리의 기본
    • React의 useState, Vue의 반응형 시스템도 이 패턴

5. 마치며

오늘 배운 3가지 패턴만 제대로 활용해도 코드 품질이 확 달라집니다. 특히 Factory 패턴은 정말 자주 쓰이니 꼭 익혀두세요.

실제 프로젝트에 적용할 때는 YAGNI(You Aren't Gonna Need It) 원칙을 기억하세요. 당장 필요하지 않은 패턴을 미리 적용하면 오히려 복잡도만 증가합니다.

다음엔 어떤 패턴을 다뤄볼까요? Singleton, Decorator, Adapter 패턴도 실무에서 유용합니다!

❤️ 도움이 되셨다면 하트 한 번, 궁금한 점은 댓글로 남겨주세요!


참고자료

728x90
반응형

'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