We will find a way, we always have.

-interstellar

Computer Science/디자인 패턴

[디자인 패턴] 디자인 패턴 (feat: 스프링)

Redddy 2023. 12. 10. 15:55

디자인 패턴 Design Pattern

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


디자인 패턴의 분류

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

생성 패턴 (Creational Pattern)

  • 생성 패턴의 목적
    • 클래스의 캡슐화를 통해 코드의 유연성과 재사용 가능성을 향상시키는 패턴
  • 생성 패턴의 예
    • 추상 팩토리 (Abstract Factory)
    • 빌더 (Builder)
    • 팩토리 메서드 (Factory Method)
    • 프로토타입 (Prototype)
    • 싱글턴 (Singleton)

구조 패턴 (Structural Pattern)

  • 구조 패턴의 목적
    • 클래스와 객체를 조합하여 더 큰 구조를 만드는 패턴
    • 인터페이스를 사용하여 서로 다른 인터페이스를 가진 객체들을 조합
  • 구조 패턴의 예
    • 어댑터 (Adapter)
    • 브리지 (Bridge)
    • 컴포지트 (Composite)
    • 데코레이터 (Decorator)
    • 퍼사드 (Facade)
    • 플라이웨이트 (Flyweight)
    • 프록시 (Proxy)

행위 패턴 (Behavioral Pattern)

  • 행위 패턴의 목적
    • 클래스나 객체 사이의 알고리즘이나 책임 분배에 관련된 패턴
    • 객체들의 알고리즘을 캡슐화하여 객체의 행위를 변경하고 싶은 경우
    • 객체들의 행위를 객체 안으로 캡슐화하여 재사용성을 높이고 싶은 경우
  • 행위 패턴의 예
    • 책임 연쇄 (Chain of Responsibility)
    • 커맨드 (Command)
    • 인터프리터 (Interpreter)
    • 반복자 (Iterator)
    • 중재자 (Mediator)
    • 메멘토 (Memento)
    • 옵저버 (Observer)
    • 상태 (State)
    • 전략 (Strategy)
    • 템플릿 메서드 (Template Method)
    • 방문자 (Visitor)


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

싱글턴 패턴 (Singleton Pattern)

  • 어플리케이션에서 객체를 하나만 만들어 사용하기 위한 패턴
  • 하나의 인스턴스만을 생성하고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴
  • 스프링은 싱글턴 패턴을 사용하여 (Bean)을 관리
  • 때문에 하나의 빈 인스턴스가 컨테이너 내에서 공유되고, 여러 요청에서 동일한 빈 인스턴스를 사용
  • 빈 스코프를 프로토타입으로 설정하면, 싱글톤이 아닌 매번 새로운 인스턴스를 생성 (디폴트는 싱글톤)

싱글턴 패턴의 구현 방법

public class Singleton {

    private static Singleton instance;

    private Singleton() {}

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

안티패턴

  • 싱글턴 패턴을 안티패턴으로 간주하여 자바 코드에서 사용이 감소하고 있음
  • 전역 상태 공유
    • 싱글턴은 전역 상태를 공유하므로, 다른 객체들 사이에 강한 결합을 만들고, 테스트하기 어렵게 만듦
  • 개발 확장 어려움
    • 만약 다수의 싱글톤이 서로 강하게 결합되어 있다면, 코드의 확장이 어려워질 수 있다.
    • OCP(Open-Closed Principle)를 위반하게 됨
  • 스레드 세이프 문제
    • 멀티스레드 환경에서 동시에 getInstance() 메서드를 호출하면, 여러 인스턴스가 생성될 수 있음
    • 이를 해결하기 위해 getInstance() 메서드에 synchronized 키워드를 사용할 수 있지만, 성능이 저하됨

팩토리 패턴 (Factory Pattern)

  • 객체를 생성하는 인터페이스를 정의하고, 인스턴스를 만드는 일을 서브 클래스에게 위임하는 패턴
  • 객체를 만들어주는 클래스를 별도로 만들어서 객체를 생성하는 방식
  • 스프링에서는 빈을 생성하는 IoC 컨테이너가 팩토리 패턴을 사용하여 빈을 관리

정적 팩토리 메서드 (Static Factory Method)

  • 객체를 생성하는 메서드를 정적(static)으로 선언하여 객체를 생성하는 방식
  • new 연산자 대신 정적 팩토리 메서드를 사용할 시 생성자가 이름을 가질 수 있어 가독성 향상
  • 싱글턴 패턴에서 사용되는 생성자 역시 정적 팩토리 메서드

예시 코드

public class Car {

    private String name;
    private String color;

    public Car(String name, String color) {
        this.name = name;
        this.color = color;
    }

    public static Car createCar(String name, String color) {
        return new Car(name, color);
    }
}

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

  • 객체를 생성하는 인터페이스는 미리 정의하되, 객체 생성은 서브 클래스(팩토리)로 위임하는 패턴
  • 생성된 객체를 반환하는 메서드를 추상화하여 인터페이스를 통한 다형성을 제공
public interface Product {
    void use();
}

public class ConcreteProductA implements Product {
    @Override
    public void use() {
        System.out.println("Using Product A");
    }
}

public class ConcreteProductB implements Product {
    @Override
    public void use() {
        System.out.println("Using Product B");
    }
}

public interface ProductFactory {
    Product createProduct();
}

public class ConcreteProductAFactory implements ProductFactory {
    @Override
    public Product createProduct() {
        return new ConcreteProductA();
    }
}

public class ConcreteProductBFactory implements ProductFactory {
    @Override
    public Product createProduct() {
        return new ConcreteProductB();
    }
}

추상 팩토리 패턴 (Abstract Factory Pattern)

