디자인패턴 적용 Guide

디자인 패턴이란 특정 컨텍스트 내에서 주어진 문제에 대한 해결책이다. 어떤 컨텍스트 내에서 일련의 제약조건에 의해 영향을 받을 수 있는 문제에 봉착했다면, 그 제약조건 내에서 목적을 달성하기 위한 해결책을 찾아낼 수 있는 디자인을 적용한다. 디자인 패턴의 과다한 사용은 불필요하게 복잡한 코드를 초래할 수 있다. 항상 가장 간단한 해결책으로 목적을 달성할 수 있도록 하고, 반드시 필요할 때만 디자인 패턴을 적용하자. 코딩할 때 어떤 패턴을 사용하고 있는지 주석으로 적어주자. 클래스와 메서드 이름을 만들 때도 사용 중인 패턴이 분명하게 드러날 수 있도록 해보자. 다른 개발자들이 그 코드를 볼 때 무엇을 어떻게 구현했는지 훨씬 빠르게 이해할 수 있다.

1. 코드 작성 Guide

  • 변경 되는 코드는 변경 되지 않는 코드로 부터 분리한다.
  • 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다.
  • 상속보다는 구성요소로 객체를 활용해 행동 인터페이스를 구현한다.
  • 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨한 결합 디자인을 사용한다.
  • 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있게한다.(OCP : Open-Closed Principle)
  • 추상화된 것에 의존하도록 만들어라. 구상클래스에 의존하도록 만들지 않도록 한다.
  • 최소 지식 원칙 - 정말 친한 친구하고만 얘기하라. 다음 네 종류의 객체의 메서드만 호출한다.
    • 객체 자체
    • 메서드에 매개변수로 전달된 객체
    • 그 메서드에서 생성하거나 인스턴스를 만든 객체
    • 그 객체에 속하는 구성 요소
  • 헐리우드 원칙 - 고수준 구성 요소가 저수준 구성요소에 의존 되어서는 안됨.
    • 예) 상위 클래스에서 하위 클래스 구성 요소를 호출하지 말아야함

2. 생성 관련 디자인 패턴

생성 관련 디자인 패턴으로 Factory, Builder, Prototype, Singleton 패턴이 있다. Factory 패턴과 Singleton 패턴에 대해 알아 보도록 한다.

팩토리 패턴(Factory Pattern)

  • 팩토리 메서드 패턴 :
    • 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다.
    • 클래스의 인스턴스를 만드는 일을 서브클래스에 맡긴다.
  • 제품을 생산하는 부분과 사용하는 부분을 분리시킬 수 있다.
  • 추상 팩토리 패턴 :
    • 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고 생성한다.
    • 구상 클래스는 서브 클래스에 의해 만들어진다.

싱글턴 패턴(Singleton Pattern)

  • 해당 클래스의 인스턴스 하나만 만들고, 어디서든지 그 인스턴스에 접근할 수 있도록(전역 접근) 하기 위한 패턴

3. 구조 디자인 패턴

Adapter, Bridge, Composite,Decorator, Facade, Flyweight, Proxy 패턴이 있다.

어댑터 패턴(Adapter Pattern)

  • 한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다. 어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.

컴포지트 패턴(Composite Pattern)

  • 객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층구조로 만들 수 있다. 이 패턴을 이용하면 클라이언트에서 개별 객체와 다른 객체들로 구성된 복합 객체(composite)를 똑같은 방법으로 다룰 수 있다.
  • 클라이언트에서 객체 컬렉션과 개별 객체를 똑같은 식으로 처리할 수 있다.
  • 예) 트리 구조의 패턴, 디렉토리 구조
  • 예) XMLObject 객체가 컴포지트 패턴을 구현한 게 아닐까

데코레이터 패턴(Decorator Pattern)

  • 객체에 추가적인 요건을 동적으로 첨가한다.

  • 데코레이터는 서브클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공한다.

  • 예: 스타버즈 커피

