Design Pattern

김하영·2021년 2월 1일
0
post-custom-banner

토픽 : Design Pattern

  1. Design Pattern 의 개념
  2. GoF Design Pattern 의 구성
  3. Spring framework 에서 사용되는 대표적인 패턴 3가지
  4. Observer 패턴을 이용하여 코드 작성해 보기

Design Pattern 의 개념

소프트웨어 공학론 안의 좋은 코드를 설계하기 위한 일종의 디자인(설계) 패턴.

  • 좋은 코드란?
    디자인 패턴에서 말하는 좋은 코드는 설계적 관점에서의 좋은 코드이다.
    즉, 확장과 수정에 용이하여 설계 이후에도 추가적인 유지 보수에 비용이 적게들어가는 코드를 말한다.
    객체지향적으로 생각하면 추구해야할 설계 방향은 다음과 같다.

    객체 간 응집도는 높이고, 결합도는 낮게.
    요구 사항 변경 시, 코드 변경을 최소화 하는 방향으로.

즉, 프로그램을 개발하는 과정에서 빈번하게 발생하는 디자인(설계) 상의 문제를 정리해서 상황에 따라 간편하게 적용해서 쓸 수 있는 패턴이다.

Design Pattern 의 필요성

디자인 패턴을 사용하면 아래와 같은 효과를 얻을 수 있기 때문에 필요하다.

  1. 개발자간의 원할한 의사소통
    : 개발자들이 여러 가지 디자인 패턴을 알고있다면 문제가 발생했을때 그것을 해결하기위해 어떤 디자인 패턴을 사용할 것인지 논의할 수 있다.

  2. 소프트웨어 구조 파악 용이
    : 디자인 패턴을 잘 알고 있다면 그 디자인 패턴이 적용된 소프트웨어의 전체적인 구조를 쉽게 파악이 가능하다.

  3. 재사용성을 통한 개발 기간 단축
    : 이미 만들어진 디자인 패턴을 사용하면 처음부터 만들지 않아 개발 기간을 단축할 수 있다.

  4. 설계 변경 요청에 대한 유연한 대처
    : 디자인 패턴을 사용하여 설계하면 각종 버그나 기능 추가, 운영체제 호환성 등의 이유로 설계를 변경하거나 유지보수를 할 때 짧은 시간 내 완료할 수 있다.

GoF Design Pattern 의 구성

GoF(Gang of Four)에서는 23가지 디자인 패턴을 3가지 유형으로 분류한다.

  • GoF(Gang of Four)
    《디자인 패턴》(Design Patterns, ISBN 0-201-63361-2)은 소프트웨어 설계에 있어 공통된 문제들에 대한 표준적인 해법과 작명법을 제안한 책이다. 이 분야의 사인방(Gang of Four, 줄여 GoF)으로 불리는 에리히 감마(Erich Gamma), 리처드 헬름(Richard Helm), 랄프 존슨(Ralph Johnson), 존 블리시데스(John Vlissides)가 같이 썼다.

A. 생성 패턴(Creational Patterns)

  • 객체를 생성하는데 관련된 패턴들
  • 객체가 생성되는 과정의 유연성을 높이고 코드의 유지를 쉽게 함
  • 생성 패턴 종류
  1. 추상 팩토리 패턴(Abstract Factory Pattern)
    : 동일한 주제의 다른 팩토리를 묶어 준다.
  2. 빌더 패턴(Builder Pattern)
    : 생성(construction)과 표기(representation)를 분리해 복잡한 객체를 생성한다.
  3. 팩토리 메서드 패턴(Factory Method Pattern)
    : 생성할 객체의 클래스를 국한하지 않고 객체를 생성한다.
  4. 프로토타입 패턴(Prototype Pattern)
    : 기존 객체를 복제함으로써 객체를 생성한다.
  5. 싱글턴 패턴(Singleton pattern)
    : 한 클래스에 한 객체만 존재하도록 제한한다.