  • 서로 연관되거나 의존적인 객체들의 조합을 만드는 인터페이스를 제공하는 패턴
  • 구체적인 클래스에 의존하지 않고, 인터페이스를 통해 서로 연관된 객체들을 생성
  • 팩토리 메서드 패턴과 마찬가지로 객체를 생성하는 인터페이스를 정의하고, 객체 생성은 서브 클래스로 위임

public interface AbstractFactory {
  Product createProduct();
  AnotherProduct createAnotherProduct();
}

public class ConcreteFactory1 implements AbstractFactory {
  @Override
  public Product createProduct() {
    return new ConcreteProductA();
  }

  @Override
  public AnotherProduct createAnotherProduct() {
    return new ConcreteAnotherProductA();
  }
}

public class ConcreteFactory2 implements AbstractFactory {
  @Override
  public Product createProduct() {
    return new ConcreteProductB();
  }

  @Override
  public AnotherProduct createAnotherProduct() {
    return new ConcreteAnotherProductB();
  }
}

팩토리 메서드 패턴과 추상 팩토리 패턴 비교

공통점

  1. 템플릿 메서드 패턴 사용
  2. 팩토리 클래스를 사용하여 생성
  3. 객체 생성을 서브 클래스로 위임

차이점

  1. 팩토리 클래스에서 생성하는 객체의 개수
    • 팩토리 메서드 패턴은 하나의 객체만 생성
    • 추상 팩토리 패턴은 관련된 객체들을 묶어서 생성
  2. 팩토리 메서드에서 만드는 객체의 종류
    • 팩토리 메서드 패턴은 파라미터에 따라 객체의 종류가 결정
    • 추상 팩토리 패턴은 파라미터에 따라 관련된 객체들을 생성하는 팩토리의 종류가 결정
  3. 겹합도를 낮추는 대상
    • 팩토리 메서드 패턴은 ConcreteProduct와 Client 간의 결합도를 낮출때 사용
    • 추상 팩토리 패턴은 ConcreteFactory와 Client 간의 결합도를 낮출때 사용
  4. 포커스
    • 팩토리 메서드 패턴은 메서드(Factory Method) 레벨에서 포커스를 맞춤
    • 추상 팩토리 패턴은 클래스(Abstract Factory) 레벨에서 포커스를 맞춤

프록시 패턴 (Proxy Pattern)

  • 어떤 객체에 대한 접근을 제어하기 위한 용도로 대리인이나 대변인에 해당하는 객체를 제공하는 패턴
  • 프록시 객체는 실제 객체에 대한 참조를 가지고 있어서 클라이언트의 요청을 처리하고, 실제 객체로 작업을 위임하는 패턴
  • 스프링에서는 프록시 패턴을 사용하여 AOP(https://github.com/jmxx219/CS-Study/blob/main/Java-Spring/AOP.md)를 구현
  • @Transactional 애노테이션 사용시 프록시 객체에 접근하여 트랜잭션을 처리

사용 시나리오

  1. 보안 제어
    • 실제 객체에 직접 접근하는 것을 허용하지 않고, 프록시를 통해 보안 제어를 수행
  2. 원격 지역 호출(Remote Proxy)
    • 원격 서버에 존재하는 객체에 접근하기 위해 프록시를 사용
  3. 캐싱(Caching)
    • 실제 객체에 접근할 때, 프록시를 통해 캐싱 기능을 제공
  4. 지연 로딩(Lazy Loading)
    • 실제 객체의 생성과 초기화를 필요한 시점까지 미뤄, 자원을 효율적으로 관리
  5. 로깅
    • 프록시를 통해 실제 객체에 대한 호출 정보를 기록하거나, 성능 모니터링 등의 추가 기능을 수행

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

  • 어떤 작업을 처리하는 일부분을 서브 클래스로 캡슐화하여 전체 일을 수행하는 구조는 바꾸지 않으면서 특정 단계에서 수행하는 내용을 바꾸는 패턴
  • 스프링에서는 JdbcTemplate, HibernateTemplate 과 같은 템플릿 클래스를 사용하여 데이터베이스 액세스와 같은 일반적인 작업을 단순화
  • DispatcherServlet에서도 사용
  • DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet 이러한 상속 구조를 가지고 있는데, DispatcherServlet의 doService() 메서드가 템플릿 메서드 패턴으로 구현

FrameworkServlet 코드

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에게 위임
    //...중략...
}

DispatcherServlet 코드

public class DispatcherServlet extends FrameworkServlet {
    @Override
    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
        logRequest(request);
        //...중략...
    }
}

  • FrameworkServlet에서 doService에 대한 구현을 하위클래스에 위임하고 있다.
  • 하위 클래스인 DispatcherServlet에서는 doService를 구현하여 서블릿의 기능을 수행하고 있다.
  • DispatcherServlet을 구현하여 processRequest를 호출하면 frameworkServlet의 processRequest의 로직을 타다 soService(request, response)를 만나면 DispatcherSerlvet의 로직을 수행

어댑터 패턴 (Adapter Pattern)

  • 서로 일치하지 않는 인터페이스를 갖는 클래스들을 함께 동작시키기 위해 사용하는 패턴
  • 스프링에서는 어댑터 패턴을 사용하여 다양한 유형의 컨트롤러 및 메시지 변환 작업을 수행



ref