[오브젝트] 디자인 패턴과 프레임워크(chap15)

KwonMoYang·2025년 7월 22일
post-thumbnail

🎯디자인패턴/프레임워크란?

디자인 패턴

소프트웨어 설계에서 반복적으로 발생하는 문제에 대해 반복적으로 적용할 수 있는 해결 방법

⇒ 설계의 재사용

반복되는 문제 상황 → 검증된 해결 패턴 → 구체적 구현 → 유연하고 확장 가능한 설계
  • 변경의 방향과 주기를 이해하는 것이 중요하다

프레임워크

구조적 관점: 추상클래스나 인터페이스를 정의하고 인스턴스 사이의 상호작용을 통해 시스템 전체 혹은 일부를 구현해 놓은 재사용 가능한 설계

사용목적 관점: 애플리케이션 개발자가 현재의 요구사항에 맞게 커스터마이징할 수 있는 애플리케이션의 골격

⇒ 어플리케이션의 아키텍처를 제공


🧩 패턴의 본질과 협력

패턴이란?

한 컨텍스트에서 유용한 동시에 다른 컨텍스트에서도 유용한 '아이디어'

⇒ 실무 경험의 산물

Context A ─┐
Context B ─┤─→ Pattern Idea ─┬─→ Role (역할)
Context C ─┘                 ├─→ Responsibility (책임)
                              └─→ Collaboration (협력)
  • 패턴은 홀로 존재하지 않는다
  • 특정 패턴 내 컴포넌트와 컴포넌트 간의 관계는 더 작은 패턴에 의해 서술될 수 있다

패턴과 협력

패턴은 공통으로 사용할 수 있는 역할, 책임, 협력의 템플릿(정형화된 틀)이다.

요소설명예시
역할(Role)객체가 협력 안에서 수행하는 임무Sender, Receiver, Observer
책임(Responsibility)객체가 알아야 하는 정보와 수행할 작업메시지 전송, 상태 변경 통지
협력(Collaboration)목적을 달성하기 위한 객체들간의 상호작용요청-응답, 발행-구독

따라서, 패턴을 학습할 때는 어떤 "역할, 책임, 협력"을 제공하고 있는지 기억하자!


🎨 주요 디자인 패턴 분석

대부분의 디자인 패턴은 협력을 일관성 있고 유연하게 만드는 것을 목적으로 한다.
⇒ 특정한 변경을 캡슐화하기 위한 목적!

Template Method 패턴

구조 도식:

DataProcessor (추상클래스)
├── processData() [final] ← 템플릿 메서드
│   ├── readData() [구현됨]
│   ├── processBusinessLogic() [추상메서드] ← 하위클래스에서 구현
│   └── saveData() [구현됨]
│
├── ExcelProcessor
│   └── processBusinessLogic() 구현
│
└── CsvProcessor
    └── processBusinessLogic() 구현
// 알고리즘의 골격은 정의하되, 세부 구현은 서브클래스에 위임
public abstract class DataProcessor {
    // 템플릿 메서드 - 알고리즘의 뼈대
    public final void processData() {
        readData();
        processBusinessLogic(); // 추상 메서드
        saveData();
    }
    
    protected abstract void processBusinessLogic();
    
    private void readData() { /* 공통 로직 */ }
    private void saveData() { /* 공통 로직 */ }
}

적용 시점: 알고리즘 구조는 동일하나 특정 단계의 구현이 달라야 할 때

Strategy 패턴

구조 도식:

PriceCalculator ──→ DiscountStrategy (인터페이스)
                          ├── RegularCustomerDiscount
                          ├── VipCustomerDiscount
                          └── NewCustomerDiscount

런타임에 전략 교체 가능 ↕️
// 알고리즘을 캡슐화하여 런타임에 교체 가능하게 만듦
public class PriceCalculator {
    private DiscountStrategy discountStrategy;
    
    public PriceCalculator(DiscountStrategy strategy) {
        this.discountStrategy = strategy;
    }
    
    public double calculatePrice(double originalPrice) {
        return discountStrategy.applyDiscount(originalPrice);
    }
}

적용 시점: 동일한 목적을 가진 여러 알고리즘 중 런타임에 선택해야 할 때

Composite 패턴

구조 도식:

FileComponent (추상클래스)
├── File (개별 객체)
│   └── display() → 직접 처리
│
└── Directory (복합 객체)
    ├── children: List<FileComponent>
    └── display() → 자식들에게 재귀적 위임
        ├── File.display()
        ├── Directory.display()
        │   ├── File.display()
        │   └── File.display()
        └── File.display()
// 개별 객체와 복합 객체를 동일하게 취급
public abstract class FileComponent {
    public abstract void display();
}