B. 구조 패턴(Structural Patterns)

  • 프로그램 구조에 관련된 패턴들
  • 프로그램 내의 자료구조나 인터페이스 구조 등 프로그램의 구조를 설계하는데 활용할 수 있는 패턴들
  • 구조 패턴 종류
  1. 어댑터 패턴(Adapter Pattern)
    : 인터페이스가 호환되지 않는 클래스들을 함께 이용할 수 있도록, 타 클래스의 인터페이스를 기존 인터페이스에 덧씌운다.
  2. 브리지 패턴(Bridge Pattern)
    : 추상화와 구현을 분리해 둘을 각각 따로 발전시킬 수 있다.
  3. 합성 패턴(Composite pattern)
    : 0개, 1개 혹은 그 이상의 객체를 묶어 하나의 객체로 이용할 수 있다.
  4. 데코레이터 패턴(Decorator Pattern)
    : 기존 객체의 매서드에 새로운 행동을 추가하거나 오버라이드 할 수 있다.
  5. 파사드 패턴(Facade Pattern)
    : 많은 분량의 코드에 접근할 수 있는 단순한 인터페이스를 제공한다.
  6. 플라이웨이트 패턴(Flyweight Pattern)
    : 다수의 유사한 객체를 생성·조작하는 비용을 절감할 수 있다.
  7. 프록시 패턴(Proxy Pattern)
    : 접근 조절, 비용 절감, 복잡도 감소를 위해 접근이 힘든 객체에 대한 대역을 제공한다.

C. 행동 패턴(Behavioral Patterns)

  • 반복적으로 사용되는 객체들의 상호작용을 패턴화 해놓은 것들
  1. 책임연쇄 패턴(Chain of responsibility)
    : 책임들이 연결되어 있어 내가 책임을 못 질 것 같으면 다음 책임자에게 자동으로 넘어가는 구조이다.
  2. 커맨드 패턴(Command Pattern)
    : 명령어를 각각 구현하는 것보다는 하나의 추상 클래스에 메서드를 하나 만들고 각 명령이 들어오면 그에 맞는 서브 클래스가 선택되어 실행한다.
  3. 해석자 패턴(Interpreter Pattern)
    : 문법 규칙을 클래스화한 구조를 갖는 SQL 언어나 통신 프로토콜 같은 것을 개발할 때 사용한다.
  4. 반복자 패턴 (Iterator Pattern)
    : 반복이 필요한 자료구조를 모두 동일한 인터페이스를 통해 접근할 수 있도록 메서드를 이용해 자료구조를 활용할 수 있도록 해준다.
  5. 옵저버 패턴(Observer Pattern)
    : 어떤 클래스에 변화가 일어났을 때, 이를 감지하여 다른 클래스에 통보해준다.
  6. 전략 패턴 (Strategy Pattern)
    : 알고리즘 군을 정의하고 각각 하나의 클래스로 캡슐화한 다음, 필요할 때 서로 교환해서 사용할 수 있게 해준다.
  7. 템플릿 메서드 패턴 (Template method pattern)
    : 상위 클래스에서는 추상적으로 표현하고 그 구체적인 내용은 하위 클래스에서 결정된다.
  8. 방문자 패턴 (visitor Pattern)
    : 각 클래스의 데이터 구조로부터 처리 기능을 분리하여 별도의 visitor 클래스로 만들어놓고 해당 클래스의 메서드가 각 클래스를 돌아다니며 특정 작업을 수행한다.
  9. 중재자 패턴 (Mediator Pattern)
    : 클래스간의 복잡한 상호작용을 캡슐화하여 한 클래스에 위임해서 처리한다.
  10. 상태 패턴 (State Pattern)
    : 동일한 동작을 객체의 상태에 따라 다르게 처리해야 할 때 사용한다.
  11. 기념품 패턴 (Memento Pattern)
    : Ctrl + z 와 같은 undo 기능 개발할 때 유용한 디자인패턴. 클래스 설계 관점에서 객체의 정보를 저장한다.

Spring framework 에서 사용되는 대표적인 패턴 3가지

A. Spring - AOP
: 구조 패턴(Structural Patterns) > 프록시 패턴(Proxy Pattern)

  • 프록시 패턴(Proxy Pattern) 이란?

