스프링 프레임워크에서 사용되는 디자인 패턴

디자인 패턴(Design Pattern)

  • 소프트웨어 디자인 과정에서 자주 발생하는 문제들에 대한 일반적인 해결책

  • 객체 지향 4대 원칙(캡슐화, 상속, 추상화, 다형성)설계 원칙(SOLID)를 기반으로 구현되어 있음

    • 4대 원칙 ➜ 요리 도구

    • 설계 원칙 ➜ 도구의 사용법

    • 디자인 패턴 ➜ 레시피

  • 디자인 패턴 분류

    • GoF(Gang of Four)의 디자인 패턴은 크게 생성 패턴, 구조 패턴, 행위 패턴으로 분류됨

생성 패턴 (Creational Pattern)

  • 생성 패턴의 목적

    • 객체 인스턴스를 생성할 때와 관련된 문제에 대한 해결책을 다룸

      • 객체 생성 과정을 보다 유연하게 만들어주고, 코드의 타이트한 결합을 방지함

    • 클래스의 캡슐화를 통해 코드의 유연성과 재사용 가능성을 향상시키는 패턴

  • 생성 패턴의 예

    • 싱글턴 (Singleton): 하나의 클래스 인스턴스를 전역에서 접근 가능하게 하면서 해당 인스턴스가 한 번만 생성도록 보장하는 패턴

    • 팩토리 메서드 (Factory Method): 객체를 생성하기 위한 인터페이스를 정의하고, 서브 클래스에서 어떤 클래스의 인스턴스를 생성할지 결정(서브 클래스에게 객체 생성을 위임)하는 패턴

    • 추상 팩토리 (Abstract Factory): 관련된 객체의 패밀리를 생성하기 위한 인터페이스 제공하며, 구체적인 팩토리 클래스를 통해 객체 생성을 추상화하는 패턴

    • 빌더 (Builder): 복잡한 객체의 생성 과정을 단순화하고, 객체를 단계적으로 생성하여 구성하는 패턴

    • 프로토타입 (Prototype): 객체를 복제하여 새로운 객체를 생성하는 패턴으로, 기존 객체를 템플릿으로 사용하는 패턴

구조 패턴 (Structural Pattern)

  • 구조 패턴의 목적

    • 클래스와 객체를 조합하여 더 큰 구조를 만드는 패턴

    • 서로 다른 인터페이스를 가진 객체들을 조합하여 단일 인터페이스르 제공하거나, 서로 다른 객체들을 묶어 새로운 기능을 제공하는 패턴

  • 구조 패턴의 예

    • 어댑터 (Adapter): 인터페이스 호환성을 제공하지 않는 클래스를 사용하기 위해 래퍼(Wrapper)를 제공하는 패턴

    • 브리지 (Bridge): 추상화와 구현을 분리하여 두 가지를 독립적으로 확장할 수 있는 패턴

    • 컴포지트 (Composite): 개별 객체와 복합 객체를 동일하게 다루어 트리 구조의 객체를 구성하는 패턴

    • 데코레이터 (Decorator): 객체에 동적으로 새로운 기능을 추가하여 객체를 확장할 수 있는 패턴

    • 퍼사드 (Facade): 서브 시스템을 더 쉽게 사용할 수 있도록 단순한 인터페이스를 제공하는 패턴

    • 플라이웨이트 (Flyweight): 공유 가능한 객체를 통해 메모리 사용을 최적화하는 패턴

    • 프록시 (Proxy): 다른 객체에 대한 대리자(Proxy)를 제공하여 접근 제어, 지연 로딩 등을 구현하는 패턴

