
소프트웨어 설계에서 반복적으로 발생하는 문제에 대해 반복적으로 적용할 수 있는 해결 방법
⇒ 설계의 재사용
반복되는 문제 상황 → 검증된 해결 패턴 → 구체적 구현 → 유연하고 확장 가능한 설계
구조적 관점: 추상클래스나 인터페이스를 정의하고 인스턴스 사이의 상호작용을 통해 시스템 전체 혹은 일부를 구현해 놓은 재사용 가능한 설계
사용목적 관점: 애플리케이션 개발자가 현재의 요구사항에 맞게 커스터마이징할 수 있는 애플리케이션의 골격⇒ 어플리케이션의 아키텍처를 제공
한 컨텍스트에서 유용한 동시에 다른 컨텍스트에서도 유용한 '아이디어'
⇒ 실무 경험의 산물
Context A ─┐
Context B ─┤─→ Pattern Idea ─┬─→ Role (역할)
Context C ─┘ ├─→ Responsibility (책임)
└─→ Collaboration (협력)
패턴은 공통으로 사용할 수 있는 역할, 책임, 협력의 템플릿(정형화된 틀)이다.
| 요소 | 설명 | 예시 |
|---|---|---|
| 역할(Role) | 객체가 협력 안에서 수행하는 임무 | Sender, Receiver, Observer |
| 책임(Responsibility) | 객체가 알아야 하는 정보와 수행할 작업 | 메시지 전송, 상태 변경 통지 |
| 협력(Collaboration) | 목적을 달성하기 위한 객체들간의 상호작용 | 요청-응답, 발행-구독 |
따라서, 패턴을 학습할 때는 어떤 "역할, 책임, 협력"을 제공하고 있는지 기억하자!
대부분의 디자인 패턴은 협력을 일관성 있고 유연하게 만드는 것을 목적으로 한다.
⇒ 특정한 변경을 캡슐화하기 위한 목적!
구조 도식:
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() { /* 공통 로직 */ }
}
적용 시점: 알고리즘 구조는 동일하나 특정 단계의 구현이 달라야 할 때
구조 도식:
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);
}
}
적용 시점: 동일한 목적을 가진 여러 알고리즘 중 런타임에 선택해야 할 때
구조 도식:
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); // 재귀적 처리
}
}
적용 시점: 부분-전체 계층구조에서 개별 객체와 복합 객체를 균등하게 다뤄야 할 때
구조 도식:
추상화 계층 구현 계층
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 드라이버의 관계
동작 시퀀스:
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
제어 흐름 비교:
라이브러리 방식:
개발자 → 라이브러리 함수 호출 → 결과 반환 → 개발자
프레임워크 방식:
프레임워크 → 확장 포인트 호출 → 개발자 코드 실행 → 프레임워크가 흐름 제어
| 구분 | 프레임워크 | 라이브러리 |
|---|---|---|
| 제어권 | 프레임워크가 애플리케이션 흐름 제어 | 개발자가 직접 호출하여 사용 |
| 확장성 | 정해진 확장 포인트에서만 커스터마이징 | 필요한 기능만 선택적 사용 |
| 관계 | 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 패턴) │
│ │
└────────────────────────────────────────────────────────┘
JdbcTemplate, RestTemplateApplicationEvent와 @EventListenerDataSource 추상화와 실제 DB 드라이버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();
}
}
}
| 위험 신호 | 올바른 접근 |
|---|---|
| 패턴을 위한 패턴 사용 | 실제 문제 해결을 위한 도구로 활용 |
| 모든 곳에 패턴 적용 | 복잡성 증가 대비 효과 검토 |
| 트렌드만 따라하기 | 현재 컨텍스트에 맞는 선택 |