어떤 객체를 직접적으로 참조하는 것이 아니라, 그 객체를 대행(대리,proxy)하는 객체를 통해 대상객체에 접근하는 방식이다.
이 방식을 사용하면 실제 대상 객체가 메모리에 존재하지 않아도 기본적인 정보를 참조하거나 설정할수 있고, 실제 객체의 기능이 반드시 필요한 시점까지 객체의 생성을 미룰 수 있다.
프록시 객체와 실제 객체는 같은 인터페이스를 구현하여, 프록시 객체는 실제 객체와 치환이 가능하다.

  • 프록시 패턴의 장점
  1. 가상 프록시
    사이즈가 큰 객체가 로딩되기 전에도 proxy를 통해 참조를 할 수 있다.
    이미지, 동영상 등의 로딩에서 반응성 혹은 성능이 향상된다.

  2. 보호 프록시
    실제 객체의 public, protected 메소드들을 숨기고 인터페이스를 통해 노출시킬수 있다.
    안전성이 보장된다.

  3. 원격 프록시
    로컬에 있지 않고 떨어져 있는 객체를 사용할 수 있다.
    예) RMI, EJB 의 분산처리

  4. 스마트 프록시
    원래 객체의 접근에 대해서 사전처리를 할수 있다.
    예) 객체의 레퍼런스 카운트 처리

  5. 변형된 가상 프록시
    원본 객체의 참조만 필요할때는 원복객체를 사용하다가, 최대한 늦게 복사가 필요한 시점에 원본객체를 예) Concurrent package의 CopyInWriteArrayList

  • 프록시 패턴의 단점
  1. 객체를 생성할때 한단계를 거치게 되므로, 빈번한 객체 생성이 필요한 경우 성능이 저하될 수 있다.

  2. 프록시 내부에서 실제 객체 생성을 위해서 thread가 생성되고 동기화가 구현되야 하는 경우 성능이 저하되고 로직이 난해해진다.

  • 스프링의 프록시 패턴

스프링에서는 AOP(Aspect Oriented Programming)에 기반해 수많은 로깅처리, 트랜잭션 처리를 하고 있다. Spring의 AOP는 CGLIB이라는 런타임 프록시 생성 라이브러리를 사용해, AOP를 위한 프록시 객체를 생성한다. xml과 AspectJ 스크립트를 사용해 대상객체들에 대한 프록시를 생성하므로 코드로 적을수는 없다. 참고로 CGLIB을 사용하면 프록시의 대상이 되는 객체가 인터페이스를 구현안했더라도 프록시 방식으로 대상 객체의 메소드를 사용할수 있다.

B. 스프링의 XML 및 configuration 파일안에 설정된 Bean
: 생성 패턴(Creational Patterns) > 싱글턴 패턴(Singleton pattern)

  • 싱글턴 패턴(Singleton pattern) 이란?

자원이 단지 하나만 필요한 객체들이 있다.
그 객체는 다양한 클래스와 쓰레드 컨택스트 내에서 공유되어 사용될수 있어야 한다.
예를 들어 현재 JVM에 할당받은 힙메모리 크기를 가져오고 싶다고 할때, 여러 개의 객체를 만들 필요가 없다. 왜냐하면 어플리케이션 입장에서 바라보는 JVM은 단지 하나일 뿐이기 때문이다. 이렇게 단일 리소스 환경에 대해서 객체로서 표현하는 방법이 싱글턴(Singleton)이다.

  • 싱글톤의 장점
  1. 객체가 단일하게 생성되므로 메모리가 절약된다.
  2. new 를 사용하는 과정을 줄이므로 라인 수를 줄일 수 있다.
  3. 메모리를 공유하는 좋은 방법이다.
  • 싱글톤의 단점
  1. 메모리를 할당하고, 한번 사용하면 언제 해제될지 모른다.
  2. OOP의 컨셉과 맞지 않는 면이 있다.
    OOP에서는 모든 객체를 생명주기가 존재하는 것으로 인식하는 경향이 있다.
  • 스프링의 싱글턴 패턴

스프링은 빈을 등록할 때 범위(scope)를 지정할 수 있는데 디폴트가 싱글턴(Singleton) 이다.
그 외에도 prototype, request, session 이 있습니다.

  1. prototype : 컨테이너에 빈을 요청할 때마다 매번 새로운 객체를 만든다.
  2. request : HTTP 요청 하나당 하나의 객체를 만든다.
  3. session : HTTP 세션별로 인스턴스화 되며 세션이 끝나면 소멸된다.