행위 패턴 (Behavioral Pattern)

  • 행위 패턴의 목적

    • 클래스나 객체 사이의 알고리즘이나 책임 분배에 관련된 패턴

      • 객체들의 알고리즘을 캡슐화하여 객체의 행위를 변경하고 싶은 경우

      • 객체들의 행위를 객체 안으로 캡슐화하여 재사용성을 높이고 싶은 경우

  • 행위 패턴의 예

    • 옵저버 (Observer): 객체 간의 일대다 종속 관계를 정의하여 한 객체의 상태 변경이 일어나면 다른 객체들에게 알리는 패턴

    • 전략 (Strategy): 알고리즘을 객체의 행동으로 캡슐화하여, 동일한 행동을 다른 알고리즘으로 교체할 수 있게하는 패턴

    • 커맨드 (Command): 요청을 객체로 캡슐화하여 요청을 매개변수화 하고, 요청을 큐에 저장하거나 로깅하고 실행을 지연시키는 패턴

    • 상태 (State): 객체의 상태를 캡슐화하고, 상태 전환을 관리하는 패턴

    • 책임 연쇄 (Chain of Responsibility): 요청을 보내는 객체와 이를 처리하는 객체를 분리하여, 다양한 처리자 중 하나가 요청을 처리하는 패턴

    • 방문자 (Visitor): 객체 구조의 요소에 대한 연산을 정의하여, 구조의 요소가 연산을 수행하도록 하는 패턴

    • 인터프리터 (Interpreter): 언어나 문법에 대한 해석기를 제공하여, 주어진 언어로 표현된 문제를 해결하는 패천

    • 메멘토 (Memento): 객체의 내부 상태를 저장하고 복원할 수 있는 기능을 제공하는 패턴

    • 중재자 (Mediator): 객체 간의 상호 작용을 캡슐화하여, 객체 간의 직접적인 통신을 방지하는 패턴

    • 템플릿 메서드 (Template Method): 알고리즘 구조를 정의하면서 하위 클래스(서브 클래스)에서 각 단계의 구현을 제공하는 패턴

    • 반복자 (Iterator): 컬렉션 내의 요소들에 접근하는 방법을 표준화하여 컬렉션 내부 구조에 독립적으로 접근할 수 있는 패턴

스프링 프레임워크에서 사용되는 디자인 패턴

싱글톤 패턴 ➜ 스프링 컨테이너(스프링 빈) 팩토리 메서드 패턴 ➜ 스프링의 DI(BeanFactory, Application Context) 프록시 패턴 ➜ 스프링의 AOP(@Transactional) 템플릿 메서드 패턴 ➜ JDBCTemplate, DispatcherServlet 어댑터 패턴 ➜ HandlerAdapter 등등

