UML로 디자인 패턴과 코드 구조를 일치시키는 설계

UML로 Factory, Strategy, Observer 패턴의 역할과 관계를 모델링하고 코드·CI 검증까지 연결하는 설계 방법을 다룬다.

2026-08-14 · 최초 발행 2025-10-31

모델과 구현이 같은 구조를 가리키게 만들기

설계 문서의 패턴 구조와 실제 코드가 어긋나면, UML은 설명용 산출물로만 남는다. UML 기반 패턴 모델링은 클래스·시퀀스·패키지 다이어그램으로 역할과 책임을 드러내고, 참여자와 관계를 코드 구조에 대응시키는 방식이다.

여기서 모델은 Interface, Concrete, Context, Subject/Observer 같은 참여자와 Realization, Dependency, Composition 관계를 명시한다. 구현 단계에서는 패키지 구성, 인터페이스, 네이밍 규칙을 모델링 가이드와 연결하고, 정적 분석을 CI에 넣어 인터페이스 미구현이나 의존성 역전 위배를 검증한다.

Factory는 생성 책임을 분리하고, Strategy는 선택 가능한 행위를 캡슐화하며, Observer는 이벤트 기반의 비동기 확장을 담당한다. 도메인 서비스, 플러그인 구조, 통합 이벤트 라우팅은 이런 구조를 우선 적용할 수 있는 영역이다.

UML 템플릿에서 품질 게이트까지

패턴별 UML 템플릿에는 «Factory», «Strategy», «Observer/Subject» 스테레오타입을 두고, 의존성 방향, public/protected 가시성, 추상화 수준을 같은 표기 규칙으로 관리한다. 코드 쪽에서는 패키지명과 인터페이스명 규칙을 바탕으로 스캐폴딩 또는 스켈레톤을 만들고, CI가 모델과 코드의 일치성 및 패턴 제약을 확인한다.

운영 환경에서는 패턴의 분리 효과도 달라진다. Factory는 런타임 플러그인 로딩 구조를 제공하고, Strategy는 구성에 따른 정책 전환을 지원한다. Observer는 도메인 이벤트 전파와 사후 처리를 분리하므로 비동기 큐와 연결하기 쉽다.

다만 클래스 수 증가, 지나친 추상화, 다이어그램 복잡도는 함께 관리해야 한다. 행위 변경 빈도, 제품군의 다양성, 이벤트 팬아웃 규모를 기준으로 패턴 적용 임계값을 정해 두는 편이 낫다.

주문 승인에서 패턴이 만나는 지점

주문 승인 흐름에서는 Factory가 결제 게이트웨이를 만들고, Strategy가 가격을 계산하며, Observer가 승인 이후의 후속 처리를 알린다.

MailObserverInventoryObserverOrderEventBusPaymentGatewayPricingContextPaymentFactoryClientMailObserverInventoryObserverOrderEventBusPaymentGatewayPricingContextPaymentFactoryClientcreate("CARD")Gatewayprice(order)totalpay(total)approvedpublish(OrderApproved)onEvent(OrderApproved)onEvent(OrderApproved)

입력에는 주문 요청, 결제 수단, 가격 정책 식별자가 들어간다. 처리 과정은 Factory를 통한 게이트웨이 생성, Strategy의 금액 계산, 결제 승인, Observer의 후속 이벤트 전파로 이어진다. 결과로는 승인 상태와 구독자별 처리 결과가 남는다.

게이트웨이를 지원하지 않을 때는 Null Object 또는 예외 변환을 사용하고 폴백 결제 수단을 적용할 수 있다. 가격 정책이 지정되지 않은 경우에는 DefaultStrategy를 주입하고 정책 로딩 실패를 캐싱한다. 이벤트 처리가 실패하면 재시도와 데드레터 큐를 두고, 구독자별 서킷 브레이커를 적용한다.

도메인 상태가 커밋된 뒤 이벤트를 발행하도록 Outbox 패턴을 적용하고, 관찰자 핸들러는 핵심 키 기반 Upsert와 중복 감지 토큰으로 멱등성을 확보한다. 결제 승인과 재고 차감 사이의 동시성은 분산 락 또는 사가 보상 트랜잭션으로 설계한다.

역할 분리가 만드는 운영 특성

패턴/조합 성능 확장성 일관성 안정성 운영 편의
Factory 중간, 생성 비용 선형 높음, 제품군 추가 용이 높음, 생성 책임 집중 높음, 의존성 고립 높음, 테스트 대체 용이
Strategy 중간, 간접 호출 오버헤드 높음, 정책 교체 유연 높음, 행위 표준화 중간, 잘못된 조합 리스크 높음, A/B 실험 용이
Observer 높음(비동기 시) 매우 높음, 구독자 확장 중간, 최종 일관성 모델 중간, 실패 전파 관리 필요 중간, 모니터링 필수
통합 적용 중간~높음, 경로 최적화 필요 매우 높음, 모듈식 스케일 높음, 역할 분리 높음, 격리·복구 전략 병행 높음, 책임 경계 명확