public class File extends FileComponent {
    public void display() { /* 파일 표시 */ }
}

public class Directory extends FileComponent {
    private List<FileComponent> children = new ArrayList<>();
    
    public void display() {
        children.forEach(FileComponent::display); // 재귀적 처리
    }
}

적용 시점: 부분-전체 계층구조에서 개별 객체와 복합 객체를 균등하게 다뤄야 할 때

Bridge 패턴

구조 도식:

추상화 계층                    구현 계층
MessageSender ──────────→ MessageDelivery
├── TextMessageSender         ├── EmailDelivery
└── HtmlMessageSender         └── SmsDelivery

독립적 확장 가능:
- 메시지 포맷 추가 (JsonMessageSender)
- 전송 방법 추가 (PushDelivery)
// 추상화와 구현을 분리하여 독립적으로 변경 가능하게 만듦
public abstract class MessageSender {
    protected MessageDelivery messageDelivery; // 구현부에 대한 참조
    
    public MessageSender(MessageDelivery delivery) {
        this.messageDelivery = delivery;
    }
    
    public abstract void sendMessage(String message);
}

public class TextMessageSender extends MessageSender {
    public TextMessageSender(MessageDelivery delivery) {
        super(delivery);
    }
    
    public void sendMessage(String message) {
        String formattedMessage = formatAsText(message);
        messageDelivery.deliver(formattedMessage); // 구현부에 위임
    }
}

// 구현 계층
public interface MessageDelivery {
    void deliver(String message);
}

public class EmailDelivery implements MessageDelivery {
    public void deliver(String message) { /* 이메일 전송 로직 */ }
}

public class SmsDelivery implements MessageDelivery {
    public void deliver(String message) { /* SMS 전송 로직 */ }
}

적용 시점: 추상화와 구현이 모두 확장되어야 하고, 컴파일 타임이 아닌 런타임에 구현을 바꿔야 할 때
실무 예시: 스프링의 DataSource와 실제 DB 드라이버의 관계

Observer 패턴

동작 시퀀스:

OrderService → EventPublisher → 📢 이벤트 발행
                     ├── EmailNotifier → 이메일 발송 완료
                     ├── InventoryUpdater → 재고 업데이트 완료  
                     └── Logger → 로깅 완료
// 객체 간의 일대다 의존관계를 정의하여 상태 변화를 자동으로 통지
public class OrderStatusPublisher {
    private List<OrderStatusObserver> observers = new ArrayList<>();
    private OrderStatus currentStatus;
    
    public void addObserver(OrderStatusObserver observer) {
        observers.add(observer);
    }
    
    public void changeOrderStatus(OrderStatus newStatus) {
        this.currentStatus = newStatus;
        notifyAllObservers(); // 모든 관찰자에게 변경사항 통지
    }
    
    private void notifyAllObservers() {
        observers.forEach(observer -> observer.onOrderStatusChanged(currentStatus));
    }
}

public interface OrderStatusObserver {
    void onOrderStatusChanged(OrderStatus newStatus);
}

// 구체적인 관찰자들
public class EmailNotificationObserver implements OrderStatusObserver {
    public void onOrderStatusChanged(OrderStatus status) {
        if (status == OrderStatus.SHIPPED) {
            sendShippingEmail(); // 배송 완료 이메일 발송
        }
    }
}

public class InventoryObserver implements OrderStatusObserver {
    public void onOrderStatusChanged(OrderStatus status) {
        if (status == OrderStatus.CONFIRMED) {
            updateInventoryCount(); // 재고 수량 업데이트
        }
    }
}

적용 시점: 한 객체의 상태 변화가 여러 객체에게 영향을 주어야 하는 경우
실무 예시: 스프링의 ApplicationEvent@EventListener


⚡ 프레임워크 vs 라이브러리

제어 흐름 비교:

라이브러리 방식:
개발자 → 라이브러리 함수 호출 → 결과 반환 → 개발자

프레임워크 방식:  
프레임워크 → 확장 포인트 호출 → 개발자 코드 실행 → 프레임워크가 흐름 제어
구분프레임워크라이브러리
제어권프레임워크가 애플리케이션 흐름 제어개발자가 직접 호출하여 사용
확장성정해진 확장 포인트에서만 커스터마이징필요한 기능만 선택적 사용
관계IoC (Inversion of Control)일반적인 호출 관계

스프링 프레임워크 예시 분석

@Controller
public class UserController {
    @Autowired
    private UserService userService; // DI 컨테이너가 주입
    
    @RequestMapping("/users")
    public String getUsers(Model model) {
        // 비즈니스 로직에만 집중
        model.addAttribute("users", userService.findAllUsers());
        return "userList"; // 뷰 리졸버가 처리
    }
}