◽ 싱글턴 패턴 (Singleton Pattern)

  • 어플리케이션에서 객체를 딱 하나만 만들어 사용하기 위한 패턴

    • 인스턴스에서 static을 통해 최초 1번만 메모리를 할당시켜서 클래스의 인스턴스를 딱 1개만 생성하고, 어디서든지 그 인스턴스에 접근할 수 있도록 함

  • 싱글턴 패턴의 구현 방법

    public class Singleton {
        private static Singleton instance;
        
        private Singleton() {}
        
        public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    • 생성자를 private으로 선언하여 외부에서 인스턴스를 생성하지 못하도록 하고,getInstance() 메서드를 통해 싱글턴 인스턴스를 얻을 수 있도록 함

  • 순수 Java로 구현한 싱글톤 패턴의 문제점

    • 싱글톤 패턴을 구현하기 위한 코드가 더 늘어남

    • 인스턴스를 반환해주는 구현 클래스를 직접 참조해야하기 때문에 DIP를 위반함

    • 내부 속성을 변경 및 초기화하기 어렵고, private 생성자로 자식 클래스를 생성하기 어려움

싱글톤 컨테이너(스프링 컨테이너)

스프링에서는 사용자가 싱글톤 패턴을 구현하지 않고, 싱글톤 패턴의 문제점을 해결하면서 객체의 인스턴스를 싱글톤으로 관리함

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리함

    • 스프링 빈(Bean)은 싱글톤 패턴으로 관리되는 빈으로 하나의 빈 인스턴스가 컨테이너 내에서 공유되고, 여러 요청에서 동일한 빈 인스턴스를 사용함

    • 컨테이너 당 하나의 싱글톤 인스턴스를 가지는 구조로, 한 애플리케이션 내부에 여러 개의 컨테이너를 가지는 구조라면 전역적 싱글톤이 아닌 컨테이너에서의 싱글톤 인스턴스 개념을 가짐

  • @Autowired 싱글톤

    • 스프링에서 @Autowired 어노테이션을 통해 컨테이너 내 Bean을 주입받아 사용할 수 있음

      • 최초 애플리케이션 구동 시, Bean을 컨테이너에 등록하여 @Autowired 구문을 찾아 해당 변수에 등록한 Bean을 주입하여 사용함

  • 빈 스코프를 프로토타입으로 설정하면, 싱글톤이 아닌 매번 새로운 인스턴스를 생성할 수 있음 (디폴트는 싱글톤)

안티패턴

  • 전역 상태 공유

    • 싱글턴은 전역 상태를 공유하므로, 다른 객체들 사이에 강한 결합을 만들고, 테스트하기 어렵게 만듦

  • 개발 확장 어려움

    • 만약 다수의 싱글톤이 서로 강하게 결합되어 있다면, 코드의 확장이 어려워질 수 있음

    • OCP(Open-Closed Principle)를 위반하게 됨

  • 스레드 세이프 문제

    • 멀티스레드 환경에서 동시에 getInstance() 메서드를 호출하면, 여러 인스턴스가 생성될 수 있음

    • 이를 해결하기 위해 getInstance() 메서드에 synchronized 키워드를 사용할 수 있지만, 성능이 저하됨

싱글턴 패턴을 안티패턴으로 간주하여 자바 코드에서 사용이 감소하고 있음

◽ 팩토리 메서드 패턴 (Factory Method Pattern)

  • 객체를 생성하는 인터페이스는 미리 정의하되, 객체 생성은 서브 클래스(팩토리)로 위임하는 패턴

    • 객체를 만들어주는 클래스를 별도로 만들어서 객체를 생성하는 방식

  • 생성된 객체를 반환하는 메서드를 추상화하여 인터페이스를 통한 다형성을 제공함

  • 팩토리 메서드 패턴 구현 방법

    public interface Vehicle {
        void drive();
    }
    
    public class Airplane implements Vehicle {
        @Override
        public void drive() {
            System.out.println("drive Airplane");
        }
    }
    
    public class Boat implements Vehicle {
        @Override
        public void drive() {
            System.out.println("drive Boat");
        }
    }
    
    public interface VehicleFactory {
        Vehicle createVehicle();
    }
    
    public class AirplaneVehicleFactory implements VehicleFactory {
        @Override
        public Vehicle createVehicle() {
            return new Airplane();
        }
    }
    
    public class BoatVehicleFactory implements VehicleFactory {
        @Override
        public Vehicle createVehicle() {
            return new Boat();
        }
    }
    • 공통 인터페이스를 정의하고, 인터페이스를 상속한 클래스를 구현함

    • 구현한 클래스의 객체 반환용 팩토리 클래스를 정의

    • 팩토리 클래스를 통해 객체를 받아서 사용함

BeanFactory, Application Context

Spring의 DI(Dependency Injection)는 팩토리 메서드 패턴을 따름 스프링에서는 빈을 생성하는 빈 컨테이너가 팩토리 패턴을 사용하여 빈을 관리함

  • Spring의 BeanFactory

    • 스프링의 BeanFactory 인터페이스는 객체(빈) 생성과 관리의 복잡성을 추상화하는 팩토리 메서드 패턴의 일종으로, 이를 통해 스프링 컨테이너는 빈의 생명주기를 관리함

      public interface BeanFactory {
          getBean(Class<T> requiredType);
          getBean(Class<T> requiredType, Object... args);
          getBean(String name);
          // ...
      }
      • Bean Factory: Bean 정의를 포함하고 Bean을 생성하는 Spring 컨테이너

      • getBean() 메서드는 팩토리 메서드로 간주됨

    • 기본적으로 스프링은 Bean Container를 Bean을 생성하는 Factory로 취급함

  • BeanFactory의 확장 인터페이스인 Application Context

    • 스프링은 BeanFactory를 상속하여 추가적인 Application을 다루는 Application Context 인터페이스를 구현함

    • XML 파일이나 자바 어노테이션과 같은 외부 설정을 기반으로 Bean Container를 시작하기 위해 Application Context를 사용함

◽ 프록시 패턴 (Proxy Pattern)

  • 개념

    • 한 객체(proxy)가 다른 객체(subject or service)에 대한 접근을 제어하기 위한 용도로 대리인이나 대변인에 해당하는 객체를 제공하는 패턴

      • 프록시 객체는 실제 객체(대상 원본 객체)에 대한 참조를 가지고 있어서 클라이언트의 요청을 처리하고, 실제 객체로 작업을 위임함

    • 대상 객체(Subject)의 메소드를 직접 실행하는 것이 아닌, 대상 객체에 접근하기 전에 프록시(Proxy) 객체의 메서드를 접근한 후 추가적인 로직을 처리한 뒤에 대상 객체에 접근함

      • 프록시와 대상은 동일한 인터페이를 가지고 있으며 이를 통해 다른 인터페이스와 완전히 호환되도록 바꿀 수 있음

  • 사용 시기

    • 접근을 제어하거나 기능을 추가하고 싶은데 기존 객체를 수정할 수 없는 상황인 경우

    • 초기화 지연, 접근 제어, 로깅, 캐싱 등 기존 객체 동작에 수정 없이 가미하고 싶은 경우

  • Spring에서는 두 가지 타입의 Proxy를 사용함

    • 빈으로 등록하려느 객체가 인터페이스를 하나라도 구현하고 있으면 JDK를 이용하고, 인터페이스를 구현하고 있지 않으면 내장된 CGLib 라이브러리를 사용함

    • CGLib Proxy: 클래스의 바이트 코드를 조작하여 프록시 객체를 생성해주는 라이브러리

    • JDK Dynamic Proxy: Interface들을 프록시할 때 사용

Spring의 AOP

스프링에서는 프록시 패턴을 사용하여 AOP를 구현함

  • 스프링에서는 AOP에 기반해 수 많은 로킹 처리, 트랜잭션 처리를 공통 모듈화시켜서 따로 작성함

    • Spring의 AOP는 CGLib라는 런타임 프록시 생성 라이브러리를 사용해서 AOP를 위한 프록시 객체를 생성하여 사용함

  • 스프링에서는 어노테이션으로 서비스가 추상회되어 많이 사용하는데, 그 중에서 @Transactional은 DB 트랜잭션 처리를 간단하게 만들어주고 비즈니스에 집중하게 만들어줌

    @Service
    public class BookManager {
        
        @Autowired
        private BookRepository repository;
    
        @Transactional
        public Book create(String author) {
            System.out.println(repository.getClass().getName());
            return repository.create(author);
        }
    }
    • 스프링은 BookRepository 빈 객체를 EnhancerBySpringCGLIB 객체로 wrapping하고, BookRepository 객체에 대한 접근을 제어할 수 있게 함

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

  • 어떤 작업을 처리하는 일부분을 서브 클래스로 캡슐화하여 전체 일을 수행하는 구조는 바꾸지 않으면서 특정 단계에서 수행하는 내용을 바꾸는 패턴

    • 변하지 않는 기능(템플릿)은 상위 클래스에 만들어두고 자주 변경되며 확장할 기능은 하위 클래스에서 만들도록 함

    • 상위의 메서드 실행 동작 순서는 고정하면서 세부 실행 내용은 다양화될 수 있는 경우에 사용

JdbcTemplate

  • JdbcTemplate은 커넥션 정리와 결과 반복, 예외 처리, 트랜잭션 처리 등과 같은 복잡하고 세부적인 사항들을 스프링 내부에서 알아서 처리하게 해주는 코드가 들어가 있음

    • 따라서 반복되는 DB 연동 로직은 JdbcTemplate 클래스의 템플릿 메서드가 제공하고, 개발자는 달라지는 SQL 구문과 설정값만 수정하면 됨

  • DB에 질의할 때 4가지 단계를 거치게 되는데 JdbcTemplate의 query()가 템플릿 메서드 패턴으로 동작함

    public class JdbcTemplate {
        public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
            // Execute query...
        }
    }


