17.3 행동 패턴 (Strategy, Observer, Template Method)
1. Strategy (전략 패턴)
"알고리즘을 인터페이스로 분리하여 런타임에 교체 가능하게" 합니다. 조건문(if-else, switch)으로 분기하는 코드를 제거하고, 동작을 외부에서 주입하는 방식입니다.
// 결제 시스템 예제
@FunctionalInterface
interface PaymentStrategy {
boolean pay(String orderId, int amount);
}
// 결제 수단별 전략
class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
CreditCardPayment(String cardNumber) { this.cardNumber = cardNumber; }
@Override
public boolean pay(String orderId, int amount) {
System.out.printf("💳 신용카드(%s) %,d원 결제 - 주문:%s%n",
cardNumber.substring(cardNumber.length() - 4), amount, orderId);
return true; // 결제 성공
}
}
class KakaoPayPayment implements PaymentStrategy {
@Override
public boolean pay(String orderId, int amount) {
System.out.printf("📱 카카오페이 %,d원 결제 - 주문:%s%n", amount, orderId);
return true;
}
}
class NaverPayPayment implements PaymentStrategy {
@Override
public boolean pay(String orderId, int amount) {
System.out.printf("🟢 네이버페이 %,d원 결제 - 주문:%s%n", amount, orderId);
return true;
}
}
// 컨텍스트: 전략을 사용하는 클래스
class OrderService {
private PaymentStrategy paymentStrategy;
// 전략을 주입 (런타임에 변경 가능)
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(String orderId, int amount) {
System.out.println("=== 주문 처리: " + orderId + " ===");
if (paymentStrategy.pay(orderId, amount)) {
System.out.println("✅ 결제 완료!");
} else {
System.out.println("❌ 결제 실패!");
}
}
}
// 사용 - 결제 수단을 런타임에 선택
OrderService orderService = new OrderService();
orderService.setPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456"));
orderService.checkout("ORDER-001", 45000);
orderService.setPaymentStrategy(new KakaoPayPayment());
orderService.checkout("ORDER-002", 12000);
// 람다식으로 전략 구현 (일회성 전략)
orderService.setPaymentStrategy((orderId, amount) -> {
System.out.printf("🏦 계좌이체 %,d원 결제 - 주문:%s%n", amount, orderId);
return true;
});
orderService.checkout("ORDER-003", 89000);
정렬 전략 (Comparator가 전략 패턴의 대표 예시)
List<String> names = new ArrayList<>(List.of("Charlie", "alice", "BOB", "dave"));
// 다양한 정렬 전략을 런타임에 교체
names.sort(String::compareToIgnoreCase); // 대소문자 무시 알파벳순
names.sort(Comparator.comparingInt(String::length)); // 길이 순
names.sort(Comparator.comparingInt(String::length)
.thenComparing(String.CASE_INSENSITIVE_ORDER)); // 길이 후 알파벳
2. Observer (옵저버 패턴)
"한 객체의 상태 변화를 구독한 모든 객체에게 자동으로 알림" 합니다. 이벤트 기반 시스템의 핵심입니다.
// 이벤트 타입
record OrderEvent(String type, String orderId, int amount) {}
// Observer 인터페이스
@FunctionalInterface
interface OrderObserver {
void onEvent(OrderEvent event);
}
// Subject (Observable): 이벤트를 발행하는 주체
class OrderEventPublisher {
private final List<OrderObserver> observers = new ArrayList<>();
public void subscribe(OrderObserver observer) {
observers.add(observer);
}
public void unsubscribe(OrderObserver observer) {
observers.remove(observer);
}
public void publish(OrderEvent event) {
System.out.println("[Publisher] 이벤트 발행: " + event.type());
for (OrderObserver observer : observers) {
observer.onEvent(event);
}
}
}
// 다양한 Observer 구현
class EmailNotifier implements OrderObserver {
@Override
public void onEvent(OrderEvent e) {
if ("COMPLETED".equals(e.type())) {
System.out.println("📧 이메일: 주문 " + e.orderId() + " 완료 안내 발송");
}
}
}
class InventoryUpdater implements OrderObserver {
@Override
public void onEvent(OrderEvent e) {
if ("PLACED".equals(e.type())) {
System.out.println("📦 재고: 주문 " + e.orderId() + " 에 따른 재고 차감");
}
}
}
class AnalyticsTracker implements OrderObserver {
@Override
public void onEvent(OrderEvent e) {
System.out.printf("📊 분석: 이벤트 기록 [%s] 주문=%s 금액=%,d%n",
e.type(), e.orderId(), e.amount());
}
}
// 사용
OrderEventPublisher publisher = new OrderEventPublisher();
publisher.subscribe(new EmailNotifier());
publisher.subscribe(new InventoryUpdater());
publisher.subscribe(new AnalyticsTracker());
// 람다 구독도 가능
publisher.subscribe(e -> {
if (e.amount() > 100000) {
System.out.println("🌟 VIP 알림: 고액 주문 발생 - " + e.orderId());
}
});
publisher.publish(new OrderEvent("PLACED", "ORD-001", 45000));
publisher.publish(new OrderEvent("COMPLETED", "ORD-001", 45000));
publisher.publish(new OrderEvent("PLACED", "ORD-002", 150000));
3. Template Method (템플릿 메서드 패턴)
"알고리즘의 골격(흐름)은 고정하고, 세부 구현은 서브클래스에 위임" 합니다. 상위 클래스에서 전체 프로세스를 정의하고, 변경이 필요한 부분만 서브클래스가 구현합니다.
// 데이터 수집 리포트 생성기
abstract class ReportGenerator {
// 템플릿 메서드: 알고리즘의 골격 (final로 고정)
public final void generateReport(String title) {
System.out.println("=== " + title + " 리포트 생성 시작 ===");
List<String> data = fetchData(); // 1. 데이터 수집 (서브클래스 구현)
List<String> processed = processData(data); // 2. 가공 (서브클래스 구현)
String report = formatReport(title, processed); // 3. 포맷 (서브클래스 구현)
saveReport(report); // 4. 저장 (공통 로직)
System.out.println("=== 리포트 생성 완료 ===\n");
}
// 추상 메서드: 반드시 구현
protected abstract List<String> fetchData();
protected abstract List<String> processData(List<String> data);
protected abstract String formatReport(String title, List<String> data);
// 훅(Hook) 메서드: 선택적 오버라이딩
protected boolean shouldCompress() { return false; }
// 공통 로직: 변경 불필요
private void saveReport(String report) {
if (shouldCompress()) System.out.println("압축 중...");
System.out.println("저장: " + report.substring(0, Math.min(50, report.length())) + "...");
}
}
// CSV 리포트 구현체
class CsvReportGenerator extends ReportGenerator {
@Override
protected List<String> fetchData() {
System.out.println("📄 CSV 파일에서 데이터 로드");
return List.of("홍길동,30,서울", "김철수,25,부산", "이영희,28,대구");
}
@Override
protected List<String> processData(List<String> data) {
return data.stream()
.map(row -> row.split(","))
.map(parts -> String.format("이름:%s 나이:%s 도시:%s", parts[0], parts[1], parts[2]))
.collect(java.util.stream.Collectors.toList());
}
@Override
protected String formatReport(String title, List<String> data) {
return title + "\n" + String.join("\n", data);
}
}
// DB 리포트 구현체
class DatabaseReportGenerator extends ReportGenerator {
@Override
protected List<String> fetchData() {
System.out.println("🗄️ DB에서 데이터 쿼리");
return List.of("Record1", "Record2", "Record3");
}
@Override
protected List<String> processData(List<String> data) {
return data.stream().map(String::toUpperCase).collect(java.util.stream.Collectors.toList());
}
@Override
protected String formatReport(String title, List<String> data) {
return "<html><h1>" + title + "</h1>" + String.join("<br>", data) + "</html>";
}
@Override
protected boolean shouldCompress() { return true; } // 훅 오버라이딩
}
// 사용
ReportGenerator csvGen = new CsvReportGenerator();
csvGen.generateReport("월별 사용자 현황");
ReportGenerator dbGen = new DatabaseReportGenerator();
dbGen.generateReport("주간 매출 분석");
고수 팁
실무에서 패턴 사용 빈도:
-
Strategy: 가장 자주 쓰이는 패턴 중 하나. Java 8+에서는
@FunctionalInterface+ 람다로 간단히 구현됩니다.Comparator,Predicate,Function이 모두 전략 패턴입니다. -
Observer: Spring의
ApplicationEvent+@EventListener, RxJava/Project Reactor의 리액티브 스트림이 옵저버 패턴의 현대적 구현입니다. -
Template Method: Spring의
JdbcTemplate,RestTemplate,AbstractController등이 템플릿 메서드 패턴의 대표 예시입니다.
패턴 결합: 현실에서는 패턴을 단독으로 쓰기보다 여러 패턴을 조합합니다. 예) Strategy + Factory (전략 객체를 팩토리로 생성), Observer + Singleton (전역 이벤트 버스) 등.