퍼사드 패턴(Facade Pattern)

  • 어떤 서브시스템의 일련의 인터페이스에 대한 통합된 인터페이스를 제공한다. 퍼사드에서 고수준 인터페이스를 정의하기 때문에 서브시스템을 더 쉽게 사용할 수 있다.
  • 서브시스템의 호출을 퍼사드에서 처리해준다. (기본 명령 호출 정도랄까…)
  • 일련의 클래스들에 대한 인터페이스를 단순화 시킨다.
  • 각 패턴별 차이점:
    • 데코레이터 패턴 : 인터페이스는 바꾸지 않고 책임(기능)만 추가
    • 어댑터 패턴 : 한 인터페이스를 다른 인터페이스로 변환
    • 퍼사드 패턴 : 인터페이스를 간단하게 바꿈

프록시 패턴(Proxy Pattern)

  • 어떤 객체에 대한 접근을 제어하기 위한 용도로 대리인이나 대변인에 해당하는 객체를 제공하는 패턴
  • 다른 객체를 대변한느 객체를 만들어서 주 객체에 대한 접근을 제어할 수 있다.
  • 원격프록시(remote proxy): 원격 객체에 대한 접근 제어
  • 클라이언트와 원격 객체 사이에서 데이터 전달을 관리
  • 가상프록시(virtual proxy): 생성하기 힘든(인스턴스를 만드는 데 많은 비용이 드는) 자원에 대한 접근 제어
  • 보호프록시(protection proxy): 접근 권한이 필요한 자원에 대한 접근 제어 호출하는 쪽의 권한에 따라서 객체에 있는 메소드에 대한 접근 제어
  • 방화벽 프록시: 일련의 네트워크 자원에 대한 접근 제어
  • 스마트 레퍼런스 프록시: 주 객체가 참조될 때마나 추가 행동을 제공. 객체에 대한 레퍼런스 개수를 세는 등
  • 캐싱 프록시: 비용이 많이 드는 작업의 결과를 임시로 저장 웹 서버 프록시 또는 컨텐츠 관리 및 퍼블리싱 시스템 등에서 사용
  • 동기화 프록시: 여러 스레드에서 주 객체에 접근하는 경우 안전하게 작업을 처리할 목적(분산 환경 등에서 사용)
  • 복잡도 숨김 프록시: 복잡한 클래스들의 집합에 대한 접근을 제어하고 복잡도를 숨겨줌 퍼사드 프록시라고도 함. 프록시에서는 접근을 제어하지만 퍼사드 패턴에서는 대체 인터페이스만 제공
  • 지연 복사 프록시: 클라이언트에서 필요로 할 때까지 객체가 복사되는 것을 지연시킴으로써 객체의 - 복사 제어
  • 아래 객체들은 모두 클라이언트와 객체 사이에 끼여들어서 요청을 전달한다.
    • 데코레이터 패턴: 클래스에 새로운 행동을 추가하기 위한 용도
    • 어댑터 패턴: 다른 객체의 인터페이스를 바꿔주기 위한 용도
    • 프록시 패턴: 어떤 클래스에 대한 접근을 제어하기 위한 용도
  • java.reflect.Proxy 에 기능이 내장되어 있다.

4. 행동 디자인 패턴

행동 디파인 패턴에는 Chain of responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor 패턴이 있다.

커맨드 패턴(Command Pattern)

  • 요구 사항을 객체로 캡슐화 할 수 잇으며, 매개변수를 써서 여러 가지 다른 요구 사항을 집어넣을 수 있다. 또한 요청 내역을 큐에 저장하거나 로그로 기록할 수도 잇으며, 작업취소 기능도 지원 가능하다. 예: 리모콘
  • 서블릿의 doGet(), doPost() 또는 스트럿츠의 Action() 메서드도 커맨드 패턴

이터레이터 패턴(Iterator Pattern)

  • 컬렉션 구현 방법을 노출시키지 않으면서도 그 잡합체 안에 들어있는 모든 항목에 접근할 수 있게 해주는 방법을 제공한다.
  • 컬렉션의 구현을 드러내지 않으면서 컬렉셔네 있는 모든 객체들에 대해 반복작업할 수 있다.