스프링에서 싱글턴을 저장하고 관리해주는 역할은 applicationContext 하는데,
Singleton Registry, IOC 컨테이너, 스프링 컨테이너, 빈 팩토리 등으로 불린다.

스프링의 핵심 컨테이너의 빈 관리를 담당하는 BeanFactory 의 핵심 구현 클래스는 DefaultListableBeanFactory이다.

대부분의 애플리케이션 컨텍스트는 바로 이 클래스를 BeanFactory 로 사용하는데, 핵심 구현 클래스인 DefaultListableBeanFactory 가 구현하고 있는 인터페이스의 한가지가 바로 SingletonRegistry 이다.

  • 스프링은 왜 Bean을 Singleton 으로 생성할까?

스프링에서 하나의 요청을 처리하기 위해서는 Presentation Layer, Business Layer, Data Access Layer 등 다양한 기능을 담당하는 객체들이 계층형을 이루고 있는데, 클라이언트 요청 마다 각 로직을 담당하는 객체를 만들어 사용한다면, GC가 있더라도 메모리 부하가 올 수 있다.

이 때문에 엔터프라이즈 분야에서는 서비스 오브젝트(Service Object) 라는 개념을 사용해 왔는데 서블릿은 Java 엔터프라이즈 기술의 가장 기본이 되는 서비스 오브젝트라고 할 수 있다.

서블릿은 대부분의 멀티 스레딩 환경에서 싱글턴을 동작하며, 서블릿 클래스 하나당 하나의 객체를 생성하여, 클라이언트 요청 처리를 담당하는 스레드 들이 해당 객체를 공유해서 사용한다.

  • 스프링은 어떻게 빈을 싱글턴으로 생성할까?

스프링은 어노테이션 설정만으로 IoC 컨테이너(applicationContext) 에 제어권을 넘겨줌으로써 손쉽게 빈(Bean)을 싱글턴으로 생성하여 사용할 수 있다.

Component-scan 대상이 되는 어노테이션들 @Repository, @Service, @Controller, @Component 등 을 사용하면 된다.

C. 스프링의 XML 및 configuration 파일안에 설정된 Bean
: 행동 패턴(Behavioral Patterns) > 템플릿 메서드 패턴 (Template method pattern)

  • 템플릿 메소드 패턴(Template Method) 이란?

잘 구현되어 있는 알고리즘을 상황에 맞게 조금씩 다르게 확장해야 하는 경우에 유용한 패턴이다.
완성되지 않는 뼈대(Skeleton) 라는 개념으로, 뼈대에 피와 살을 붙여 살아있는 로직이 된다.

이렇게 하기 위해 하나의 객체를 추상클래스와 구현클래스로 나눈다.
로직의 기본 골격이 되는 메인 부분은 추상클래스의 일반 메소드로 둔다.(템플릿 메소드)
구현클래스별로 달라질수 있는 부분의 로직에 대해서만 구현클래스에 둔다.

결과적으로 유저가 추상클래스의 템플릿 메소드를 호출하면, 템플릿 메소드는 주 로직들을 처리하다가
구현별로 달라지는 부분에서 구현클래스의 함수를 호출하는 방식으로 구현된다.

유저 -> 추상클래스의 템플릿 메소드 -> 구현클래스의 abstract에 대한 구현 메소드

일반적인 클래스 상속구조에서는 구현클래스에서 추상클래스의 메소드를 이용하는 경우가 많은데, 템플릿 메소드에서는 반대로 추상클래스에서 구현클래스의 메소드를 참조한다.

전략패턴에서 알고리즘부분을 외부객체를 통해 수행하는 것과는 정반대의 방식이라고 볼수 있다.

  • 템플릿 패턴의 장점
  1. 코드중복을 크게 줄일수 있다.
  2. 자식객체의 롤을 최대한 줄임으로서 핵심로직에 집중한다.
  3. 쉽게 자식 객체를 추가, 확장해 나갈수 있다.
  • 템플릿 패턴의 단점
  1. 구현 클래스가 구현해야 하는 abstact method가 너무 많으면 관리가 곤란하다.
  2. 반드시 추상 클래스의 템플릿 메서드에서 구현클래스의 메서드를 부르는 식으로 구성해야 한다.(상위->하위)
    따라서, 자칫하면 혼선이 생기기 쉽다.
  • 스프링의 템플릿 메소드 패턴
    jdbc를 사용할때 사용하는 jdbcTemplate,
    jmx 사용시에 사용되는 jmxTemplate,
    jpa를 사용할때 사용하는 jpaTemplate 등의 클래스는 템플릿 메소드 패턴방식으로 구현되어있다.

