군만두의 IT 공부 일지

[스터디6] 06. 스프링이 사랑한 디자인 패턴 - 쇼핑몰 서비스 본문

프로그래밍/객체지향

[스터디6] 06. 스프링이 사랑한 디자인 패턴 - 쇼핑몰 서비스

mandus 2025. 4. 5. 08:30

목차

     

    이번에는 다양한 디자인 패턴 중 일부를 실제 프로젝트에 적용해 보기로 했다. 각 패턴의 기본 개념과 실습을 진행하려고 한다. 이전 게시글에서 설계한 쇼핑몰 서비스에 대해 디자인 패턴(어댑터, 전략, 템플릿 콜백)을 적용할 것이다.

    06. 스프링이 사랑한 디자인 패턴

    • 디자인 패턴:
      • 실제 개발 현장에서 비즈니스 요구 사항을 프로그래밍으로 처리하면서 만들어진 다양한 해결책 중에서 많은 사람들이 인정한 베스트 프랙티스를 정리한 것
      • 객체 지향의 특성 중 상속(extends), 인터페이스(interface/implements), 합성(객체를 속성으로 사용)만을 이용한다.
    • 스프링 프레임워크: 자바 엔터프라이즈 개발을 편하게 해주는 오픈소스 경량급 애플리케이션 프레임워크

    1. 어댑터 패턴(Adapter Pattern)

    • 어댑터 패턴:
      • 어댑터: 변환기(converter)
      • 객체를 속성으로 만들어서 참조하는 디자인 패턴
      • "호출당하는 쪽의 메서드를 호출하는 쪽의 코드에 대응하도록 중간에 변환기를 통해 호출하는 패턴"
      • 예) ODBC, JDBC, JRE 등

    2. 프록시 패턴(Proxy Pattern)

    • 프록시 패턴:
      • 프록시: 대리자, 대변인
      • 실제 서비스 메서드의 반환값에 가감하는 것을 목적으로 하지 않고 제어의 흐름을 변경하거나 다른 로직을 수행하기 위해 사용한다.
      • "제어 흐름을 조정하기 위한 목적으로 중간에 대리자를 두는 패턴"
      • 개방 폐쇄 원칙(OCP)와 의존 역전 원칙(DIP) 적용
    - 대리자는 실제 서비스와 같은 이름의 메서드를 구현한다. 이때 인터페이스를 사용한다.
    - 대리자는 실제 서비스에 대한 참조 변수를 갖는다(합성).
    - 대리자는 실제 서비스의 같은 이름을 가진 메서드를 호출하고 그 값을 클라이언트에게 돌려준다.
    - 대리자는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수도 있다.

    3. 데코레이터 패턴(Decorator Pattern)

    • 데코레이터 패턴:
      • 데코레이터: 도장/도배업자, 장식자
      • 실제 서비스의 반환 값을 예쁘게 포장(장식)하는 패턴
      • "메서드 호출의 반환값에 변화를 주기 위해 중간에 장식자를 두는 패턴"
      • 개방 폐쇄 원칙(OCP)와 의존 역전 원칙(DIP) 적용
    - 장식자는 실제 서비스와 같은 이름의 메서드를 구현한다. 이때 인터페이스를 사용한다.
    - 장식자는 실제 서비스에 대한 참조 변수를 갖는다(합성).
    - 장식자는 실제 서비스의 같은 이름을 가진 메서드를 호출하고, 그 반환값에 장식을 더해 클라이언트에게 돌려준다.
    - 장식자는 실제 서비스의 메서드 호출 전후에 별도의 로직을 수행할 수도 있다.

    4. 싱글턴 패턴(Singleton Pattern)

    • 싱글턴 패턴: "클래스의 인스턴스, 즉 객체를 하나만 만들어 사용하는 패턴"
    - private 생성자를 갖는다.
    - 단일 객체 참조 변수를 정적 속성으로 갖는다.
    - 단일 객체 참조 변수가 참조하는 단일 객체를 반환하는 getInstance() 정적 메서드를 갖는다.
    - 단일 객체는 쓰기 가능한 속성을 갖지 않는 것이 정석이다.

    5. 템플릿 메서드 패턴(Template Method Pattern)

    • 템플릿 메서드 패턴:
      • 상위 클래스에 공통 로직을 수행하는 템플릿 메서드와 하위 클래스에 오버라이딩을 강제하는 추상 메서드 또는 선택적으로 오버라이딩할 수 있는 훅(Hook) 메서드를 두는 패턴
      • "상위 클래스의 견본 메서드에서 하위 클래스가 오버라이딩한 메서드를 호출하는 패턴"
      • 의존 역전 원칙(DIP) 적용
    템플릿 메서드 패턴의 구성 요소
    - 템플릿 메서드: 공통 로직을 수행, 로직 중에 하위 클래스에서 오버라이딩한 추상 메서드/훅 메서드를 호출
    - 템플릿 메서드에서 호출하는 추상 메서드: 하위 클래스가 반드시 오버라이딩해야 한다.
    - 템플릿 메서드에서 호출하는 훅(Hock, 갈고리) 메서드: 하위 클래스가 선택적으로 오버라이딩한다.

    6. 팩터리 메서드 패턴(Factory Method Pattern)

    • 팩터리 메서드 패턴:
      • 팩터리: 공장
      • "오버라이드된 메서드가 객체를 반환하는 패턴"
      • 의존 역전 원칙(DIP) 적용

    7. 전략 패턴(Strategy Pattern)

    • 전략 패턴:
      • 예) 무기(전략)를 조달(생성)해서 군인(컨텍스트)에게 지급(주입)해 줄 보급 장교(클라이언트, 제3자)
      • 자바 언어에서는 상속 제한이 있어 템플릿 메서드 패턴보다는 전략 패턴이 활용된다.
      • "클라이언트가 전략을 생성해 전략을 실행할 컨텍스트에 주입하는 패턴"
      • 개방 폐쇄 원칙(OCP)과 의존 역전 원칙(DIP) 적용
    전략 패턴의 구성 요소
    - 전략 메서드를 가진 전략 객체
    - 전략 객체를 사용하는 컨텍스트(전략 객체의 사용자/소비자)
    - 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트(제3자, 전략 객체의 공급자)

    8. 템플릿 콜백 패턴(Template Callback Pattern - 견본/회신 패턴)

    • 템플릿 콜백 패턴:
      • 스프링의 3대 프로그래밍 모델 중 하나인 DI(의존성 주입)에서 사용하는 특별한 형태의 전략 패턴
      • 스프링을 이해하고 활용하기 위해서는 전략 패턴, 템플릿 콜백 패턴, 리팩터링된 템플릿 콜백 패턴을 잘 기억해 둔다.
      • "전략을 익명 내부 클래스로 구현한 전략 패턴"
      • 개방 폐쇄 원칙(OCP)과 의존 역전 원칙(DIP) 적용

    9. 스프링이 사랑한 다른 패턴들

    스프링 MVC의 경우, 다음과 같은 패턴을 활용한다.

    • 프론트 컨트롤러 패턴(Front Controller Pattern: 최전선 제어자 패턴):
      • 프론트 컨트롤러: 모든 요청의 진입점을 하나로 통합해 관리하는 객체
      • "하나의 진입점을 통해 공통 처리 로직(로깅, 인증, 인코딩 등)을 모듈화하여 각 요청 처리의 중복을 줄이는 패턴"
      • 예) 스프링 MVC의 DispatcherServlet
    - 요청이 들어오면 먼저 프론트 컨트롤러가 요청을 받아 처리 흐름을 제어한다.
    - 요청 URI에 따라 적절한 컨트롤러로 분기한다.
    - 공통 관심사를 필터나 인터셉터로 처리하기 유리하다.
    • MVC 패턴(Model-View-Controller):
      • Model: 비즈니스 로직과 데이터를 처리
      • View: 사용자에게 보여지는 UI
      • Controller: 사용자의 요청을 받아 처리 흐름을 제어
      • "응용 프로그램을 세 부분으로 분리하여 관심사를 명확히 분리하는 구조"
      • 유지보수성과 테스트 용이성을 높이고, 코드의 응집도와 결합도를 조절할 수 있다.
    - 사용자가 버튼을 클릭하면 컨트롤러가 요청을 받아 모델을 호출한다.
    - 모델은 로직을 처리하고 데이터를 반환한다.
    - 컨트롤러는 그 데이터를 기반으로 적절한 뷰를 선택해 응답을 전달한다.

     

    위에서 정리한 디자인 패턴 표를 간단히 요약하면 다음과 같다.

     

    패턴 이름 핵심 개념 스프링 적용 예 적용 원칙
    어댑터 호환되지 않는 인터페이스를 변환 Spring MVC의 HandlerAdapter -
    프록시 제어 흐름을 위해 대리 객체 사용 Spring AOP, @Transactional OCP, DIP
    데코레이터 기능을 동적으로 추가 BeanPostProcessor OCP, DIP
    싱글턴 하나의 인스턴스만 유지 스프링 빈(기본 싱글턴) -
    템플릿 메서드 공통 로직 + 추상화된 확장 포인트 JdbcTemplate, AbstractController DIP
    팩터리 메서드 객체 생성을 하위 클래스에 위임 BeanFactory, ApplicationContext DIP
    전략 알고리즘을 런타임에 주입 Validation 전략, 정책 클래스 OCP, DIP
    템플릿 콜백 전략을 익명 내부 클래스로 구현 JdbcTemplate, execute(callback) OCP, DIP
    프론트 컨트롤러 공통 진입점으로 요청을 처리 DispatcherServlet -
    MVC 모델-뷰-컨트롤러로 분리 Spring MVC 구조 SRP, SoC

    디자인 패턴 적용 (쇼핑몰 서비스)

    ✅ 목표
    - 주문/결제 흐름에 전략, 어댑터, 템플릿 콜백 패턴을 적용한다.
    - OrderService는 의존성에 묶이지 않고 유연하게 동작한다.
    - 결제 수단 및 주문 방식 확장이 쉬운 구조로 구현한다.
    // 핵심 클래스 구조 요약
    
    OrderTemplate ← 템플릿 콜백 패턴
     └─ NormalOrder (extends) – 기본 주문
     └─ GiftOrder   (extends) – 선물 주문
    
    PaymentStrategy ← 전략 패턴
     └─ KakaoPayStrategy
     └─ TossPayStrategy
    
    PaymentAdapter ← 어댑터 패턴
     └─ KakaoPayAdapter
     └─ TossPayAdapter
    
    OrderTemplateFactory – 주문 템플릿 생성
    PaymentStrategyFactory – 결제 전략 선택
    OrderService – 주문 처리 진입점

    1) PaymentAdapter & PG 어댑터 (어댑터 패턴)

    /* 외부 PG사의 서로 다른 API를 내부에서 통일된 방식으로 사용 */
    public interface PaymentAdapter {
        void process(int amount); // 결제 요청 메서드
    }
    
    /* 외부 PG 호출 로직을 숨기고 내부 인터페이스에 맞게 변환 */
    @Component
    public class KakaoPayAdapter implements PaymentAdapter {
        @Override
        public void process(int amount) {
            System.out.println("[Kakao PG] " + amount + "원 결제 요청");
            // TODO: HTTP 요청 등 외부 API 호출 구현
        }
    }
    
    @Component
    public class TossPayAdapter implements PaymentAdapter {
        @Override
        public void process(int amount) {
            System.out.println("[Toss PG] " + amount + "원 결제 요청");
        }
    }

    2) PaymentStrategy (전략 패턴)

    /* 결제 방식을 캡슐화 */
    public interface PaymentStrategy {
        String getCode(); // 결제 방식 식별용 코드 (예: "kakao", "toss")
        void pay(int amount); // 결제 메서드
    }
    
    @Component
    @RequiredArgsConstructor
    public class KakaoPayStrategy implements PaymentStrategy {
        private final KakaoPayAdapter adapter;
    
        @Override
        public String getCode() {
            return "kakao";
        }
    
        @Override
        public void pay(int amount) {
            adapter.process(amount); // 어댑터를 통해 외부 API 호출
        }
    }
    
    @Component
    @RequiredArgsConstructor
    public class TossPayStrategy implements PaymentStrategy {
        private final TossPayAdapter adapter;
    
        @Override
        public String getCode() {
            return "toss";
        }
    
        @Override
        public void pay(int amount) {
            adapter.process(amount);
        }
    }

    3) OrderTemplate (템플릿 콜백 패턴)

    public abstract class OrderTemplate {
    
        // 순서대로 실행: validate → pay → save → after
        public final void processOrder(int amount) {
            validate();
            pay(amount);
            saveOrder();
            afterOrder();
        }
    
        // 하위 클래스에서 구체화 (콜백)
        protected abstract void validate();      // 유효성 검증
        protected abstract void pay(int amount); // 결제
        protected abstract void saveOrder();     // 주문 저장
        protected void afterOrder() {}           // Hook 메서드 (예: 배송 생성)
    }

    4) 주문 템플릿 구현체

    /* 구체적인 주문 처리 방식 */
    public class NormalOrder extends OrderTemplate {
    
        private final PaymentStrategy paymentStrategy; // 결제 전략 주입
    
        public NormalOrder(PaymentStrategy strategy) {
            this.paymentStrategy = strategy;
        }
    
        @Override
        protected void validate() {
            System.out.println("주문 유효성 검증");
        }
    
        @Override
        protected void pay(int amount) {
            paymentStrategy.pay(amount); // 전략 실행 (결제 수단에 따라 분기됨)
        }
    
        @Override
        protected void saveOrder() {
            System.out.println("Order 저장 로직 실행");
        }
    
        @Override
        protected void afterOrder() {
            System.out.println("배송 정보 생성");
        }
    }

    5) 전략 팩토리 & 템플릿 팩토리

    /* 등록된 모든 결제 전략을 자동으로 수집하여, 코드값으로 전략을 반환 */
    @Component
    public class PaymentStrategyFactory {
    
        private final Map<String, PaymentStrategy> strategyMap;
    
        // 스프링이 자동으로 모든 PaymentStrategy 구현체를 List로 주입
        public PaymentStrategyFactory(List<PaymentStrategy> strategies) {
            this.strategyMap = strategies.stream()
                .collect(Collectors.toMap(PaymentStrategy::getCode, Function.identity()));
        }
    
        public PaymentStrategy getStrategy(String code) {
            // TODO: 결제 코드 전략 구현
            return strategyMap.getOrDefault(code, amount -> {
                throw new IllegalArgumentException("지원하지 않는 결제 수단입니다.");
            });
        }
    }
    /* 결제 전략을 받아 주문 템플릿을 생성 */
    @Component
    @RequiredArgsConstructor
    public class OrderTemplateFactory {
    
        private final PaymentStrategyFactory strategyFactory;
    
        public OrderTemplate create(String paymentCode) {
            PaymentStrategy strategy = strategyFactory.getStrategy(paymentCode);
            return new NormalOrder(strategy); // 템플릿에 전략 주입
        }
    }

    6) 주문 서비스 클래스

    @Service
    @RequiredArgsConstructor
    public class OrderService {
    
        private final OrderTemplateFactory orderTemplateFactory;
    
        public void order(String paymentMethod, int amount) {
            // paymentMethod에 따라 템플릿 생성 및 실행
            OrderTemplate order = orderTemplateFactory.create(paymentMethod);
            order.processOrder(amount); // 템플릿 메서드 실행
        }
    }
    패턴 적용 위치 설명
    전략 패턴 다양한 결제 수단 선택 (Kakao, Toss) PaymentStrategy를 런타임에 주입하여 유연하게 변경
    어댑터 패턴 외부 PG사 API → 내부 통일 인터페이스 PaymentAdapter를 통해 외부 API 호출을 감춤
    템플릿 콜백 패턴 주문 처리 공통 흐름 관리 OrderTemplate에 공통 흐름 정의 + 후처리 확장

    이렇게 패턴을 적용한 구조의 장점은 다음과 같이 정리할 수 있다.

    • 확장성: 결제 수단, 주문 유형이 늘어나도 기존 코드를 수정할 필요가 없다.
    • 유지보수성: 패턴별로 책임이 분리되어 있어 각 기능 테스트가 용이하다.
    • 의존성 최소화: OrderService는 템플릿 팩토리에만 의존하며, 구체 클래스와는 무관하다.

     

    이 글은 『스프링 입문을 위한 자바 객체 지향의 원리와 이해』 책을 학습한 내용을 정리한 것입니다.
    Comments