public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
    protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ...
        initContextHolders(request, localeContext, requestAttributes);
        ...
        try {
            doService(request, response); // template method pattern 이용
        }
        ...
    }
    protected abstract void doService(HttpServletRequest request, HttpServletResponse response) throws Exception; // subClass에게 위임
}

public class DispatcherServlet extends FrameworkServlet {
    @Override
    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
        logRequest(request);
        ...
    }
}
  • DispatcherServlet ➜ FrameworkServlet ➜ HttpServletBean ➜ HttpServlet 상속 구조를 가지고 있는데, DispatcherServletdoService() 메서드가 템플릿 메서드 패턴으로 구현됨

  • FrameworkServlet에서 doService()에 대한 구현은 하위 클래스에게 위임하고 있고, DispatcherServletdoService()를 구체화하고 있음

    • FrameworkServletprocessRequest() 로직을 수행하다가 doService()를 만나면 DispatcherServletdoService()로직을 수행함

◽ 어댑터 패턴 (Adapter Pattern)

  • 서로 일치하지 않는 인터페이스를 갖는 클래스들을 함께 동작시키기 위해 사용하는 패턴

  • 스프링에서는 어댑터 패턴을 사용하여 다양한 유형의 컨트롤러 및 메시지 변환 작업을 수행함


Ref

Last updated