따라서 핵심로직 처리 부분은 부모 클래스에 대부분 구현되어있고, 상속받은 자식 클래스는 최종적인 부분만 각기다르게 구현하는 방식이다.

jdbcTemplate는 커넥션 완전히 정리하기, 결과반복, 예외처리, 트랜잭션 처리등의 복잡하고 세부적인 사항들을 스프링 내부에서 알아서 처리해주는 코드가 미리 들어가 있고, 또한 다양한 generic을 지원한다.

최종적인 db binding( SELECT, UPDATE, INSERT, DELETE등의 쿼리를 전달하는 부분)을 jdbcTemplate이 템플릿이 호출한다.

Observer 패턴을 이용하여 코드 작성해 보기

  • 옵서버 패턴(observer pattern)이란?

객체의 상태 변화를 관찰하는 관찰자(옵저버) 목록을 객체에 등록하여 상태 변화가 있을 때마다
메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다.
주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다.

  • 대표적으로 활용할 수 있는 예
  1. 외부에서 발생한 이벤트(사용자 입력같은)에 대한 응답. 이벤트 기반 프로그래밍
  2. 객체의 속성 값 변화에 따른 응답, 이벤트 연쇄의 원인
  • Observer 패턴을 이용하여 코드 작성

Observer 인터페이스 정의

public interface Observer {
    public void update(String title, String news);
}

인터페이스 내에 update 메서드를 정의한다.

Publisher 인터페이스 정의

public interface Publisher {
    public void registerObserver(Observer observer);
    public void removeObserver(Observer observer);
    public void notifyObservers();
}

인터페이스 내에 아래 3가지 메서드를 정의하여 publisher이 행동 할 수 있는 것들을 명시한다.

NewsPublisher 클래스 구현

public class NewsPublisher implements Publisher{
    private ArrayList<Observer> observers;
    private String title;
    private String news;

    public NewsPublisher() {
        observers = new ArrayList<>();
        title = null;
        news = null;
    }

    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        for(Observer observer : observers) {
            observer.update(title, news);
        }
    }

    public void setNews(String title, String news) {
        this.title = title;
        this.news = news;
        notifyObservers();
    }
}

Publisher 인터페이스를 직접 이용하는 클래스로 observers를 ArrayList에 모을 수 있게 한다.

NewsSubscriber 클래스 구현

public class NewsSubscriber implements Observer{
    private String observerName;
    private String news; private
    Publisher publisher;

    public NewsSubscriber(String subscriber, Publisher publisher) {
        this.observerName = subscriber;
        this.publisher = publisher;
        publisher.registerObserver(this);
    }

    @Override
    public void update(String title, String news) {
        this.news = title + "!!! " + news; display();
    }

    private void display() {
        System.out.println("=== " + observerName + " 수신 내용 ===\n" + news + "\n");
    }
}

Observer 인터페이스를 직접 이용하는 클래스.
NewsSubscriber이 생성될 때 자신의 publisher이 누구인지 지정해준다.

그때 자신의 publisher의 registerObserver에 의해 등록이 될 것이고
추후 publisher의 notifyObservers()에서 publisher의 모든 observer가 내용을 전달받게 된다.

ObserverPatternMain 클래스 구현

public class ObserverPatternMain {
    public static void main(String[] args) {
        NewsPublisher newsPublisher = new NewsPublisher();
        NewsSubscriber newsSubscriber1 = new NewsSubscriber("옵저버1", newsPublisher);
        NewsSubscriber newsSubscriber2 = new NewsSubscriber("옵저버2", newsPublisher);
        newsPublisher.setNews("특보", "옵저버 패턴이 만들어졌습니다.");
        newsPublisher.setNews("정정", "옵저버 패턴으로 내용이 정정됨을 알립니다.");
        newsPublisher.removeObserver(newsSubscriber1);
        newsPublisher.setNews("속보", "누군가 구독 해제를 했습니다.");
    }
}

실행 결과

profile
Back-end Developer
post-custom-banner

0개의 댓글