Java 코드로 연결한 Factory·Strategy·Observer

전제: JDK 17, Gradle/Maven 프로젝트, 표준 라이브러리만 사용.

import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

// Strategy
interface PricingStrategy {
    BigDecimal price(Order order);
}
final class DefaultPricing implements PricingStrategy {
    public BigDecimal price(Order order) { return order.baseAmount(); }
}
final class SeasonalPricing implements PricingStrategy {
    public BigDecimal price(Order order) {
        return order.baseAmount().multiply(new BigDecimal("0.9"));
    }
}
final class PricingContext {
    private final PricingStrategy strategy;
    PricingContext(PricingStrategy strategy) { this.strategy = strategy; }
    BigDecimal price(Order order) { return strategy.price(order); }
}

// Factory
interface PaymentGateway { boolean pay(BigDecimal amount); }
final class CardGateway implements PaymentGateway {
    public boolean pay(BigDecimal amount) { return true; }
}
final class VAccountGateway implements PaymentGateway {
    public boolean pay(BigDecimal amount) { return true; }
}
final class PaymentFactory {
    static PaymentGateway create(String type) {
        return switch (type) {
            case "CARD" -> new CardGateway();
            case "VACCT" -> new VAccountGateway();
            default -> throw new IllegalArgumentException("unsupported type: " + type);
        };
    }
}

// Observer
record OrderApproved(String orderId) {}
interface Observer<T> { void onEvent(T event); }
final class OrderEventBus {
    private final List<Observer<OrderApproved>> observers = new CopyOnWriteArrayList<>();
    void subscribe(Observer<OrderApproved> o) { observers.add(o); }
    void publish(OrderApproved e) { observers.forEach(o -> o.onEvent(e)); }
}

// Domain
record Order(String id, BigDecimal baseAmount) {}

public class Demo {
    public static void main(String[] args) {
        var order = new Order("O-1", new BigDecimal("100.00"));
        var gateway = PaymentFactory.create("CARD");
        var total = new PricingContext(new SeasonalPricing()).price(order);

        var bus = new OrderEventBus();
        bus.subscribe(e -> System.out.println("Inventory updated: " + e.orderId()));
        bus.subscribe(e -> System.out.println("Mail sent: " + e.orderId()));

        if (gateway.pay(total)) {
            // 트랜잭션 커밋 이후 발행되는 것으로 가정
            bus.publish(new OrderApproved(order.id()));
        }
    }
}

Factory는 Composition Root의 조립 영역에서 사용하고, 런타임 선택이 필요한 경우에 한정하는 편이 좋다. 수동 Factory를 과도하게 늘리면 DI 컨테이너와의 경계가 흐려진다.

전략 선택은 Feature Flag나 요청 헤더 같은 구성 기반 메커니즘으로 처리해 조건 분기가 퍼지는 일을 막는다. 성능 민감 경로에서는 인라인 전략 바인딩과 캐싱을 함께 적용한다. 도메인 내부 Observer 호출은 동기로 유지해 단순성을 확보하고, 외부 통합은 메시지 큐로 비동기화한다. 구독 해제와 구독자 수 모니터링도 메모리 누수 방지를 위해 필요하다.

패턴 조합이 맞는 시스템

결제 라우팅에서는 Factory가 결제 게이트웨이를 고르고, Strategy가 수수료·환율 정책을 적용한다. 승인과 실패 이벤트는 Observer를 통해 정산, CRM, 알림으로 팬아웃된다.

추천·랭킹 엔진에서는 Strategy로 알고리즘을 A/B/C 실험에 맞게 교체하고, Factory로 피처 전처리 파이프라인을 구성한다. Observer는 모델 성능 지표 수집과 피드백 루프 연결을 맡는다.

알림·구독 플랫폼은 Observer로 주제 기반 구독을 관리하고 비동기 큐 및 재시도 정책과 연계할 수 있다. Strategy는 클릭 최적화나 정시 발송 같은 전송 정책을 선택한다. 플러그인 아키텍처에서는 Factory가 플러그인 로더를 구현하고, Strategy가 확장 포인트별 행위를 주입하며, Observer가 수명주기 이벤트와 감사 로깅을 처리한다.

변경 비용과 관찰가능성에 남는 효과

신규 기능을 추가할 때 영향 범위는 3050% 축소되고, 코드 중복은 2040% 감소할 수 있다. 회귀 결함 밀도는 1020% 감소하고 단위 테스트 커버리지는 1525%p 향상되며, 이벤트 기반 팬아웃은 처리량을 1.5~3배 확장한다.

설계 의도가 UML에 드러나면 팀이 공유하는 언어가 생기고 변경과 온보딩에 드는 시간이 줄어든다. 생성·행위·이벤트 책임을 분리한 구조는 운영 중 관찰가능성(Observable)도 강화한다.

UML디자인 패턴소프트웨어 설계팩토리 패턴옵저버 패턴