프레임워크의 제어역전: 개발자는 비즈니스 로직에만 집중하고, 나머지는 프레임워크가 처리


🔄 패턴과 프레임워크의 관계

스프링 아키텍처에서의 패턴 활용

┌─────────────────── Spring Framework ───────────────────┐
│                                                        │
│  📋 Presentation Layer                                 │
│  ├── @Controller                                       │
│  └── HandlerMapping (Composite 패턴)                   │
│                                                        │
│  🏢 Business Layer                                     │
│  ├── @Service                                          │
│  └── @EventListener (Observer 패턴)                    │
│                                                        │
│  💾 Data Layer                                         │
│  ├── @Repository                                       │
│  ├── JdbcTemplate (Template Method 패턴)               │
│  └── DataSource (Bridge 패턴)                          │
│                                                        │
│  🔧 Infrastructure                                     │
│  ├── BeanFactory (Factory 패턴)                        │
│  └── Security Strategy (Strategy 패턴)                 │
│                                                        │
└────────────────────────────────────────────────────────┘

프레임워크에 녹아든 패턴들

  • Template Method: 스프링의 JdbcTemplate, RestTemplate
  • Strategy: 스프링 시큐리티의 인증 전략
  • Observer: 스프링의 ApplicationEvent@EventListener
  • Bridge: 스프링의 DataSource 추상화와 실제 DB 드라이버
  • Factory: 스프링의 빈 팩토리
  • Composite: 스프링 MVC의 HandlerMapping 체인

스프링 이벤트 시스템 활용 예시

// 이벤트 발행자
@Component
public class OrderService {
    @Autowired
    private ApplicationEventPublisher eventPublisher;
    
    public void processOrder(Order order) {
        // 주문 처리 로직
        processOrderLogic(order);
        
        // 이벤트 발행 - 여러 리스너가 자동으로 반응
        eventPublisher.publishEvent(new OrderProcessedEvent(order));
    }
}

// 이메일 알림 리스너
@EventListener
@Component
public class EmailNotificationListener {
    public void handleOrderProcessed(OrderProcessedEvent event) {
        sendOrderConfirmationEmail(event.getOrder());
    }
}

// 재고 업데이트 리스너
@EventListener 
@Component
public class InventoryUpdateListener {
    public void handleOrderProcessed(OrderProcessedEvent event) {
        updateInventoryForOrder(event.getOrder());
    }
}

왜 이 방식이 좋은가?

  • 결합도 감소: OrderService는 이메일 발송이나 재고 관리를 몰라도 됨
  • 확장성: 새로운 리스너 추가 시 기존 코드 수정 불필요
  • 테스트 용이성: 각 컴포넌트를 독립적으로 테스트 가능

🗺️ 언제 패턴을 적용할까? - 나만의 기준

패턴 선택 의사결정 트리:

문제 상황 발생
       ↓
   문제 유형 분석
       ├── 중복 코드 발견 → Template Method / Strategy 검토
       ├── 복잡한 조건문 → Strategy / State 패턴 검토  
       ├── 객체 생성 복잡 → Factory 패턴 검토
       ├── 추상화-구현 분리 → Bridge 패턴 검토
       ├── 상태 변화 전파 → Observer 패턴 검토
       └── 계층구조 통일 처리 → Composite 패턴 검토
                    ↓
              적용 효과 > 복잡성 증가?
                    ├── Yes → ✅ 패턴 적용
                    └── No → ❌ 단순한 방법 유지

패턴 선택 기준

// 나쁜 예: 불필요한 패턴 적용
public class SimpleCalculatorFactory {
    public Calculator createCalculator() {
        return new Calculator(); // 단순한 생성인데 팩토리 패턴 사용
    }
}

// 좋은 예: 필요에 의한 패턴 적용
public class DatabaseConnectionFactory {
    public Connection createConnection(String dbType) {
        switch(dbType) {
            case "mysql": return new MySqlConnection();
            case "oracle": return new OracleConnection();
            default: throw new IllegalArgumentException();
        }
    }
}

패턴 적용 시 주의사항

위험 신호올바른 접근
패턴을 위한 패턴 사용실제 문제 해결을 위한 도구로 활용
모든 곳에 패턴 적용복잡성 증가 대비 효과 검토
트렌드만 따라하기현재 컨텍스트에 맞는 선택
  1. 과도한 패턴 사용 금지: 패턴을 위한 패턴은 오히려 복잡성만 증가
  2. 컨텍스트 고려: 현재 상황에 정말 필요한 패턴인지 검토
  3. 단순함 우선: 간단한 문제는 간단하게 해결
profile
Dot Your moment.

0개의 댓글