운영 중인 프로젝트에 디자인 패턴을 적용하며 배운 실전 경험을 정리합니다.

1. Observer 패턴 — 주식 자동매매

문제: TradingService가 매매 실행 후 NotificationService.sendTradeAlert()를 직접 호출. 알림 채널 추가 시 서비스 코드 수정 필요.

해결: Spring ApplicationEvent로 매매 이벤트를 발행하고, 리스너가 비동기로 처리.

// 이벤트 정의
public record TradeEvent(Trade trade, String message) {
    public static TradeEvent of(Trade trade) {
        String emoji = trade.getType() == Trade.TradeType.BUY ? "🟢 매수" : "🔴 매도";
        return new TradeEvent(trade, String.format("%s %s %d주 @%,d원",
                emoji, trade.getStockName(), trade.getQuantity(), trade.getPrice()));
    }
}

// 발행자
@Component
public class TradeEventPublisher {
    private final ApplicationEventPublisher publisher;
    public void publish(Trade trade) {
        publisher.publishEvent(TradeEvent.of(trade));
    }
}

// 리스너 — 텔레그램 알림 (비동기)
@Async @EventListener
public void onTradeEvent(TradeEvent event) {
    notificationService.sendTradeAlert(event.trade());
}

효과: 서비스 코드 수정 없이 Slack, Email 등 새 리스너 추가 가능.


2. Strategy 패턴 — 매매 전략 + 재무 지표

주식/코인 매매 전략

public interface TradingStrategy {
    String name();
    Signal evaluate(String stockCode, List<Double> prices);
    enum Signal { BUY, SELL, HOLD }
}

@Component
public class MaCrossStrategy implements TradingStrategy {
    // 5일선/20일선 골든크로스 → 매수
}

@Component  
public class RsiStrategy implements TradingStrategy {
    // RSI 30 이하 → 매수, 70 이상 → 매도
}

Spring이 List<TradingStrategy>로 자동 주입 → 새 전략 추가 시 클래스만 생성.

DART 재무 지표 추출

public interface MetricExtractor {
    String metricName();
    String extract(List<Map<String, Object>> financialItems);
}

@Component
public class RevenueExtractor implements MetricExtractor {
    @Override public String metricName() { return "매출액"; }
    @Override public String extract(List<Map<String, Object>> items) {
        return items.stream()
            .filter(i -> i.get("account_nm").toString().contains("매출"))
            .map(i -> i.get("thstrm_amount").toString())
            .findFirst().orElse("");
    }
}

기존: AnalysisService에 하드코딩된 if-else 체인 → 신규 지표 추가 시 서비스 수정 필요. 개선: 새 MetricExtractor 구현체만 추가 → OCP(개방-폐쇄 원칙) 준수.


3. Template Method 패턴 — 코인 매매 전략

문제: RsiCryptoStrategyBollingerBandStrategy가 동일한 평가 흐름(데이터 검증 → 지표 계산 → 임계값 비교)을 반복.

public abstract class AbstractCryptoStrategy implements CryptoStrategy {
    
    @Override
    public final Signal evaluate(String coin, List<Double> prices) {
        if (prices.size() < requiredDataPoints()) return Signal.HOLD;
        double indicator = calculateIndicator(prices);
        if (indicator <= buyThreshold()) return Signal.BUY;
        if (indicator >= sellThreshold()) return Signal.SELL;
        return Signal.HOLD;
    }

    protected abstract int requiredDataPoints();
    protected abstract double calculateIndicator(List<Double> prices);
    protected abstract double buyThreshold();
    protected abstract double sellThreshold();
}

하위 클래스는 4개 메서드만 구현. 평가 흐름은 변경 불가(final).


4. Decorator 패턴 — 리스크 관리

문제: 급락장에서 전략이 “매수” 시그널을 보내지만, 리스크 관리 없이 그대로 실행되면 위험.

public class RiskManagedStrategy implements CryptoStrategy {
    private final CryptoStrategy delegate;   // 원본 전략
    private final double maxLossPercent;

    @Override
    public Signal evaluate(String coin, List<Double> prices) {
        Signal signal = delegate.evaluate(coin, prices);
        
        if (signal == Signal.BUY) {
            double change = (prices.get(0) - prices.get(10)) / prices.get(10) * 100;
            if (change < -maxLossPercent) return Signal.HOLD; // 급락 시 매수 차단
        }
        return signal;
    }
}

원본 전략 코드를 수정하지 않고 리스크 체크를 감쌈. 여러 Decorator 중첩 가능.


5. Facade 패턴 — 대시보드

문제: DashboardControllerKisApiClient, TradeRepository, TradingService를 각각 호출.

@Component
public class TradingFacade {
    private final KisApiClient kisApi;
    private final TradeRepository tradeRepo;
    private final TradingService tradingService;

    public Map<String, Object> getPortfolioSummary() {
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("trades", tradeRepo.findTop50());
        summary.put("priceHistory", tradingService.getPriceHistory());
        summary.put("balance", kisApi.getBalance());
        return summary;
    }
}

컨트롤러는 facade.getPortfolioSummary() 한 줄로 모든 데이터 획득.


6. Adapter 패턴 — 미디어 검색

public interface MediaSource {
    String name();
    SearchResult searchPhotos(String query, int page, int perPage);
    SearchResult searchVideos(String query, int page, int perPage);
}

현재 Pexels만 구현. 향후 Unsplash, Pixabay 추가 시 MediaSource 구현체만 작성.


7. Chain of Responsibility — 매매 검증

public interface TradeValidator {
    boolean validate(String stockCode, int qty, int price);
    String reason();
}

@Component
public class MarketHoursValidator implements TradeValidator {
    // 장 시간이 아니면 false
}

검증 로직을 체인으로 연결 — 새 검증 규칙 추가 시 Validator 구현체만 생성.


적용 결과 요약

패턴 프로젝트 Before After
Observer auto-trading 직접 알림 호출 이벤트 기반 비동기
Strategy auto/crypto/dart if-else 체인 인터페이스 + 자동 주입
Template Method crypto-trading 코드 중복 공통 흐름 + 커스텀 계산
Decorator crypto-trading 리스크 관리 없음 전략 래핑
Facade auto-trading 컨트롤러 복잡 단일 진입점
Adapter media-search Pexels 하드코딩 멀티 소스 확장
CoR auto-trading 검증 로직 산재 검증 체인

핵심 교훈: 디자인 패턴은 “코드를 바꾸지 않고 기능을 확장”하기 위한 것. OCP(개방-폐쇄 원칙)를 지키면 새 전략, 새 알림 채널, 새 데이터 소스 추가가 기존 코드 수정 없이 가능해진다.