옵저버 패턴(Observer Pattern)

  • 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의한다.
  • 주제(Subject) & 옵저버(Observer)
  • Observable & Observer: Observable 에 register, remove, notify 가 있고, Observer 에 update 가 있다. (notify 에서 update 를 호출)
  • 예: 신문 구독 서비스, 기상관측 시스템

스트래티지 패턴(Strategy Pattern)

  • 알고리즘군을 정의하고 각각을 캐슐화하여 교환해서 사용할 수 있도록 만든다.

  • 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

  • 구성을 사용한다.

  • 일반적으로 서브클래스를 만드는 방법을 대신하여 유연성을 극대화하기 위한 용도로 쓰인다.

  • 예: QuarkBehavior & FlyBehavior

스테이트 패턴(State Pattern)

  • 객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있다. 마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다.
  • 상태 전환의 흐름을 결정하는 코드를 어느 쪽에 집어넣는지 잘 고려해야 한다. (상태 객체인지, Context 객체인지)
  • 각 상태를 클래스로 캡슐화함으로써 나중에 변경시켜야 하는 내용을 국지화시킬 수 있다.
  • 스트래티지 패턴 : 어떤 클래스의 인스턴스를 만들고 그 인스턴스에게 어떤 행동을 구현하는 전략 객체를 건내준다.
  • 스테이트 패턴 : 컨텍스트 객체를 생성할 때 초기 상태를 지정해주는 경우 이후로는 컨텍스트 객체가 알아서 상태를 변경.

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

탬플릿 메소드 패턴은 탬플릿 기능을 구현할 수 있는 패턴이며, 구현 방법은 상위 추상클래스에 탬플릿에 해당하는 메소드가 정의되어 있고, 추상클래스를 상속하는 하위 클래스는 상위 클래스에서 확장(extend)되거나 대체(replace)하는 기능을 수행한다. 크게 다음과 같이 구성한다.

  • 추상 클래스
    • 구체적인 동작(함수) 정의하거나, 하위 클래스에 대체 되어야할 추상 함수 정의
  • 서브 클래스 : 추상 클래스가 결정한 동작을 받아서 결정하며, 일부는 Overring하여 서브 클래스에서 처리(일부 재정의)

위와 같이 해두면 다음과 같은 장점이 있다.

  • 중복 코드를 줄일 수 있다.
    • A,B,C의 공통 로직 처리는 abstract 클래스에서 처리
  • 하위 클래스의 역할을 줄여 핵심 로직 관리가 용이하다.
    • 하위 클래스에서 재정의 하지 않은 메서드는 핵심 로직이 아님
    • 상위 추상클래스에 정의된 함수만 핵심 로직이됨
  • 모듈 분리에 유리하다.
    • 추상 클래스가 제어를 대신 하게 할 수 있다. (추상화된 것에 의존 하도록 하라)

그런데 다음과 같은 단점도 있다.

  • 공통 로직 처리가 많아질 수 록 추상 클래스가 비대해질 수 있다.

아래 소스 코드 예를 보자.

car.java public abstract class car { public abstract void start(); public void stop(){ System.out.println(“stop”); }

    public void startstop(){
        start();
        stop();
    }
}

bus.java

public class bus extends car {
    public void start(){  // @Override가 아님에 유의
        System.out.println("bus is starting");
    }
}

main.java

public class main {
    public static void main(String[] args) {
        car myBus=new bus();
        myBus.startstop();
    }
}

car는 bus의 상위클래스이자 추상클래스이다. main.java를 보면 bus라는 클래스를 추상클래스에 대입함으로서, 추상클래스가 제어를 대리하도록 하고 있다. 제어의 역전IoC(Inverse of Control)이란 어떠한 일을 하도록 권한을 상위 추상 클래스에 특정 클래스의 제어 권한을 넘긴다는 개념이다. 이 개념을 보면 탬플릿 메소드 패턴의 개념과 일치함을 확인할 수 있다.