728x90

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

템플릿 메소드 패턴은 알고리즘의 골격을 정의한다. 템플릿 메소드를 사용하면 알고리즘의 일부 단계를 서브클래스에서 구현할 수 있으며, 알고리즘의 구조는 그대로 유지하면서 알고리즘의 특정 단계를 서브클래스에서 재정의할 수도 있다.

  • 서브클래스에서 언제든 필요할 때마다 알고리즘을 가져다가 쓸 수 있도록 캡슐화 해보자
  • 할리우드 디자인 원칙을 알아보자

개요 - 커피와 홍차 만들기

커피와 홍차는 둘 다 카페인을 가지고 있고, 가장 중요한 점은 매우 비슷한 방법으로 만들어진다는 것이다.

커피 만드는 법

  1. 물을 끓인다.
  2. 끓는 물에 커피를 우려낸다.
  3. 커피를 컵에 따른다.
  4. 설탕과 우유를 추가한다.

홍차 만드는 법

  1. 물을 끓인다.
  2. 끓는 물에 찻잎을 우려낸다.
  3. 홍차를 컵에 따른다.
  4. 레몬을 추가한다.

Coffee 클래스와 Tea 클래스 만들기

이제 커피와 홍차를 만드는 클래스를 준비해 보자.

public class Coffee {

    public void prepareRecipe() {
        boilWater();
        brewCoffeeGrinds();
        pourInCup();
        addSugarAndMilk();
    }

    private void boilWater() {
        System.out.println("물 끓이는 중");
    }

    private void brewCoffeeGrinds() {
        System.out.println("필터로 커피를 우려내는 중");
    }

    private void pourInCup() {
        System.out.println("컵에 따르는 중");
    }

    private void addSugarAndMilk() {
        System.out.println("설탕과 우류를 추가하는 중");
    }

}
public class Tea {

    public void prepareRecipe() {
        boilWater();
        steepTeaBag();
        pourInCup();
        addLemon();
    }

    private void boilWater() {
        System.out.println("물 끓이는 중");
    }

    private void steepTeaBag() {
        System.out.println("필터로 커피를 우려내는 중");
    }

    private void pourInCup() {
        System.out.println("컵에 따르는 중");
    }

    private void addLemon() {
        System.out.println("설탕과 우류를 추가하는 중");
    }

}

조금 전에 Coffee 클래스에서 구현했던 방법과 비슷한 것을 느낄 수 있다.

두 번째와 네 번째 단계가 조금 다르지만 기본적으로 같다고 할 수 있다. 이렇게 공통적으로 코드가 중복된다면 디자인 수정을 고려해보자.

혹시 Coffee와 Tea 클래스는 거의 같으니까 두 클래스의 공통된 부분을 추상화해서 베이스 클래스로 만드는 것을 어떨까??


Coffee 클래스와 Tea 클래스 추상화하기

https://books.google.co.kr/books?id=Lw8LEAAAQBAJ&printsec=frontcover&hl=ko&source=gbs_ge_summary_r&cad=0#v=onepage&q&f=false

CaffeineBeverage

  • prepareRecipe() 메소드는 서브클래스마다 다르기 때문에 추상 메서드로 선언
  • boilWater()와 putInCup() 메서드는 두 클래스에서 공통으로 사용되므로 슈퍼클래스에 정의

서브 클래스(Coffee, Tea)

  • 서브 클래스는 prepareRecipe() 메소드를 오버라이드해서 음료 제조법을 구현
  • Coffee나 Tea 클래스에만 있던 메소드는 서브 클래스에 그대로 남김

추상화 방법 들여다보기

제조법을 다시 살펴보면 커피와 홍차 제조법의 알고리즘이 똑같다는 사실을 알 수 있다.

  1. 물을 끓인다.
  2. 뜨거운 물을 사용해서 커피 또는 찻잎을 우려낸다.
  3. 만들어진 음료를 컵에 따른다.
  4. 각 음료에 맞는 첨가물을 추가한다.

https://books.google.co.kr/books?id=Lw8LEAAAQBAJ&printsec=frontcover&hl=ko&source=gbs_ge_summary_r&cad=0#v=onepage&q&f=false

  • 2, 4 이 둘은 추상화되지 않았지만 똑같다. 서로 다른 음료에 적용될 뿐이다.
  • 이제 prepareRecipe() 까지 추상화하는 방법을 찾아보자. 생각해보면 커피를 필터로 우려내는 일과 티백을 물에 넣어서 홍차를 우려내는 일은 별로 다르지 않다. 사실 거의 같다고 볼 수 있기 때문에 brew() 메서드를 만들어서 커피를 우려내는 홍차를 우려내는 똑같은 메서드를 사용해보자.
  • 이와 마찬가지로 설탕과 우유를 추가하는 일이나 레몬을 추가하는 일도 마찬가지다. 음료에 첨가물을 넣는다는 사실 자체는 똑같기 때문이다. 따라서 addConfiments() 메소드를 양쪽에 사용해보자
public abstract class CaffeineBeverage {

    public void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    protected abstract void addCondiments();

    protected abstract void brew();

    private void boilWater() {
        System.out.println("물 끓이는 중");
    }

    private void pourInCup() {
        System.out.println("컵에 따르는 중");
    }

}
  • 이제 다시 만든 메소드를 prepareRecipe()에 넣어보자.
public class Coffee extends CaffeineBeverage {

    @Override
    protected void brew() {
        System.out.println("필터로 커피를 우려내는 중");
    }

    @Override
    protected void addCondiments() {
        System.out.println("설탕과 우류를 추가하는 중");
    }

}

------------------------------------------------------------------

public class Tea extends CaffeineBeverage{

    @Override
    protected void brew() {
        System.out.println("필터로 커피를 우려내는 중");
    }

    @Override
    protected void addCondiments() {
        System.out.println("설탕과 우류를 추가하는 중");
    }

}
  • 두 클래스에서 음료를 만드는 방법은 CaffeinBeverage에 의해 결정되므로 음료를 우려내는 brew()와 첨가물을 추가하는 addCondiments()를 수정하자.

추상화 과정 다시 살펴보기

https://books.google.co.kr/books?id=Lw8LEAAAQBAJ&printsec=frontcover&hl=ko&source=gbs_ge_summary_r&cad=0#v=onepage&q&f=false

조금 다른 방식으로 구현해야 하는 부분이 있긴 하지만, 만드는 방법이 사실상 똑같으므로 제조법을 일반화해서 베이스 클래스에 넣었다.

그 후 베이스 클래스에서 전체 처리 과정을 관리하며, 첫 번째와 세 번째 단계는 직접 처리하고 두 번째와 네 번째 단계는 Tea와 coffee 서브클래스에 의존한다.


템플릿 메소드 패턴 알아보기

지금까지 Coffee와 Tea 클래스에 템플릿 메소드 패턴을 적용했다고 할 수 있다. 템플릿 메소드는 CaffeinBeverage 클래스에 들어있다.

https://books.google.co.kr/books?id=Lw8LEAAAQBAJ&printsec=frontcover&hl=ko&source=gbs_ge_summary_r&cad=0#v=onepage&q&f=false

  • prepareRecipe()는 템플릿 메소드이다.
  • 템플릿 메소드란 어떤 알고리즘의 템플릿 역할을 하는 메서드이다.
  • 템플릿 내에서 알고리즘의 각 단계는 메소드로 표현한다.
  • 어떤 메소드는 해당 클래스에서 처리되기도 하고, 서브 클래스에서 처리되는 메소드도 있다.
  • 서브 클래스에서 구현해야 하는 메소드는 abstract로 선언해야 한다.

즉 템플릿 메소드는 알고리즘의 각 단계를 정의하며, 서브 클래스에서 일부 단계를 구현할 수 있도록 유도한다.


템플릿 메소드 패턴의 장점

기존 Tea & Coffee 클래스

  • 각 클래스가 각각 작업을 처리한다.
    • 두 클래스에서 각자 알고리즘을 수행
  • Coffee 와 Tea 클래스에 중복된 코드가 존재
  • 알고리즘이 바뀌면 서브클래스를 일일이 열어서 여러 군데를 고쳐야 한다.
  • 클래스 구조상 새로운 음료를 추가하려면 꽤 많은 일을 해야 한다.
  • 알고리즘 지식과 구현 방법이 여러 클래스에 분산되어 있다.

템플릿 메소드를 사용한 CaffeinBeverage

  • CaffeinBeverage 클래스에서 작업을 처리한다.
    • 알고리즘을 독점
  • 서브 클래스에서 코드를 재사용할 수 있다.
  • 알고리즘이 한 군데에 모여 있으므로 한 부분만 고치면 된다.
  • 다른 음료도 쉽게 추가할 수 있는 프레임워크를 제공한다.
    • 음료를 추가할 때 몇 가지 메소드만 더 만들면 된다.
  • CaffeinBeverage 클래스에 알고리즘 지식이 집중되어 있으며 일부 구현만서브클래스에 의존한다.

템플릿 메소드 패턴의 정의

이제 패턴의 정의와 특징을 자세히 알아보자.

간단하게 말하면 템플릿 메소드 패턴은 알고리즘의 템플릿(틀)을 만든다. 템플릿이란 일련의 단계로 알고리즘을 정의한 메소드이다. 여러 단계 가운데 하나 이상의 단계가 추상 메소드로 정의되며, 그 추상 메소드는 서브 클래스에서 구현된다. 이러면 서브 클래스가 일부분의 구현을 처리하게 하면서도 알고리즘의 구조는 바꾸지 않아도 된다.

클래스 다이어그램

https://books.google.co.kr/books?id=Lw8LEAAAQBAJ&printsec=frontcover&hl=ko&source=gbs_ge_summary_r&cad=0#v=onepage&q&f=false

  • 템플릿 메소드는 알고리즘을 구현할 때 primitiveOperation을 활용한다.
    • 알고리즘은 이 단계들의 구체적인 구현으로부터 분리되어 있다.
  • 서브 클래스가 알고리즘의 각 단계를 마음대로 건드리지 못하게 final로 선언합니다.
  • 추상 클래스 내에 구상 메소드로 정의된 단계도 있다. 해당 메소드는 fianl로 선언되었으므로 서브 클래스에서 오버라이드할 수 없다.
    • 이 메소드는 템플릿 메소드에서 직접 호출할 수도 있고
    • 서브클래스에서 호출해서 사용할 수도 있다.
  • 기본적으로 아무것도 하지 않는 구상 메소드를 정의할 수도 있다.
    • 이런 메소드를 후크(hook) 라고 부른다.
    • 서브 클래스에서 오버라이드할 수도 있지만, 반드시 그래야 하는건 아니다.

후크 알아보기

후크는 추상 클래스에서 선언되지만 기본적인 내용만 구현되어 있거나 아무 코드도 들어있지 않은 메소드이다.

이러면 서브 클래스는 다양한 위치에서 알고리즘에 끼어들 수 있다. 물론 그냥무시하고 넘어갈 수도 있다. 후크는 다양한 용도로 사용된다. 한 가지 사용법의 예시를 알아보자.

public abstract class CaffeineBeverage {

    public void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        if (customerWantsCondiments()) {
            addCondiments();
        }
    }

    protected abstract void addCondiments();

    protected abstract void brew();

    private void boilWater() {
        System.out.println("물 끓이는 중");
    }

    private void pourInCup() {
        System.out.println("컵에 따르는 중");
    }

    private boolean customerWantsCondiments() { // hook
        return true;
    }

}
  • 별 내용이 없는 기본 메소드를 구현해 놓았다.
  • 해당 메소드는 true만 리턴할 뿐 다른 작업은 하지 않는다.
  • 이 메소드는 서브클래스에서 필요할 때 오버라이드할 수 있는 메소드이므로 후크이다.

후크 활용하기

후크를 사용하려면 서브 클래스에서 후크를 오버라이드해야 한다.

위의 예제에서는 알고리즘의 특정 부분을 처리할지 말지를 결정하는 용도로 후크를 사용했다.

즉, 음료에 첨가물을 추가할지 말지를 결정하는 메소드다.

public class CoffeeWithHook extends CaffeineBeverage { // 후크를 오버라이드해서 원하는 기능을 추가

    @Override
    protected void brew() {
        System.out.println("필터로 커피를 우려내는 중");
    }

    @Override
    protected void addCondiments() {
        System.out.println("설탕과 우류를 추가하는 중");
    }

    @Override
    protected boolean customerWantsCondiments() {
        String answer = getUserInput();
        if (answer.toLowerCase().startsWith("y")) {
            return true;
        }
        return false;
    }

    private String getUserInput() {
        String answer = null;
        System.out.println("커피에 우유와 설탕을 넣을까요? (y/n)? ");
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        try {
            answer = br.readLine();
        } catch (IOException exception) {
            System.out.println("io exception");

        }
        if (answer == null) {
            return "no";
        }
        return answer;
    }
}

후크 코드 테스트

public class BeverageTestDrive {

    public static void main(String[] args) {
        CoffeeWithHook coffeeWithHook = new CoffeeWithHook();
        System.out.println("커피 준비 중");
        coffeeWithHook.prepareRecipe();
    }

}

===================================================================

커피 준비 중
물 끓이는 중
필터로 커피를 우려내는 중
컵에 따르는 중
커피에 우유와 설탕을 넣을까요? (y/n)? 
y
설탕과 우류를 추가하는 중

Q) 템플릿을 만들 때 추상 메소드를 써야할 때와 후크를 써야할 때를 어떻게 구분할 수 있을까??

서브 클래스가 알고리즘의 특정 단계를 제공해야만 한다면 추상 메소드를 써야 한다. 알고리즘의 특정 단계가 선택적으로 적용된다면 후크를 쓰면 된다. 후크를 쓰면 서브클래스에서 필요할 때 후크를 구현할 수도 있지만, 꼭 구현해야 하는건 아니기 때문이다.

Q) 후크는 정확하게 어떤 용도로 쓰이는 걸까??

여러 가지 용도로 쓰인다. 알고리즘에서 필수적이지 않은 부분을 서브클래스에서 구현하도록 만들고 싶을 때 후크를 쓸 수 있다. 또한 템플릿 메소드에서 앞으로 일어날 일이나 막 일어난 일에 서브클래스가 반응할 수 있도록 기회를 제공하는 용도로도 쓰일 수 있다.

예를 들면, 내부적으로 특정 목록을 재정렬한 후에 서브 클래스에서 특정 작업을 수행하도록 싶을 때, justReOrderedList() 같은 이름을 가진 후크 메소드를 쓸 수도 있다. 또한 앞에 예제에서 봤듯이 서브클래스가 추상 클래스에서 진행되는 작업을 처리할지 말지 결정하게 하는 기능을 부여하는 용도로 후크를 쓸 수도 있다.

Q) 서브클래스에서 AbstractClass에 있는 모든 추상 메소드를 구현해야 할까??

그렇다. 모든 서브클래스에서 모든 추상 메소드를 정의해야 한다.

즉, 템플릿 메소드에 있는알고리즘의 단계 중에서 정의되지 않은 부분을 모두 채워 줘야 한다.

Q) 추상 메소드가 너무 많아지면 서브 클래스에서 일일이 추상 메소드를 구현해야 하니까 별로 좋지 않을 수 있지 않을까??

맞다. 템플릿 메소드를 만들 때는 그 점을 꼭 생각해 봐야 한다.

알고리즘의 단계를 너무 잘게 쪼개지 않는 것도 한 가지 방법이 될 수 있다. 하지만 알고리즘을 큼직한 몇 가지 단계로만 나누어 놓으면 유연성이 떨어진다는 단점도 있으니 잘 생각해서 결정해야 한다.

그리고 모든 단계가 필수는 아니라는 점도 기억하자. 필수가 아닌 부분을 후크로 구현하면 그 추상 클래스의 서브 클래스를 만들 때 부담이 조금 줄어들 것이다.


할리우드 원칙

디자인 원칙 중 할리우드 원칙이 있다. 이 원칙은 보통 다음과 같이 정의될 수 있다.

할리우드에서 배우들과 연락하는 것과 비슷하게, 슈퍼 클래스에서 모든 것을 관리하고 필요한 서브클래스를 불러서 써야 한다는 원칙이다. 이런 할리우드 원칙을 활용하면 의존성 부패(dependency rot)를 방지할 수 있다.

어떤 고수준 구성 요소가 저수준 구성 요소에 의존하고, 그 저수준 구성 요소는 다시 고수준 구성 요소에 의존하고, 그 고수준 구성 요소는 다시 또 다른 구성 요소에, 그 다른 구성 요소는 또 저수준 구성 요소에 의존하는 것과 같은 식으로 의존성이 복잡하게 꼬여있는 상황을 의존성이 부패했다고 부른다. 이렇게 의존성이 부패하면 시스템 디자인이 어떤 식으로 되어 있는지 아무도 알아볼 수 없다.

할리우드 원칙을사용하면, 저수준 구성 요소가 시스템에 접속할 수는 있지만 언제, 어떻게 그 구성 요소를 사용할지는 고수준 구성 요소가 결정한다.

즉 고수준 구성 요소가 저수준 구성 요소에게 “먼저 연락하지 마세요. 제가 먼저 연락 드리겠습니다.” 라고 이야기 하는 것과 같다.

https://books.google.co.kr/books?id=Lw8LEAAAQBAJ&printsec=frontcover&hl=ko&source=gbs_ge_summary_r&cad=0#v=onepage&q&f=false

  • 저수준 구성 요소도 컴퓨테이션에 참여할 수 있다.
  • 하지만 언제, 어떻게 쓰일지는 고수준 구성 요소가 결정한다.
    • 저수준 구성 요스는 절대 고수준 구성 요소를 직접 호출할 수 없다.

할리우드 원칙과 템플릿 메소드 패턴

할리우드 원칙과 템플릿 메소드 패턴의 관계는 쉽게 알 수 있다. 템플릿 메소드 패턴을 써서 디자인 하면 서브클래스에게 “우리가 연락할 테니까 먼저 연락하지마”라고 얘기하는 구조이기 대문이다.

디자인을 다시 한번 살펴보자

https://books.google.co.kr/books?id=Lw8LEAAAQBAJ&printsec=frontcover&hl=ko&source=gbs_ge_summary_r&cad=0#v=onepage&q&f=false

  • CaffeineBeverage 는 고수준 구성 요소이다.
    • 음료를 만드는 방법에 해당하는 알고리즘을 장악하고 있고, 메소드 구현이 필요한 상황에만 서브클래스를 불러낸다.
  • CaffeineBeverage 클래스의 클라이언트는 Tea, Coffee 같은 구상 클래스가 아닌 CaffeineBeverage 에 추상화되어 있는 부분에 의존한다. 이러면 전체 시스템의 의존성을 줄일 수 있다.
  • 서브 클래스는 자질 구레한 메소드 구현을 제공하는 용도로만 쓰인다.
  • Tea와 Coffee는 호출 당하기 전까지는 추상 클래스를 직접 호출하지 않는다.

Q) 할리우드 원칙과 의존성 뒤집기 원칙은 어떤 관계일까??

의존성 뒤집기 원칙은 될 수 있으면 구상 클래스 사용을 줄이고 추상화된 것을 사용해야 한다는 원칙이다. 할리우드 원칙은 저수준 구성 요소가 컴퓨테이션에 참여하면서도 저수준 구성 요소와 고수준 계층 간 의존을 없애도록 프레임워크는 구성 요소를 구축하는 기법이다.

따라서 두 원칙은 객체를 분리한다는 하나의 목표를 공유하지만, 의존성을 피하는 방법에 있어서 의존성 뒤집기 원칙이 훨씬더 강하고 일반적인 내용을 담고 있다.

할리우드 원칙은 저수준 구성요소를 다양하게 사용할수 있으면서도 다른 클래스가 구성 요소에 너무 의존하지 않게 만들어주는 디자인 구현 기법을 제공한다.

Q) 저수준 구성 요소에서는 고수준 구성 요소에 있는 메소드를 호출할 수 없는 것일까??

그렇지 않다. 사실 저수준 구성 요소에서도 상속 계층구조 위에 있는 클래스가 정의한 메소드를, 상속으로 호출하는 경우도 빈번하게 있다. 하지만 저수준 구성 요소와 고수준 구성 요소 사이에 순환 의존성이 생기지 않도록 해야한다.


자바 api 속 템플릿 메서드 패턴 알아보기

바로 Arrays.sort 메소드이다.

private static void legacyMergeSort(Object[] a) {
    Object[] aux = a.clone();
    mergeSort(aux, a, 0, a.length, 0);
}

private static void mergeSort( // 템플릿 메소드
        Object[] src,
    Object[] dest,
    int low,
    int high,
    int off
    ) {
      // Insertion sort on smallest arrays
      if (length < INSERTIONSORT_THRESHOLD) {
          for (int i=low; i<high; i++)
              for (int j=i; j>low &&
                       ((Comparable) dest[j-1]).compareTo(dest[j])>0; j--) // 템플릿 메소드를 완성하려면 comapreTo() 메소드를 구현해야 한다.
                  swap(dest, j, j-1); // Arrays 클래스에 이미 정의되어 있는 구상 ㅇ메솓,
          return;
      }
        // ...

}

만약 배열 속 오리 클래스들을 정렬해야 한다면 Arrays에 있는 정렬용 템플릿 메소드에서 알고리즘을 제공하지만, 오리 비교 방법은 comapreTo() 메소드로 구현해야 한다.

하지만 템플릿 메소드 패턴을 배울 때 서브 클래스에서 일부 단계를 구현한다고 배웠는데, 해당 예제에서는 서브 클래스를 만들지 않고 있다.

sort() 메소드는 정적 메소드이고 정적 메소드 자체는 크게 문제가 되지 않는다. 슈퍼 클래스에 들어있다고 생각하면 되기 때문이다. 하지만 sort() 자체가 특정 슈퍼클래스에 정의되어 있는게 아니므로 sort() 메소드가 우리가 comapreTo() 메소드를 구현했는지 알아낼 수 있는 방법이 필요하다는 점이다.

이러한 문제를 해결하기 위해 Comaprable 인터페이스가 도입되었다. 이제 해당 인터페이스를 구현하기만 하면 문제가 해결 된다.

public class Duck implements Comparable<Duck> {

    private final String name;
    private final int weight;

    public Duck(String name, int weight) {
        this.name = name;
        this.weight = weight;
    }

    public String getName() {
        return name;
    }

    public int getWeight() {
        return weight;
    }

    @Override
    public int compareTo(Duck otherDuck) {
        return Integer.compare(this.weight, otherDuck.getWeight());
    }
}

Q) 오리 정렬 코드가 정말 템플릿 메소드 패턴일까? 억지스러운 것일까?

Arrays.sort() 메소드는 분명 템플릿 메소드 패턴의 정의의 방법을 사용하지 않고 있지만 실전에서 패턴을 적용하는 방법이 책에 나와 있는 방법과 완전히 같을 수는 없다. 주어진 상황과 구현ㄴ상 제약조건에 맞게 고쳐서 적용해야 한다.

일반적으로 자바에서는 배열의 서브클래스를 만들 수 없지만, 어떤 배열에서도 정렬 기능을 사용할 수 있도록 만들어야 했다. 그래서 sort() 메소드를 정적 메소드로 정의한 다음, 대소를 비교하는 부분은 정렬될 객체에서 구현되도록 만든 것이다.

온전한 템플릿 메소드라고 할 순 없겠지만 템플릿 메소드 패턴의 기본 정신을 충실히 따르고 있다. 또한 서브클래스를 만들어야 한다는 제약 조건을 없앰으로써 오히려 더 유연하면서 유용한 정렬 메소드를 만들었다.

Q) 구현해놓은 것을 보니 템플릿 메소드 패턴 보다는 전략 패턴과 가까워 보이는데 템플릿 메소드 패턴이라고 볼 수 있는 근거는 무엇일까?

전략 패턴에서 객체 구성을 사용하니까 어떻게 보면 일리가 있지만 전략 패턴에서는 구성할 때 사용하는 클래스에서 알고리즘을 완전히 구현한다.

Arrays 클래스에서 사용하는 알고리즘은 불완전한다. comapreTo() 를 다른 클래스에서 제공해 줘야 하기 때문이다. 따라서 템플릿 메소드 패턴이 적용되었다고 볼 수 있다.


개념이 비슷해 보이는 패턴

  • 템플릿 메소드 패턴 : 알고리즘의 어떤 단계를 구현하는 방법을 서브클래스에서 결정
  • 전략 패턴 : 바꿔 쓸 수 있는 행동을 캡슐화하고, 어떤 행동을 사용할지는 서브클래스에 맡김
  • 팩토리 메소드 패턴 : 구상 클래스의 인스턴스 생성을 서브클래스에서 결정

템플릿 메소드 vs 전략 패턴

두 가지 모두 같은 요구사항을 구현할 수 있지만 템플릿 메소드 패턴은 알고리즘의 개요를 정의하는 역할을 한다. 진짜 작업 중 일부는 서브클래스에서 처리하며 각 단계마다 다른 구현을 사용하면서도 알고리즘 구조 자체는 그대로 유지할 수 있다. 따라서 알고리즘을 더 강하게 제어할 수 있고, 코드 중복도 거의 없다. 만약 알고리즘이 전부 똑같고 코드 한 줄씩만 다르다면 템플릿 메서드 패턴을 사용한 클래스가 전략 패턴을 사용한 클래스보다 효율적일 수 있다.

하지만 전략 패턴은 상속이 아닌 객체 구성을 사용하기 때문에 상속에서 오는 단점들이 없고 훨씬 더 유연하다는 장점이 있다. 부모 같이 어떤 것에도 의존하지 않고 알고리즘을 전부 알아서 구현할 수 있기 때문이다.


핵심 정리

  • 템플릿 메소드는 알고리즘의 단계를 정의하며 일부 단계를 서브클래스에서 구현하도록 할 수 있다.
  • 템플릿 메소드 패턴은 코드 재사용에 큰 도움이 된다.
  • 템플릿 메소드가 들어있는 추상 클래스는 구상 메소드, 추상 메소드, 후크를 정의할 수 있다.
  • 추상 메소드는 서브클래스에서 구현한다.
  • 후크는 추상 클래스에 들어있는 메소드로 아무 일도 하지 않거나 기본 행동만을 정의한다.
    • 서브 클래스에서 후크를 오버라이드 할 수 있다.
  • 할리우드 원칙에 의하면, 저수준 모듈을 언제 어떻게 호출할지는 고수준 모듈에서 결정하는 것이 좋다.
  • 템플릿 메소드 패턴은실전에서도 꽤 자주 쓰이지만 반드시 교과서적인 방식으로 적용되진 않는다.
  • 전략 패턴과 템플릿 메소드 패턴은 모두 알고리즘을 캡슐화하는 패턴이지만 전략 패턴은 상속을, 템플릿 메소드 패턴은 구성을 사용합니다.
  • 팩토리 메소드 패턴은 특화된 템플릿 메소드 패턴입니다.

객체지향 도구 상자

  • 객체지향의 기초(4요소)
    • 캡슐화
    • 상속
    • 추상화
    • 다형성
  • 객체지향 원칙
    • 바뀌는 부분을 캡슐화한다.
    • 상속보다는 구성을 활용한다.
    • 구현이 아닌 인터페이스(super type)에 맞춰서 프로그래밍한다.
    • 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
    • 클래스는 확장에 대해서는 열려 있지만 변경에 대해서는 닫혀 있어야 한다. (OCP)
    • 추상화된 것에 의존하라. 구상 클래스에 의존하지 않도록 한다.
    • 진짜 절친에게만 이야기해야 한다.
    • 먼저 연락하지 마세요. 저희가 연락 드리겠습니다.
  • 객체지향 패턴
    • 스트레지티 패턴 : 알고리즘군을 정의하고 각각의 알고리즘을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 전략을 사용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.
    • 옵저버 패턴 : 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.
    • 데코레이터 패턴: 객체에 추가 요소를 동적으로 더할 수 있습니다. 데코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.
    • 추상 팩토리 패턴 : 서로 연관된, 또는 의존적인 객체들로 이루어진 제품군을 생성하기 위한 인터페이스를 제공한다. 구상 클래스는 서브 클래스에 의해 만들어진다.
    • 팩토리 메소드 패턴 : 객체를 생성하기 위한 인터페이스를 만든다. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하도록 한다. 팩토리 메소드를 이용하면 인스턴스를 만드는 일을 서브클래스로 미룰 수 있다.
    • 싱글턴 패턴 : 클래스 인스턴스가 하나만 만들어지도록 하고, 그 인스턴스에 대한 전역 접근을 제공한다.
    • 커맨드 패턴 : 요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화할 수 있다. 이러면 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있다.
    • 어댑터 패턴 : 특정 클래스 인터페이스를 클라이언트에서 요구하는 다른 인터페이스로 변환한다. 인터페이스가 호환되지 않아 같이 쓸 수 없었던 클래스를 사용할 수 있게 도와준다.
    • 퍼사드 패턴 : 서브시스템에 있는 일련의 인터페이스를 통합 인터페이스로 묶어 준다. 또한 고수준 인터페이스도 정의하므로 서브시스템을 더 편리하게 사용할 수 있다.
    • 템플릿 메소드 패턴 : 알고리즘의 골격을 정의한다. 템플릿 메소드를 사용하면 알고리즘의 일부 단계를 서브클래스에서 구현할 수 있으며, 알고리즘의 구조는 그대로 유지하면서 알고리즘의 특정 단계를 서브클래스에서 재정의할 수도 있다.
728x90
728x90

어댑터 패턴 & 퍼사드 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

어댑터 패턴 : 특정 클래스 인터페이스를 클라이언트에서 요구하는 다른 인터페이스로 변환한다. 인터페이스가 호환되지 않아 같이 쓸 수 없었던 클래스를 사용할 수 있게 도와준다.

퍼사드 패턴 : 서브시스템에 있는 일련의 인터페이스를 통합 인터페이스로 묶어 준다. 또한 고수준 인터페이스도 정의하므로 서브시스템을 더 편리하게 사용할 수 있다.


개요 - 어댑터

https://secretroute.tistory.com/entry/Head-First-Design-Patterns-제7강-Adapter-패턴과-Facade-패턴

우리 주변에서 볼 수 있는 어댑터의 역할은 전원 소켓의 인터페이스를 플러그에서 필요로 하는 인터페이스로 바꿔 준다고 할 수 있다. 객체지향 어댑터도 똑같이 어떤 인터페이스를 클라이언트에서 요구하는 형태로 적응시키는 역할을 한다.


객체지향 어댑터

어떤 소프트웨어 시스템에서 새로운 업체에서 제공한 클래스 라이브러리를 사용해야 하는데 그 업체에서 사용하는 인터페이스가 기존에 사용하던 인터페이스와 다르다고 가정해 보자.

https://codingsmu.tistory.com/59

그런데 기존 코드를 바꿔서 이 문제를 해결할 수 없는 상황이고, 업체에서 공급받은 클래스도 변경할 수 없다면 어떻게 해야 할까?? 바로 새로운 업체에서 사용하는 인터페이스를 기존에 사용하던 인터페이스에 적응시켜 주는 클래스를 만들면 된다.

https://secretroute.tistory.com/entry/Head-First-Design-Patterns-제7강-Adapter-패턴과-Facade-패턴

어댑터는 기존 시스템에서 사용하던 인터페이스를 구현해서 새로운 업체에서 제공한 클래스에 요구 내역을 전달할 수 있다. 어댑터는 클라이언트로부터 요청을 받아서 새로운 업체에서 제공하는 클래스를 클라이언트가 받아들일 수 있는 형태의 요청으로 변환해 주는 중개인 역할을 하는 것이다.


어댑터 사용방법

어댑터를 어떻게 사용하는지 한번 살펴보자

public interface Duck {

    void quack();
    void fly();

}

public class MallardDuck implements Duck {

    @Override
    public void quack() {
        System.out.println("quack");
    }

    @Override
    public void fly() {
        System.out.println("fly");
    }
}
public interface Turkey {

    void gobble();
    void fly();

}

public class WildTurkey implements Turkey {

    @Override
    public void gobble() {
        System.out.println("gobble");
    }

    @Override
    public void fly() {
        System.out.println("short fly");
    }
}

Duck 객체가 모자라서 Turkey 객체를 대신 사용해야 하는 상황이라고 가정해 보자. 물론 인터페이스가 다르기에 Turkey 객체를 바로 사용할 수는 없다. 이 때 필요한 것이 어댑터이다.

// 우선 적응시킬 형식의 인터페이스를 구현해야 한다. 즉 클라이언트에서 원하는 인터페이스를 구현해야 한다.
public class TurkeyAdapter implements Duck { 


    private final Turkey turkey;

    public TurkeyAdapter(Turkey turkey) { // 그리고 기존 형식 객체의 레퍼런스가 필요한다.
        this.turkey = turkey;
    }

    @Override
    public void quack() {
        turkey.gobble();
    }

    /*
     두 인터페이스에 모두 fly가 있지만 turkey의 fly() 메소드를 Duck의 fly() 메소드에 대응시키도록 작성 
     */
    @Override
    public void fly() {
        for (int i = 0; i < 5; i++) {
            turkey.fly();
        }
    }
}
public class DuckTestDrive {

    public static void main(String[] args) {
        Duck duck = new MallardDuck();
        Turkey turkey  = new WildTurkey();
        Duck turkeyAdapter = new TurkeyAdapter(turkey);

        System.out.println(" turkey said that");
        turkey.gobble();
        turkey.fly();

        System.out.println("\n duck said that");
        testDuck(duck);

        System.out.println("\n turkeyAdapter said that");
        testDuck(turkeyAdapter);
    }

    private static void testDuck(Duck duck) {
        duck.quack();
        duck.fly();
    }

}

=======================================================
turkey said that
gobble
short fly

duck said that
quack
fly

turkeyAdapter said that
gobble
short fly
short fly
short fly
short fly
short fly
  • 클라이언트의 testDuck() 메소드는 오리와 칠면조를 전혀 구분하지 못한다.

어댑터 패턴

https://codingsmu.tistory.com/59

이제 어댑터가 어떤 식으로 작동하는지 살펴보자

  1. 클라이언트는 타깃 인터페이스에 맞게 구현되어 있으며, 타깃 인터페이스로 메소드를 호출해서 어댑터에 요청을 보낸다.
  2. 어댑터는 타깃 인터페이스를 구현하며, 어댑티 인스턴스를 가지고 있다. 어댑터는 어댑티 인터페이스로 그 요청을 어댑티에 관한(하나 이상의) 메소드 호출로 변환한다.
  3. 클라이언트는 호출 결과를 받긴 하지만 중간에 어댑터가 있다는 사실을 모르므로, 클라이언트와 어댑티는 서로 분리되어 있다.

Q. 어댑터가 얼마나 적응시켜 줘야 할까? 대형 타깃 인터페이스를 구현해야 한다면 할 일이 정말 많아지지 않을까?

어댑터 구현은 타깃 인터페이스로 지원해야 하는 인터페이스의 크기에 비례해서 복잡해진다. 하지만 다른 대안이 없다. 클라이언트에서 호출하는 부분을 새로운 인터페이스에 맞춰서 고치려면 정말 많은 부분을 고려해야 하고, 코드도 많이 고쳐야 한다. 이런 방법보다는 모든 변경 사항을 캡슐화할 어댑터 클래스 하나만 제공하는 방법이 더 나을 것이다.

Q. 하나의 어댑터는 하나의 클래스만 감싸야 할까?

어댑터 패턴은 하나의 인터페이스를 다른 인터페이스로 변환하는 용도로 쓰인다. 하나의 어댑터에서 타깃 인터페이스를 구현하려고 2개 이상의 어댑티를 감싸야 하는 상황도 생길 수 있다. 사실 이런 내용은 퍼사드 패턴과 관련이 있으므로 퍼사드 패턴 때 다시 보자.

Q. 시스템에 오래된 부분과 새로 만든 부분이 섞여 있으면 어떻게 해야할까?? 어떤 곳에는 어댑터를 사용하고 다른 곳에서 어댑터로 감싸지 않은 인터페이스를 사용하면 헷갈리지 않을까?

이런 상황에서는 두 인터페이스를 모두 지원하는 다중 어댑터(Two Way Adapter)를 만들면 된다. 다중 어댑터로 필요한 인터페이스를 둘 다 구현해서 어댑터가 기존 인터페이스와 새로운 인터페이스 역할을 할 수 있게 하면 된다.


어댑터 패턴의 정의

이제 어댑터 패턴의 정의를 알아보자.

이 패턴을 사용하면 호환되지 않는 인터페이스를 사용하는 클라이언트를 그대로 활용할 수 있다. 인터페이스를 변환해 주는 어댑터를 만들면 되기 때문이다. 이러면 클라이언트와 구현된 인터페이스를 분리할 수 있으며, 변경 내역이 어댑터에 캡슐화되기에 나중에 인터페이스가 바뀌더라도 클라이언트를 바꿀 필요가 없다.

클래스 다이어그램

https://secretroute.tistory.com/entry/Head-First-Design-Patterns-제7강-Adapter-패턴과-Facade-패턴

클라이언트

  • 클라이언트는 타킷 인터페이스만 볼 수 있다.

어댑터

  • 어댑터에서 타깃 인터페이스를 구현한다.
  • 어댑터는 어댑티로 구성되어 있다.

어댑티

  • 모든 요청은 어댑티에 위임된다.

어댑터 패턴은 여러 객체지향 원칙을 반영하고 있다. 어댑티를 새로 바뀐 인터페이스로 감쌀 때는 Composition을 사용한다. 이런 접근번은 어댑티의 모든 서브클래스에 어댑터를 쓸 수 있다는 장점이 있다.

그리고 어댑터 패턴은 클라이언트를 특정 구현이 아닌 인터페이스에 연결한다. 서로 다른 백엔드 클래스로 변환시키는 여러 어댑터를 사용할 수도 있다. 이렇게 인터페이스를 기준으로 코딩했기에 타깃 인터페이스만 제대로 유지한다면 나중에 다른 구현을 추가하는 것도 가능하다.


객체 어댑터와 클래스 어댑터

사실 어댑터에는 두 종류가 있다. 하나는 객체 어댑터, 다른 하나는 클래스 어댑터이다.

지금까지 본 예제와 다이어그램 모두 객체 어댑터에 해당하는 내용들이다. 그렇다면 클래스 어댑터란 무엇이고 왜 살펴보지 않았을까? 클래스 어댑터 패턴을 쓰려면 상속이 필요한데 자바에서는 다중 상속이 불가능하므로 자바에서는 불가능하다. 하지만 다중 상속이 가능한 언러를 사용하다 보면 클래스 어댑터를 써야 할 때도 있으니 클래스 다이어그램을 살펴보자

https://secretroute.tistory.com/entry/Head-First-Design-Patterns-제7강-Adapter-패턴과-Facade-패턴

어댑티를 적응시킬때 구성을 사용하는 대신, 어댑터를 어댑티와 타깃 클래스의 서브클래스로 만든다. 상속을 사용하는 클래스 어댑터에 비해 객체 어댑터는 composition을 사용하므로 상속을 통한 코드 분량을 줄이지는 못하지만, 어댑티한테 필요한 일을 시키는 코드만 작성하면 되기 때문에 작성해야할 코드가 적고 유연성을 확보할 수 있다.


어댑터 패턴 실전 적용

https://swk3169.tistory.com/255

Enumeration

Enumeration을 리턴하는 elements() 메소드가 구현되어 있었던, 초기 컬렉션 형식(Vector, Stack, Hashtable 등)은 Enumeration 인터페이스를 이용하면 컬렉션 내에서 각 항목이 관리되는 방식에는 신경 쓸 필요 없이 컬렉션의 모든 항목에 접근이 가능하다.

Iterator

최근에는 Enumeration과 마찬가지로 컬렉션에 있는 일련의 항목들에 접근할 수 있게 해 주면서 항목을 제거할 수 도 있게 해 주는 iterator라는 인터페이스를 이용하기 시작했다.

Enumeration vs Iterator

Enumeration 인터페이스를 사용하는 구형 코드를 다뤄야 할 때도 가끔 있지만 새로운 코드를 만들 때는 Iterator만 사용하는 것이 좋다. 이때 어댑터 패턴을 적용해보자.

https://codingsmu.tistory.com/59

  • Itrerator : 타깃 인터페이스
  • Enumeration : 어댑티 인터페이스
  • 그런데 Iterator의 remove() 메소드는 Enumeration에는 이런 기능을 제공하는 메소드가 없다.
    • 어떻게 해야할까??

어댑터 디자인하기

클래스 다이어그램은 다음과 같다. 먼저 타깃 인터페이스를 구현하고, 어댑티 객체로 구성된 어댑터를 구현해야 한다. hasNext()와 next() 메소드는 타깃에서 업대티로 바로 연결된다.

https://codingsmu.tistory.com/59

remove()는 어떻게 처리할까?

  • 어댑터 차원에서 완벽하게 작동하는 remove() 메소드의 구현 방법은 없다. 따라서 그나마 좋은 방법은 런타임 예외를 던지는 것이다.
  • Iterator 인터페이스는 remove()는 default method로 UnsupportedOperationException을 던지고 있다.

이처럼 메소드가 일대일로 대응되지 않는 상황에서는 어댑터를 완벽하게 적용할 수 없다. 클라이언트는 예외 발생 가능성을 염두에 두고 있어야 하기 때문이다. 하지만 클라이언트에서 주의를 기울이고, 어댑터 문서를 잘 만들어 두면 괜찮을 것이다.

EnumerationIteraotr

public class EnumerationIterator implements Iterator<Object> {

    private final Enumeration<?> enumeration;

    public EnumerationIterator(Enumeration<?> enumeration) {
        this.enumeration = enumeration;
    }

    @Override
    public boolean hasNext() {
        return enumeration.hasMoreElements();
    }

    @Override
    public Object next() {
        return enumeration.nextElement();
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

퍼사드 패턴

지금까지 어댑터 패턴을 써서 어떤 클래스의 인터페이스를 클라이언트가 원하는 인터페이스로 변환하는 방법을 어댑터 패턴을 이용하여 구현했다.

이제 조금 다른 이유로 인터페이스를 변경하는 또 다른 패턴을 알아보자. 바로 퍼사드 패턴이다.

퍼사드 패턴은 인터페이스를 단순하게 바꾸려고 인터페이스를 변경하다. 하나 이상의 클래스 인터페이스를 깔끔하면서도 효과적인 퍼사드(겉모양, 외관)으로 덮어주기 때문이다.

데코레이터 vs 어댑터 vs 퍼사드

해당 패턴 모두 객체를 감싸고 있는 공통점을 가지고 있다. 하지만 모두 사용하는 용도가 다르다.

  • 데코레이터는 인터페이스는 바꾸지 않고 감싸고 있는 객체의 행동과 책임을 확장하는 용도로 사용한다.
  • 어댑터는 하나의 인터페이스를 다른 인터페이스로 변환하는 용도로 사용한다.
  • 퍼사드는 인터페이스를 간단하게 변경하는 용도로 사용한다.

개요 - 홈시어터 만들기

패턴을 알아보기 전에 영화나 tv 시리즈 몰아보기가 유행에 따라 각광받고 있는 홈시어터를 구축해보자. 스트리밍 플레이어, 프로젝터, 자동 스크린, 서라운드 음향, 팝콘 기계 등 클래스들이 서로 복잡하게 얽혀 있다.

https://invincibletyphoon.tistory.com/22

영화를 보기위한 일련의 작업

이제 영화를 보려고 하지만 영화를 보려면 몇 가지 일을 더해야 한다.

  1. 팝콘 기계를 켠다
  2. 팝콘을 튀긴다.
  3. 조명을 어둡게 조절한다.
  4. 스크린을 내린다.
  5. 프로젝터를 켠다.
  6. 프로젝터 입력을 스트르밍 플레이어로 설정한다.
  7. 프로젝터를 와이드 스크린 모드로 전환한다.
  8. 앰프를 켠다.
  9. 앰프 입력을 스트리밍 플레이어로 설정한다.
  10. 앰프를 서라운드 음향 모드로 전환한다.
  11. 앰프 볼륭을 중간인 5로 설정한다.
  12. 스트리밍 플레이어를 켠다.
  13. 영화를 재생한다.

이제 이 작업들을 처리하기 위한 어떤 클래스와 메소드가 필요한지 살펴보자.

popper.on();
popper.pop();

lights.dim(10);
screen.down();
projector.on();
projector.setInput(player);
projector.wideScreenMode();

amp.on();
amp.setDvd(player);
amp.setSurroundSound();
amp.setVolume(5);

player.on();
player.play(movie);

클래스가 6개나 필요하고, 만약 영화가 끝나면 어떻게 해야할까?, 방금 했던 일을 전부 역순으로 처리해야 하지 않을까? 다른 라디오나 시스템이 업그레이드하면 이런 복잡한 작동 방법을 또 배워야 하지 않을까?

  • 이렇게 복잡한 일을 퍼사드 패턴으로 간단하게 처리할 수 있는지 알아보자

퍼사드 작동 원리

쓰기 쉬운 인터페이스를 제공하는 퍼사드 클래스를 구현함으로써 복잡한 시스템을 훨씬 편리하게 사용할 수 있다. 물론 기존의 시스템을 직접 건드리고 싶다면 기존 인터페이스를그대로 사용하면 된다.

  1. 홈시어터 시스템용 퍼사드를 만들어보자.
    • watchMovie()와 같이 몇 가지 간단한 메소드만 들어있는 HomeTheaterFacade 클래스를 새로 만들어야 한다.
  2. 퍼사드 클래스는 홈시어터 구성 요소를 하나의 서브시스템으로 간주한다.
    • watchMovie() 메서드는 서브시스템의 메소드를 호출해서 필요한 작업을 처리한다.
  3. 이제 클라이언트 코드는 서비스시템이 아닌 홈시어터 퍼사드에 있는 메서드를 호출한다.
    • watchMovie() 메서드만 호출하면 조명, 스트리밍 플레이어, 앰프 등 알아서 준비된다.
  4. 퍼사드를 쓰더라도 서브시스템에 여전히 직접 접근할 수 있다.
    • 서브시스템 클래스의 고급 기능이 필요하면 언제든지 사용 가능하다.

Q. 퍼사드로 서브시스템 클래스를 캡슐화하면 저수준 기능을 원하는 클라이언트는 어떻게 서브시스템 클래스에 접근할 수 있을까?

퍼사드 클래스는 서브시스템 클래스를 캡슐화하지 않는다. 서브시스템의 기능을 사용할 수 있는 간단한 인터페이스를 제공할 뿐이다. 클라이언트에서 특정 인터페이스가 필요하다면 서브시스템 클래스를 그냥 사용하면 된다. 이점이 퍼사드 클래스의 대표적인 장점이다. 단순화된 인터페이스를 제공하면서도, 클라이언트에서 필요로 한다면 시스템의 모든 기능을 사용할 수 있도록 해줍니다.

Q. 퍼사드에서 기능을 추가하거나 각각의 요청을 서브시스템에 그대로 전달하기도 할까?

퍼사드는 단순화된 서브시스템의 기능을 활용하게 해주는 일 외에도 ‘스마트’한 기능을 알아서 추가한다. 예를 들어, 홈시어터 퍼사드는 새로운 행동을 구현하지는 않지만, 팝콘을 튀기기 전에 팝콘 기계를 켜야 한다는 사실을 알고 있습니다. 그래서 팝콘 기계를 알아서 킨다. 그리고 각 구성 요소를 켜고 적절한 모드를 선택하는 것도 알아서 잘할 정도로 ‘스마트’하다.

Q. 하나의 서브시스템에는 하나의 퍼사도만 만들수 있을까?

그렇지 않다. 특정 서브시스템에 대해 만들 수 있는 퍼사드의 개수에는 제한이 없다.

Q. 더 간단한 인터페이스를 만들 수 있다는 점 말고 퍼사드의 또 다른 장점은 없을까?

퍼사드를 사용하면 클라이언트 구현과 서브시스템을 분리할 수 있다. 예를 들어 홈시어터 시스템을 업그레이드 하기로 가정해보자. 이런 경우 인터페이스가 크게 달라질 수 있을 것이다. 만약 클라이언트를 퍼사드로 만들었다면 클라이언트 코드는 고칠 필요 없이 퍼사드만 바꾸면 된다.

Q. 어댑터는 한 클래스만 감싸고 퍼사드는 여러 클래스를 감쌀 수 있는 것일까?

그렇지 않다. 어댑터 패턴은 하나 이상의 클래스 인터페이스를 클라이언트에서 필요로 하는 인터페이스로 변환한다. 클라이언트가 여러 클래스를 사용할 수도 있기 대문이다. 반대로 퍼사드도 꼭 여러 클래스를 감싸야만 하는 건 아니다. 아주 복잡한 인터페이스를 가지고 있는 단 하나의 클래스에 대한 퍼사드를 만들 수도 있다.

어댑터와 퍼사드의 차이점은 감싸는 클래스의 개수에 있는 것이 아니라 용도에있다. 어댑터 패턴은 인터페이스를 변경해서 클라이언트에서 필요로 하는 인터페이스로 적응시키는 용도로 쓰인다. 반면 퍼사드 패턴은 어떤 서브시스템에 대한 간단한 인터페이스를 제공하는 용도로 쓰인다.


홈시어터 퍼사드

public class HomeTheaterFacade {

    // composition 부분, 사용하고자 하는 서브시스템의 모든 구성 요소가 인스턴스 변수 형태로 저장된다.
    private final Amplifier amp;
    private final Tuner tuner;
    private final StreamingPlayer player;
    private final Projector projector;
    private final TheaterLights lights;
    private final Screen screen;
    private final PopcornPopper popper;

    public HomeTheaterFacade(Amplifier amp, Tuner tuner, StreamingPlayer player,
        Projector projector,
        TheaterLights lights, Screen screen, PopcornPopper popper) {
        this.amp = amp;
        this.tuner = tuner;
        this.player = player;
        this.projector = projector;
        this.lights = lights;
        this.screen = screen;
        this.popper = popper;
    }

    public void watchMovie(String movie) {
        System.out.println("영화 볼 준비 중");
        popper.on();
        popper.pop();

        lights.dim(10);
        screen.down();
        projector.on();
        projector.wideScreenMode();
        projector.setInput(player);

        amp.on();
        amp.setDvd(player);
        amp.setSurroundSound();
        amp.setVolume(5);

        player.on();
        player.play(movie);
    }

    public void endMovie() {
        System.out.println("홈시어터 끄는 중");
        popper.off();
        lights.on();
        screen.up();
        projector.off();
        amp.off();
        player.stop();
        player.off();
    }
}
  • 각 서브시스템에 들어있는 구성요소에게 위임하며 단순화된 인터페이스를 제공한다.
public class HomeTheaterTestDrive {

    public static void main(String[] args) {
        // 구성 요소 초기화
        // 지금은 구성 요소를 직접 생성하지만 보통은 클라이언트에 퍼사드가 주어지므로 직접 구성 요소를 생성하지 않아도 된다.

        Amplifier amp = new Amplifier();
        Tuner tuner = new Tuner();
        StreamingPlayer player = new StreamingPlayer();
        Projector projector = new Projector();
        TheaterLights lights = new TheaterLights();
        Screen screen = new Screen();
        PopcornPopper popper = new PopcornPopper();
        HomeTheaterFacade homeTheater = new HomeTheaterFacade(
            amp,
            tuner,
            player,
            projector,
            lights,
            screen,
            popper
        );
        // 단순화된 인터페이스를 사용
        homeTheater.watchMovie("king kong");
        homeTheater.endMovie();
    }

}

퍼사드 패턴의 정의

퍼사드 패턴을 사용하려면 어떤 서브시스템에 속한 일련의 복잡한 클래스를 단순하게 바꿔서 통합한 클래스를 만들어야 한다. 다른 패턴과 달리 퍼사드 패턴은 상당히 단순한 편이다. 복잡한 추상화 같은 게 필요 없다. 하지만 퍼사드 패턴을 사용하면 클라이언트와 서브시스템이 서로 긴밀하게 연결되지 않아도 되고, 최소 지식 객체지향 원칙을 준수하는데도 도움이 된다.

퍼사드 패턴의 정의는 다음과 같다.

여기서 가장 중요한 점은 패턴의 용도이다. 정의를 보면 퍼사드 패턴은 단순화된 인터페이스로 서브시스템을 더 편리하게 사용하려고 쓰인다는 사실을 알 수 있다. 퍼사드 패턴의 클래스 다이어그램에서도 이 사실을 확인할 수 있다.

클래스 다이어그램

https://secretroute.tistory.com/entry/Head-First-Design-Patterns-제7강-Adapter-패턴과-Facade-패턴


최소 지식 원칙

최소 지식 원칙(Principle of Least Knowledge)에 따르면 객체 사이의 상호자용은 될 수 있으면 아주 가까운 ‘친구’ 사이에서만 허용하는 편이 좋다. 이 원칙은 보통 다음과 같이 정의될 수 있다.

그런데 이게 정확히 무슨 소리일까? 시스템을 디자인할 때 어떤 객체든 그 객체와 상호작용을 하는 클래스의 개수와 상호작용 방식에 주의를 기울여야 한다는 뜻이다.

이 원칙을 잘 따르면 여러 클래스가 복잡하게 얽혀 있어서, 시스템의 한 부분을 변경했을 때 다른 부분까지 줄줄이 고쳐야 하는 상황을 미리 방지할 수 있다. 여러 클래스가 서로 복잡하게 의존하고 있다면 관리하기도 힘들고, 남들이 이해하기 어려운 불안정한 시스템이 만들어진다.

친구를 만들지 않고 다른 객체에 영향력 행사하기

그런데 어떻게 하면 여러 객체와 친구가 되는 것을 피할 수 있을까?

이 원칙은 친구를 만들지 않는 4개의 가이드라인을 제시한다.

  1. 객체 자체
  2. 메소드에 매개변수로 전ㄴ달된 객체
  3. 메소드를 생성하거나 인스턴스를 만든 객체
  4. 객체에 속하는 구성 요소

해당 가이드라인에 따르면 다른 메소드를 호출해서 리턴받은 객체의 메소드를 호출하는 일도 바람직 하지 않다. 따라서 꽤 까다로운 가이드라인이다. 메소드를 호출한 결과로 리턴받은 객체에 들어있는 메소드를 호출하면 다른 객체의 일부분에 요청하게 되고, 직접적으로 알고 지내는 객체의 수가 늘어나는 단점이 있다.

이러한 상황에서 최소 지식 원칙을 따르려면 객체가 대신 요청하도록 만들어야 한다. 그러면 그 객체의 한 구성 요소를 알고 지낼 필요가 없어지고 친구의 수를 줄이는 데도 도움이 된다.

Before

public float getTemp() {
        Thermometer thermometer = station.getThermometer();
        return thermometer.getTemperature();
}
  • station으로 부터 thermometer 객체를 받은 다음, 그 객체의 getTemperature() 메소드를 직접 호출

After

public float getTemp() {
        return station.getTemperature();
}
  • 최소 지식 원칙을 적용해서 thermometer 에게 요청을 전달하는 메소드를 station 클래스에 추가
  • 의존해야 하는 클래스의 개수를 줄인다.

절친에게만 메소드 호출하기

다음은 자동차를 나타내는 Car 클래스이다. 이 클래스르 살펴보면 최소 지식 원칙을 따르면서 메소드를 호출하는 방법을 어느 정도 파악할 수 있다.

public class Car {

    Engine engine; // 해당 클래스의 구성요소, 구성요소의 메소드는 호출해도 된다.

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start(Key key) {            // 매개변수로 전달된 객체의 메소드는 호출 가능하다.
        Doors doors = new Doors();          // 새로운 객체를 생성, 해당 객체의 메소드 호출 가능
        boolean authorized = key.turns();   // 매개변수로 전달된 객체
        if (authorized) {
            engine.start();                 // 이 객체의 구성 요소를 대상으로 메소드 호출 가능
            updateDashboardDisplay();       // 객체 내에 있는 메소드 호출 가능
            doors.lock();                   // 직접 생성하거나 인스턴스를 만든 객체의 메소드 호출 가능
        }
    }

    private void updateDashboardDisplay() {
        // update display
    }
}

Q. 데메테르의 법칙이라는 것도 있던데, 최소 지식 원칙과 어떤 관계일까?

데메테르의 법칙과 최소 지식 원칙은 완전히 똑같은 말이다. 하지만 좀 더 직관적이고 법칙이라는 단어가 없는 최소 지식 원칙을 선호한다. 모든 원칙은 상황에 따라서 적절하게 따라야 한다.

Q. 최소 지식 원칙도 단점이 있을까?

물론 존재한다. 이 원칙을 잘 따르면 객체 사이의 의존성을 줄일 수 있으며 소프트웨어 관리가 더 편해지지만, 메소드 호출을 처리하는 ‘래퍼’ 클래스를 더 만들어야 할 수도 있다. 그러면 시스템이 복잡해지고, 개발 시간도 늘어나고, 성능도 떨어진다.


핵심 정리

  • 기존 클래스를 사용하려고 하는데 인터페이스가 맞지 않으면 어댑터를 쓰면 된다.
  • 큰 인터페이스와 여러 인터페이스를 단순하게 바꾸거나 통합해야 하면 퍼사드를 쓰면 된다.
  • 어댑터는 인터페이스를 클라이언트에서 원하는 인터페이스로 바꾸는 역할을 한다.
  • 퍼사드는 클라이언트를 복잡한 서브시스템과 분리하는 역할을 한다.
  • 어댑터를 구현할 때는 타깃 인터페이스의 크기와 구조에 따라 코딩해야 할 분량이 결정된다.
  • 퍼사드 패턴에서드는 서브시스템으로 퍼사드를 만들고 진짜 작업은 서브클래스에 맡긴다.
  • 어댑터 패턴에는 객체 어댑터 패턴과 클래스 어댑터 패턴이 있다.
    • 클래스 어댑터를 사용하려면 다중 상속이 가능해야 한다.
  • 한 서브시스템에 퍼사드를 여러 개 만들어도 된다.
  • 어댑터는 객체를 감싸서 인터페이스를 바꾸는 용도로, 데코레이터는 객체를 감싸서 새로운 행동을 추가하는 용도로, 퍼사드는 일련의 객체를 감싸서 단순하게 만드는 용도로 쓰인다.

객체지향 도구 상자

  • 객체지향의 기초(4요소)
    • 캡슐화
    • 상속
    • 추상화
    • 다형성
  • 객체지향 원칙
    • 바뀌는 부분을 캡슐화한다.
    • 상속보다는 구성을 활용한다.
    • 구현이 아닌 인터페이스(super type)에 맞춰서 프로그래밍한다.
    • 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
    • 클래스는 확장에 대해서는 열려 있지만 변경에 대해서는 닫혀 있어야 한다. (OCP)
    • 추상화된 것에 의존하라. 구상 클래스에 의존하지 않도록 한다.
    • 진짜 절친에게만 이야기해야 한다.
  • 객체지향 패턴
    • 스트레지티 패턴 : 알고리즘군을 정의하고 각각의 알고리즘을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 전략을 사용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.
    • 옵저버 패턴 : 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.
    • 데코레이터 패턴: 객체에 추가 요소를 동적으로 더할 수 있습니다. 데코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.
    • 추상 팩토리 패턴 : 서로 연관된, 또는 의존적인 객체들로 이루어진 제품군을 생성하기 위한 인터페이스를 제공한다. 구상 클래스는 서브 클래스에 의해 만들어진다.
    • 팩토리 메소드 패턴 : 객체를 생성하기 위한 인터페이스를 만든다. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하도록 한다. 팩토리 메소드를 이용하면 인스턴스를 만드는 일을 서브클래스로 미룰 수 있다.
    • 싱글턴 패턴 : 클래스 인스턴스가 하나만 만들어지도록 하고, 그 인스턴스에 대한 전역 접근을 제공한다.
    • 커맨드 패턴 : 요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화할 수 있다. 이러면 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있다.
    • 어댑터 패턴 : 특정 클래스 인터페이스를 클라이언트에서 요구하는 다른 인터페이스로 변환한다. 인터페이스가 호환되지 않아 같이 쓸 수 없었던 클래스를 사용할 수 있게 도와준다.
    • 퍼사드 패턴 : 서브시스템에 있는 일련의 인터페이스를 통합 인터페이스로 묶어 준다. 또한 고수준 인터페이스도 정의하므로 서브시스템을 더 편리하게 사용할 수 있다.
728x90
728x90

커맨드 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

커맨드 패턴을 이용하면 요구 사항을 객체로 캡슐화 할 수 있으며, 매개변수를 써서 여러 가지 다른 요구 사항을 집어넣을 수도 있다. 또한 요청 내역을 큐에 저장하거나 로그로 기록할 수도 있으며, 작업 취소 기능도 지원 가능하다.

  • 호출 캡슐화
  • 한 차원 높은 단계의 캡슐화인 메소드 호출을 캡슐화하는 것을 배워보자
  • 메소드 호출을 캡슐화 하면 계산 과정의 각 부분들을 결정화시킬 수 있끼 때문에, 계산하는 코드를 호출한 객체에서는 어떤 식으로 일을 처리해야 하는지에 대해 전혀 신경쓰지 않아도 된다.
  • 그 외에도 캡슐화된 메소드 호출을 로그 기록용으로 저장을 한다거나 취소 기능을 구현하기 위해 재사용하는 것과 같은 작업을 할 수 도 있다.
  • 요청하는 객체와 요청을 수행하는 객체를 분리하고 싶다면 커맨드 패턴을 사용하자

개요 - 홈 오토메이션 리모콘

리모컨 API 디자인을 해보자. 해당 리모컨에는 일곱 가지 프로그래밍이 가능한 슬롯과 각 슬롯에 대한 온오프 스위치가 있다. 각 슬롯은 서로 다른 가정용 기기에 연결할 수 있다. 리모컨에는 작업 취소 버튼도 장착되어 있다.

조명, 팬, 욕조, 오디오를 비롯한 각종 홈 오토메이션 장비들을 제어하기 위한 용도로 다양한 업체에서 공급 받은 자바 클래스들을 같이 받았다.

각 슬롯을 한 가지 기기 또는 하나로 엮여 있는 일련의 기기들에 할당할 수 있도록 리모컨을 프로그래밍하기 위한 API를 제작해보자.


제공 받은 클래스

https://faun.pub/head-first-design-patterns-using-go-5-encapsulating-invocation-the-command-pattern-2f8c0a79d1c7

제공 받은 클래스들을 살펴보자 리모컨에서 제어해야 하는 객체의 인터페이스에 대한 정보를 얻을 수 있을 것이다.

공통적인 인터페이스가 있는 것 같진 않다. 리모컨에는 on, off 버튼만 있지만, 가전제품 클래스에는 여러 메서드가 존재하고 더 큰 문제는 앞으로 이런 클래스들이 더 추가될 수 있다는 점이다.

따라서 리모컨 버튼을 누르면 자동으로 해야할 일을 처리할 수 있도록 하고 리모컨에서 제품 업체에게 전달받은 클래스에 대해 자세히 알 필요가 없도록 디자인을 진행해야 할 것 같다.

해결책 : 커맨드 패턴

이를 해결하기 위해 어떻게 해야할까??

  • 작업을 요청한 쪽과 작업을 처리하는 쪽을 분리시킨다.
  • 여기서는 리모컨이 작업을 요청하는 쪽, 업체에서 공급한 클래스의 인스턴스는 작업을 처리하는 쪽
  • 이를 위해 커맨드 객체를 추가하여 분리시킬수 있다.
    • 커맨드 객체는 특정 객체에 대한 특정 작업 요청을 캡슐화한다.
    • 따라서 버튼마다 커맨드 객체를 저장해 두면 사용자가 버튼을 눌렀을 때 커맨드 객체를 통해서 작업을 처리하도록 만든다.
    • 리모컨은 작업이 무엇인지 전혀 모르고 작업을 완료하기 위한 객체와 상호작용하는 방법을 알고 있는 커맨드 객체만 존재하므로, 리모컨은 벤더쪽 클래스와 분리된다.
  • 이러한 패턴을 커맨드 패턴 이라고 한다.

커맨드 패턴 다이어그램

https://faun.pub/head-first-design-patterns-using-go-5-encapsulating-invocation-the-command-pattern-2f8c0a79d1c7

클라이언트

  • 클라이언트는 커맨드 객체를 생성해야 한다.
  • 커맨드 객체는 리시버에 전달할 일련의 행동으로 구성된다.

리시버

  • 커맨드 객체에는 행동과 리시버에 대한 정보가 같이 들어 있다.

커맨드

  • 커맨드 객체에서 제공하는 메소드는 excute() 하나 뿐이다.
  • 이 메소드에는 행동을 캡슐화하며, 리시버에 있는 특정 행동을 처리하기 위한 메소드를 호출하기 위한 메서드이다.

인보커

  • 클라이언트에서는 인보커 객체의 setCommand() 메소드를 호출하는데, 이 때 커맨드 객체를 넘겨줍니다.
  • 그 커맨드 객체는 나중에 쓰으기 전까지 인보커 객체에 보관됩니다.
  • 인보커에서 커맨드 객체의 excute() 메소드를 호출하면 리시버에 있는 특정 행동을 하는 메소드가 호출된다.

인보커 로딩

  1. 클라이언트에서 커맨드 객체 생성
  2. setCommand()를 호출하여 인보커에 커맨드 객체를 저장
  3. 나중에 클라이언트에서 인보커한테 그 명령을 실행시켜 달라는 요청을 함

커맨드 패턴과 식당 비유해보기

  • 클라이언트 == 손님
  • 주문서 == 커맨드 객체
  • 주문받기(takeOrder()) == setCommand()
  • 웨이트리스 == 인보커 객체
  • 주문(orderUp()) == execute()
  • 주방장 == 리시버 객체

커맨드 객체

이제 첫 커맨드 객체를 만들어 보자.

Command 인터페이스 만들기

  • 커맨드 객체는 모두 같은 인터페이스를 구현해야 한다.
  • 해당 인터페이스에는 메소드가 하나 밖에 없다.
  • 일반적으로 execute() 이름을 많이 사용한다.
public interface Command {

    void execute();

}

이제 전등을 켜기 위한 커맨드 클래스를 구현해보자. 벤더사에서 제공한 클래스를 보니 Light 클래스에는 on(), off() 두 개의 메소드가 있다.

public class LightOnCommand implements Command {

    Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.on();
    }
}
  • 생성자에 이 커맨드 객체로 제어할 특정 전등에 대한 정보가 전달된다.
  • 해당 객체는 light라는 인스턴스 변수에 저장이 되며, execute() 메소드가 호출되면 light 객체가 바로 그 요청에 대한 리시버가 된다.
  • execute() 메소드에서는 리시버 객체에 있는 on() 메서드를 호출한다.

커맨드 객체 사용하기

이제 커맨드 객체를 써서 가정요 기기를 조작하기 위해 버튼이 하나 밖에 없는 리모콘이 있다고 가정하고 코드를 작성해보자.

public class SimpleRemoteControl { // 인보커

    Command slot;

    public SimpleRemoteControl() {
    }

    public void setCommand(Command command) {
        slot = command;
    }

    public void buttonWasPressed() {
        slot.execute();
    }
}
class SimpleRemoteControlTest { // 1

    public static void main(String[] args) {
        SimpleRemoteControl remoteControl = new SimpleRemoteControl(); // 2
        Light light = new Light(); // 3
        LightOnCommand lightOn = new LightOnCommand(light); // 4

        remoteControl.setCommand(lightOn); // 5
        remoteControl.buttonWasPressed();
    }

}
  • 1 : 클라이언트에 해당하는 부분(SimpleRemoteControlTest)
  • 2 : remoteControl 변수가 인보커 역할을 한다.
    • 필요한 작업을 요청할 때 사용할 커맨드 객체를 인자로 전달받는다.
  • 3 : 요청을 받아서 처리할 리시버인 Light 객체를 생성한다.
  • 4 : 커맨드 객체를 생성한다.
    • 이 때 리시버를 전달해 준다.
  • 5 : 커맨드 객체를 인보커한테 전달해 준다.


커맨드 패턴의 정의

이제 커맨드 패턴의 정의를 알아보고 더 자세히 살펴보자.

  • 커맨드 객체는 일련의 행동을 특정 리시버하고 연결시킴으로써 요구 사항을 캡슐화한 것이다.
    • 이렇게 하기 위해서 행동과 리시버를 한 객체에 집어넣고 execute()라는 메소드 하나만 외부에 공개하는 방법을 쓴다.
    • 이 메소드 호출에 의해서 리시버에서 일련의 작업이 처리된다.
    • 외부에서 볼 때는 어떤 객체가 리시버 역할을 하는지, 그 리시버에서 실제로 어떤 일을 하는지 알 수 없다. 그냥 execute() 메소드를 호출하면 요구 사항이 처리된다는 것만 알 수 있을 뿐이다.
    • 즉, 커맨드는 캡슐화된 요구사항이다.
      • 리시버 : action
      • execute() : recevier.action();
  • 명령을 통해서 객체를 매개변수화하는 예도 몇 가지 볼 수 있었다.
    • 리모컨 입장에서는 특정 인터페이수만 구현이 되어 있다면 그 커맨드 객체에서 실제로 어떤일을 하는지 신경 쓸 필요가 없다.
    • 인보커(리모컨에 있는 슬롯 등)에 매개변수를 써서 여러가지 요구사항을 전달할 수도 있다.
  • 커맨드 객체들을 써서 큐나 로그를 구현하거나 작업 취소를 할 수 있으며, 메타 커맨드 패턴이라는 것을 이용하여 명령들로 이루어진 매크로를 만들어서 여러 개의 명령을 한 번에 실행할 수도 있다.

커맨드 패턴 클래스 다이어그램

https://faun.pub/head-first-design-patterns-using-go-5-encapsulating-invocation-the-command-pattern-2f8c0a79d1c7

클라이언트

  • ConcreteCommand를 생성하고 Receiver를 설정한다.

인보커

  • 인보커에는 명령이 들어 있으며, execute() 메소드를 호출함으로써 커맨드 객체에게 특정 작업을 수행해 달라는 요구를 하게 된다.

리시버

  • 리시버는 요구 사항을 수항해기 위해 어떤 일을 처리해야 하는지 알고 있는 객체이다.

커맨드 인터페이스

  • 모든 커맨드 객체에서 구현해야 하는 인터페이스
  • 모든 명령은 execute() 메소드 호출을 통해 수행되며, 이 메소드에서는 리시버에 특정 작업을 처리하라는 지시를 전달한다.
  • 이 인터페이스를 보면 undo() 메소드도 들어있는데, 잠시 후에 알아보자

구상 커맨드

  • 구상 커맨드는 특정 행동과 리시버 사이를 연결해 준다.
  • 인보커에서 execute() 호출을 통해 요청을 하면 구상 커맨드 객체에서 리시버에 있는 메소드를 호출함으로써 그 작업을 처리한다.
  • execute() → receiver.action();

슬롯에 명령 할당하기

이제 리모컨의 각 슬롯에 명령을 할당해보자. 이제 리모컨이 인보커가 되는 것이다.

사용자가 버튼을 누르면 그 버튼에 상응하는 커맨드 객체의 execute() 메소드가 호출되고, 그러면 리시버(vendor class)에서 특정 행동을 하는 메소드가 실행될 것이다.

  • 각 슬롯마다 커맨드 객체가 할당된다.
  • 사용자가 버튼을 누르면 해당 커맨드 객체의 execute() 메소드가 호출된다.
  • execute() 메소드에서는 리시버로 하여금 특정 작업을 처리하도록 지시한다.
public class RemoteControl {

    private static final int SLOT_SIZE = 7;
    Command[] onCommands;
    Command[] offCommands;

    public RemoteControl() {
        offCommands = new Command[SLOT_SIZE];
        onCommands = new Command[SLOT_SIZE];

        Command noCommand = new NoCommand();
        for (int i = 0; i < SLOT_SIZE; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;

        }
    }

    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    public void onButtonWasPushed(int slot) {
        onCommands[slot].execute();
    }

    public void offButtonWasPushed(int slot) {
        offCommands[slot].execute();
    }

    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("\n------ Remote Control -------\n");
        for (int i = 0; i < onCommands.length; i++) {
            stringBuilder
                .append("[slot ")
                .append(i)
                .append("] ")
                .append(onCommands[i].getClass().getName())
                .append("    ")
                .append(offCommands[i].getClass().getName())
                .append("\n");
        }
        return stringBuilder.toString();
    }
}
public class RemoteLoader {

    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl();

        Light livingRoomLight = new Light("Living Room");
        Light kitchenLight = new Light("Kitchen");
        CeilingFan ceilingFan = new CeilingFan("Living Room");
        GarageDoor garageDoor = new GarageDoor("Garage");
        Stereo stereo = new Stereo("Living Room");

        LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
        LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
        LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight);
        LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight);

        CeilingFanOnCommand ceilingFanOn = new CeilingFanOnCommand(ceilingFan);
        CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan);

        GarageDoorUpCommand garageDoorUp = new GarageDoorUpCommand(garageDoor);
        GarageDoorDownCommand garageDoorDown = new GarageDoorDownCommand(garageDoor);

        StereoOnWithCDCommand stereoOnWithCD = new StereoOnWithCDCommand(stereo);
        StereoOffCommand stereoOff = new StereoOffCommand(stereo);

        remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
        remoteControl.setCommand(1, kitchenLightOn, kitchenLightOff);
        remoteControl.setCommand(2, ceilingFanOn, ceilingFanOff);
        remoteControl.setCommand(3, stereoOnWithCD, stereoOff);

        System.out.println(remoteControl);

        remoteControl.onButtonWasPushed(0);
        remoteControl.offButtonWasPushed(0);
        remoteControl.onButtonWasPushed(1);
        remoteControl.offButtonWasPushed(1);
        remoteControl.onButtonWasPushed(2);
        remoteControl.offButtonWasPushed(2);
        remoteControl.onButtonWasPushed(3);
        remoteControl.offButtonWasPushed(3);
    }
}

NoCommand 객체

NoCommand 객체는 일종의 널 객체이다. 딱히 리턴할 객체는 없지만 클라이언트 쪽에서 null을 처리하지 않아도 되도록 하고 싶을 때 널 객체를 활용하면 좋다. 특정 슬롯을 쓰려고 할 때 마다 거기에 뭔가가 로딩되어 있는지 확인하려면 좀 귀찮기 때문이다.


완성된 리모컨 api 디자인

https://blog.yevgnenll.me/posts/what-is-command-pattern

  • 리모컨 코드를 최대한 단순하게 만들어서 협력 업체가 새로운 벤더 클래스를 공급하더라도 리모컨 코드를 고치지 않도록 하는 것에 중점을 두었다.
  • 커맨트 패턴을 도입해서 RemoteControl 클래스와 협력 업체로부터 제공되는 클래스를 논리적으로 분리했다.
  • 이러한 디자인은 리모컨의 유지보수 비용을 줄이는데 굉장히 도움이 된다.
  • 커맨드 클래스는 람다 표현식을 사용하여 커맨드 객체를 생성하는 단계를 건너뛰고 인스턴스를 생성하는 대신 그 자리에 함수 객체를 사용할 수 있다.
    • 물론 추상 메서드가 하나일 때만 가능
//        remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
        remoteControl.setCommand(0, () -> livingRoomLight.on(), () -> livingRoomLight.off());
        remoteControl.setCommand(1, kitchenLightOn, kitchenLightOff);
        remoteControl.setCommand(2, ceilingFanOn, ceilingFanOff);
        remoteControl.setCommand(3, stereoOnWithCD, stereoOff);

작업 취소 기능 추가하기

커맨드에서 작업 취소 기능을 지원하려면 execute() 메소드와 비슷한 undo() 메소드가 있어야 한다.

excute() 메소드에서 했던 작업과 정반대의 작업을 처리하면 된다. 커맨드 클래스에 작업 취소 기능을 추가하기 전에 우선 Command 인터페이스에 undo() 메소드를 추가해야 한다.

public interface Command {
        void excete();
    void undo();
}
  • 그리고 RemoteControl 클래스에 사용자가 마지막으로 누른 버튼을 기록하고, UNDO 버튼을 눌렀을 때 필요한 작업을 처리하는 코드를 추가해야 한다.
package command.client;

import command.cmd.Command;
import command.cmd.NoCommand;

public class RemoteControl {

    private static final int SLOT_SIZE = 7;
    Command[] onCommands;
    Command[] offCommands;
    Command undoCommand;

    public RemoteControl() {
        offCommands = new Command[SLOT_SIZE];
        onCommands = new Command[SLOT_SIZE];

        Command noCommand = new NoCommand();
        for (int i = 0; i < SLOT_SIZE; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;

        }
        // 다른 슬롯과 마찬가지로 사용자가 다른 버튼을 한 번도 누르지 않은 상태에서 undo 버튼을 누르더라도 별 문제가 없도록 한다.
        undoCommand = noCommand;
    }

    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    public void onButtonWasPushed(int slot) {
        onCommands[slot].execute();
        // 사용자가 버튼을 누르면 해당 커맨드 객체의 execute() 메서드를 호출한 다음
        // 그 객체의 레퍼런스를 undoCommand 인스턴스 변수에 저장한다.
        // on과 off 버튼을 처리할 때도 같은 방법 사용
        undoCommand = onCommands[slot];
    }

    public void offButtonWasPushed(int slot) {
        offCommands[slot].execute();
        undoCommand = offCommands[slot];
    }

    public void undoButtonWasPushed() {
        undoCommand.undo();
    }

    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("\n------ Remote Control -------\n");
        for (int i = 0; i < onCommands.length; i++) {
            stringBuilder
                .append("[slot ")
                .append(i)
                .append("] ")
                .append(onCommands[i].getClass().getSimpleName())
                .append("    ")
                .append(offCommands[i].getClass().getSimpleName())
                .append("\n");
        }
        return stringBuilder
            .append("[undo]")
            .append("    ")
            .append(undoCommand.getClass().getSimpleName())
            .toString();
    }
}
public class RemoteLoader {

    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl();

        Light livingRoomLight = new Light("Living Room");

        LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
        LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);

        remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);

        remoteControl.onButtonWasPushed(0);
        remoteControl.offButtonWasPushed(0);
        System.out.println(remoteControl);
        remoteControl.undoButtonWasPushed();
        remoteControl.offButtonWasPushed(0);
        remoteControl.onButtonWasPushed(0);
        System.out.println(remoteControl);
        remoteControl.undoButtonWasPushed();

    }
}
Living Room light is on
Living Room light is off

------ Remote Control -------
[slot 0] LightOnCommand    LightOffCommand
[slot 1] LightOnCommand    LightOffCommand
[slot 2] CeilingFanOnCommand    CeilingFanOffCommand
[slot 3] StereoOnWithCDCommand    StereoOffCommand
[slot 4] NoCommand    NoCommand
[slot 5] NoCommand    NoCommand
[slot 6] NoCommand    NoCommand
[undo]    LightOffCommand >>>>>> undoCmd에 마지막으로 호출되었던 커맨드 저장
Living Room light is on >>>>>> 사용자가 undo 버튼 클릭
Living Room light is off
Living Room light is on

------ Remote Control -------
[slot 0] LightOnCommand    LightOffCommand
[slot 1] LightOnCommand    LightOffCommand
[slot 2] CeilingFanOnCommand    CeilingFanOffCommand
[slot 3] StereoOnWithCDCommand    StereoOffCommand
[slot 4] NoCommand    NoCommand
[slot 5] NoCommand    NoCommand
[slot 6] NoCommand    NoCommand
[undo]    LightOnCommand >>>>>> undoCmd에 마지막으로 호출되었던 커맨드 저장
Living Room light is off >>>>>> 사용자가 undo 버튼 클릭

Process finished with exit code 0

작업 취소 기능을 구현할 때 상태를 사용하는 방법

작업 취소 기능을 구현하다 보면 간단한 상태를 저장해야 하는 상황도 종종 생긴다.

CeilingFan 클래스로 간단한 속도와 관련된 상태를 저장해보자.

public class CeilingFan {

    String location;
    int speed; // 속도를 나타내는 상태를 저장
    public static final int HIGH = 3;
    public static final int MEDIUM = 2;
    public static final int LOW = 1;
    public static final int OFF = 0;

    public CeilingFan(String location) {
        this.location = location;
        speed = OFF;
    }

    public void high() {
        // turns the ceiling fan on to high
        speed = HIGH;
        System.out.println(location + " ceiling fan is on high");

    }

    public void medium() {
        // turns the ceiling fan on to medium
        speed = MEDIUM;
        System.out.println(location + " ceiling fan is on medium");
    }

    public void low() {
        // turns the ceiling fan on to low
        speed = LOW;
        System.out.println(location + " ceiling fan is on low");
    }

    public void off() {
        // turns the ceiling fan off
        speed = OFF;
        System.out.println(location + " ceiling fan is off");
    }

    public int getSpeed() {
        return speed;
    }
}
package command.cmd;

import command.vendor.CeilingFan;

public class CeilingFanHighCommand implements Command {

    CeilingFan ceilingFan;
    int prevSpeed; // 상태 지역 변수로 선풍기의 속도를 저장

    public CeilingFanHighCommand(CeilingFan ceilingFan) {
        this.ceilingFan = ceilingFan;
    }

    public void execute() {
              // 속도를 변경하기 전에 작업을 취소해야 할 때를 대비해서 이전 속도를 저장
        prevSpeed = ceilingFan.getSpeed();
        ceilingFan.high();
    }

    @Override
    public void undo() {
        if (prevSpeed == CeilingFan.HIGH) {
            ceilingFan.high();
        } else if (prevSpeed == CeilingFan.MEDIUM) {
            ceilingFan.medium();
        } else if (prevSpeed == CeilingFan.LOW) {
            ceilingFan.low();
        } else if (prevSpeed == CeilingFan.OFF) {
            ceilingFan.off();
        }
    }
}

여러 동작을 한 번에 처리하기

package command.cmd;

public class MacroCommand implements Command {

    Command[] commands;

    public MacroCommand(Command[] commands) {
        this.commands = commands;
    }

    @Override
    public void execute() {
        for (int i = 0; i < commands.length; i++) {
            commands[i].execute();
        }
    }

    @Override
    public void undo() { // 역순으로 undo
        for (int i = commands.length - 1; i >= 0; i--) {
            commands[i].undo();
        }
    }
}
public class RemoteLoader {

    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl();
        Light light = new Light("Living Room");
        Stereo stereo = new Stereo("Living Room");

        LightOnCommand lightOnCommand = new LightOnCommand(light);
        LightOffCommand lightOffCommand = new LightOffCommand(light);
        StereoOnCommand stereoOnCommand = new StereoOnCommand(stereo);
        StereoOffCommand stereoOffCommand = new StereoOffCommand(stereo);
        Command[] partyOn = {lightOnCommand, stereoOnCommand};
        Command[] partyOff = {lightOffCommand, stereoOffCommand};
        MacroCommand partyOnMacro = new MacroCommand(partyOn);
        MacroCommand partyOffMacro = new MacroCommand(partyOff);

        remoteControl.setCommand(0, partyOnMacro, partyOffMacro);
        System.out.println(remoteControl);
        System.out.println("---- macro on ------");
        remoteControl.onButtonWasPushed(0);
        System.out.println("---- macro off ------");
        remoteControl.offButtonWasPushed(0);
    }
}
------ Remote Control -------
[slot 0] MacroCommand    MacroCommand
[slot 1] NoCommand    NoCommand
[slot 2] NoCommand    NoCommand
[slot 3] NoCommand    NoCommand
[slot 4] NoCommand    NoCommand
[slot 5] NoCommand    NoCommand
[slot 6] NoCommand    NoCommand
[undo]    NoCommand
---- macro on ------
Living Room light is on
Living Room stereo is on
Living Room stereo is set for CD input
Living Room stereo volume set to 11
---- macro off ------
Living Room light is off
Living Room stereo is off

Process finished with exit code 0

Q) 항상 리시버가 필요할까?? 커맨드 객체에서 execute()를 구현하면 안될까??

A) 일반적으로 리시버에 있는 행동을 호출하는 ‘더미’ 커맨드 객체를 만든다. 하지만 요구 사항의 전부는 아니더라도 대부분을 구현하는 ‘스마트’ 커맨드 객체를 만드는 경우도 자주 볼 수 있다. 물론 커맨드 객체에서 대부분의 행동을 처리해도 됩니다. 하지만 그러면 인보커와 리시버를 분리하기 어렵고, 리시버로 커맨드를 매개변수화할 수 없다는 점을 염두하자.

Q) 작업 취소를 할 때 히스토리 기능은 어떻게 구현할 수 있을까? 즉, undo 버튼을 여러 번 누를 수 있도록 하려면 어떻게 해야 할까??

A) 사실 그리 어려운 일은 아니다. 앞에서는 마지막으로 실행한 커맨드의 레퍼런스만 저장했었는데, 그 대신 전에 실행한 커맨드 자체를 스택에 넣으면 됩니다. 그리고 나서 사용자가 undo 버튼을 누를 때마다 인보커에서 스택 맨 위에 있는 항목을 꺼내서 undo() 메소드를 호출하도록 만들면 된다.

Q) 파티 모드를 구현할 때 PartyCommand의 execute() 메소드에서 다른 커맨드 객체의 excute()를 호출하는 방법을 써도 될까?

A) 그렇게 해도 되지만, 그러면 PartyComman에 파티 모드 코드를 직접 넣어야 하는데, 나중에 문제가 생길 수도 있습니다. MacroCommand를 사용하면 PartyCommand에 넣을 커맨드를 동적으로 결정할 수 있기에 유연성이 훨씬 좋아진다. 일반적으로 MacroCommand 만들어서 쓰는 방법이 더 우아한 방법이며, 추가해야 할 코드를 줄이는데도 도움이 된다.


커맨트 패턴 활용하기

커맨드로 컴퓨테이션의 한 부분(리시버와 일련의 행동)을 패키지로 묶어서 일급 객체 형태로 전달할 수도 있다. 그러면 클라이언트 애플리케이션에서 커맨드 객체를 생성 한 뒤 오랜 시간이 지나도 그 컴퓨테이션을 호출할 수 있다. 심지어 다른 스레드에서 호출할 수도 있다. 이점을 활용해서 커맨드 패턴을 스케줄러나 스레드 풀, 작업 큐와 같은 다양한 작업에 적용할 수 있다.

작업 큐를 떠올려 보자. 큐 한 쪽 끝은 커맨드를 추가할 수 있도록 되어 있고, 다른 쪽 끝에는 커맨드를 처리하는 스레드들이 대기하고 있다. 각 스레드는 우선 execute() 메소드를 호출하고 호출이 완료되면 커맨드 객체를 버리고 새로운 커맨드 객체를 가져옵니다.

작업 큐

  • 커맨드 인터페이스를 구현하는 객체를 큐에 추가한다.

컴퓨테이션(?)

  • 컴퓨테이션을 고정된 개수의 스레드로 제한할 수 있다.

작업 처리 스레드

  • 스레드는 큐에서 커맨드를 하나씩 제겋면서 커맨드의 execute() 메소드를 호출한다.
  • 메소드 실행이 끝나면 다시 큐에서 새로운 커맨드 객체를 가져간다.

작업 큐 클래스는 계산 작업을 하는 객체들과 완전히 분리되어 있다. 한 스레드가 한동안 금융 관련 계산을 하다가 잠시 후에는 네트워크로 뭔가를 내려받을 수도 있다. 작업 큐 객체는 전혀 신경쓸 필요가 없다. 큐에 커맨드 패턴을 구현하는 객체를 넣으면 그 객체를 처리하는 스레드가 생기고 자동으로 execute() 메소드가 호출된다.


커맨드 패턴 더 활용하기

어떤 애플리케이션은 모든 행동을 기록해 두었다가 애플리케이션이 다운되었을 때 그 행동을 다시 호출해서 복구할 수 있어야 한다. 커맨드 패턴을 사용하면 store()load() 메소드를 추가해서 이런 기능을 구현할 수 있다. 자바에서는 이런 메소드를 객체 직렬화로 구현할 수도 있지만, 직렬화와 관련된 제약 조건 때문에 쉽지 않다.

로그 기록은 어떤 명령을 실행하면서 디스크에 실행 히스토리를 기록하고, 애플리케이션이 다운되면 커맨드 객체를 다시 로딩해서 execute() 메소드를 자동으로 순서대로 실행하는 방식으로 작동한다.

지금까지 예로 든 리모컨에는 이런 로그 기록이 무의미하다. 하지만 데이터가 변경될 때마다 매번 저장할 수 없는 방대한 자료구조를 다루는 애플리케이션에 로그를 사용해서 마지막 체크 포인트 이후로 진행한 모든 작업을 저장한 다음 시스템이 다운되었을 때 최근 수행된 작업을 다시 적용하는 방법으로 사용할 수 있다.

스프레드시트 애플리케이션을 예를 들어 볼까요? 매번 데이터가 변경될 때마다 디스크에 저장하지 않고, 특정 체크 포인트 이후의 모든 행동을 로그에 기록하는 방식으로 복구 시스템을 구축할 수 있다. 더 복잡한 애플리케이션에는 이런 테크닉을 확장해서 일련의 작업에 트랜잭션을 활용해서 모든 작업이 완변하게 처리되도록 하거나, 아무것도 처리되지 않게 롤백되도록 할 수 있다.


실전 커맨드 패턴

자바의 스윙 라이브러리에는 사용자 인터페이스 구성 요소에서 발생하는 이벤트에 귀를 기울이는 ActionListener 형태의 옵저버가 어마어마하게 많다는 걸 배웠습니다. 그런데 ActionListener 는 Observer 인터페이스이자 Command 인터페이스이기도 하며, AngelListener와 DevilListenr 클래스는 그냥 Observer가 아니라 구상 Command 클래스이다. 즉, 두 패턴이 한꺼번에 들어가 있는 예제이다.

public class SwingObserverEx { // 클라이언트
        JButton button = new JButton("할까 말까"); // 인보커
    button.addActionListener(new AngelListener());
    button.addActionListener(new DevilListener());
}
    class AngelListener implements ActionListenr { // ActionListenr 커맨드 인터페이스
                public void actionPerformed(ActionEvent event) {
                        System.out.println("하지마!") // System 리시버
                }
    }
    class DevlilListener implements ActionListenr { // Angel, DevlilListener  구상 커맨드
                public void actionPerformed(ActionEvent event) {
                        System.out.println("해!")
                }
    }

핵심 정리

  • 커맨드 패턴을 사용하면 요청하는 객체와 요청을 수행하는 객체를 분리할 수 있다.
  • 이렇게 분리하는 과정의 중심에는 커맨드 객체가 있으며, 이 객체가 행동이 들어있는 리시버를 캡슐화한다.
  • 인보커는 무언가 요청할 때 커맨드 객체의 execute() 메소드를 호출하면 된다.
  • 커맨드는 인보커를 매개변수화할 수 있다. 실행 중에 동적으로 매개변수화를 설정할 수도 있다.
  • execute() 메소드가 마지막으로 호출되기 전의 상태로 되돌리는 작업 취소 메소드를 구현하면 커맨드 패턴으로 작업 취소 기능을 구현할 수도 있다.
  • 매크로 커맨드는 커맨드를 확장해서 여러 개의 커맨드를 한 번에 호출할 수 있게 해주는 가장 간편한 방법이다. 매크로 커맨드로도 어렵지 않게 작업 취소 기능을 구현할 수 있다.
  • 프로그래밍을 하다 보면 요청을 스스로 처리하는 ‘스마트’ 커맨드 객체를 사용하는 경우도 종종 있다.
  • 커맨드 패턴을 활용해서 로그 및 트랜잭션 시스템을 구현할 수 있다.

객체지향 도구 상자

  • 객체지향의 기초(4요소)
    • 캡슐화
    • 상속
    • 추상화
    • 다형성
  • 객체지향 원칙
    • 바뀌는 부분을 캡슐화한다.
    • 상속보다는 구성을 활용한다.
    • 구현이 아닌 인터페이스(super type)에 맞춰서 프로그래밍한다.
    • 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
    • 클래스는 확장에 대해서는 열려 있지만 변경에 대해서는 닫혀 있어야 한다. (OCP)
    • 추상화된 것에 의존하라. 구상 클래스에 의존하지 않도록 한다.
  • 객체지향 패턴
    • 스트레지티 패턴 : 알고리즘군을 정의하고 각각의 알고리즘을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 전략을 사용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.
    • 옵저버 패턴 : 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.
    • 데코레이터 패턴: 객체에 추가 요소를 동적으로 더할 수 있습니다. 데코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.
    • 추상 팩토리 패턴 : 서로 연관된, 또는 의존적인 객체들로 이루어진 제품군을 생성하기 위한 인터페이스를 제공한다. 구상 클래스는 서브 클래스에 의해 만들어진다.
    • 팩토리 메소드 패턴 : 객체를 생성하기 위한 인터페이스를 만든다. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하도록 한다. 팩토리 메소드를 이용하면 인스턴스를 만드는 일을 서브클래스로 미룰 수 있다.
    • 싱글턴 패턴 : 클래스 인스턴스가 하나만 만들어지도록 하고, 그 인스턴스에 대한 전역 접근을 제공한다.
    • 커맨드 패턴 : 요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화할 수 있다. 이러면 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있다.
728x90
728x90

싱글턴 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴이다.

고전적인 싱글턴 패턴 구현법

package singleton.before;

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() {
        if(uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }

}

초콜릿 공장

초콜릿 공장에서는 초콜릿을 끓이는 장치인 초콜릿 보일러를 컴퓨터로 제어한다.

이 보일러에서는 초콜릿과 우유를 받아서 끓이고 초코바를 만드는 단계로 넘겨준다. 여기에 초콜릿 보일러를 제어하기 위한 클래스가 나와 있다.

public class ChocolateBoiler {

    private boolean empty;
    private boolean boiled;

    public ChocolateBoiler() {
        this.empty = true;
        this.boiled = false;
    }

    public void fill() {
        if (isEmpty()) {
            empty = false;
            boiled = false;
        }
    }

    public void drain() {
        if (!isEmpty() && !isBoiled()) {
            empty = true;
        }
    }

    public void boil() {
        if (!isEmpty() && !isBoiled()) {
            boiled = true;
        }
    }

    public boolean isEmpty() {
        return this.empty;
    }

    public boolean isBoiled() {
        return this.boiled;
    }

}
  • 코드를 보면 실수를 하지 않도록 주의를 기울여져 있다.
  • 하지만 두 개의 ChocolateBoiler 인스턴스가 따로 돌아가게 되면 상당히 안 좋은 상황이 일어날 수 있다는 것을 알 수 있다.
  • 만약 애플리케이션에서 ChocolateBoiler 인스턴스가 두 개 이상 만들어지게 되면 어떤 문제가 생길까??
    • 자원을 불필요하게 잡아먹고, 애플리케이션의 동작이 이상하게 돌아가는 결과에 일관성이 없어지는 심각한 문제가 발생할 것이다.
    • ChocolateBoiler 클래스를 싱글턴으로 업그레드해보자
public class ChocolateBoiler {

    private boolean empty;
    private boolean boiled;

    private static ChocolateBoiler uniqueInstance;

    private ChocolateBoiler() {
        this.empty = true;
        this.boiled = false;
    }

    public static ChocolateBoiler getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new ChocolateBoiler();
        }
        return uniqueInstance;
    }

    public void fill() {
        if (isEmpty()) {
            empty = false;
            boiled = false;
        }
    }

    public void drain() {
        if (!isEmpty() && !isBoiled()) {
            empty = true;
        }
    }

    public void boil() {
        if (!isEmpty() && !isBoiled()) {
            boiled = true;
        }
    }

    public boolean isEmpty() {
        return this.empty;
    }

    public boolean isBoiled() {
        return this.boiled;
    }

}

싱글턴 패턴의 정의

싱글턴의 고전적인 구현법을 배웠다. 그렇다면 싱글턴 패턴의 정의는 무엇이고, 실제로 어떤 식으로 싱글턴 패턴을 적용해야할까??

  • 클래스에서 자신의 단 하나뿐인 인스턴스를 관리하도록 만들면 된다.
    • 그리고 다른 어떤 클래스에서도 자신의 인스턴스를 추가로 만들지 못하도록 해야 한다.
    • 따라서 인스턴스가 필요하면 반드시 클래스 자신을 거치도록 해야 될 것이다.
  • 어디서든 그 인스턴스를 접근할 수 있도록 만들어야 한다.
    • 다른 객체에서 이 인스턴스가 필요하면 언제든지 클래스한테 요청을 할 수 있게 만들고, 요청이 들어오면 그 하나뿐인 인스턴스를 건네주도록 만들어야 한다.
    • 앞에서 보았듯이, 싱글턴이 Lazy하게 생성되도록 구현할 수도 있다. 싱글턴 클래스의 객체가 자원을 많이 잡아먹는 경우에는 유용하다.

클래스 다이어그램

https://www.hanbit.co.kr/channel/category/category_view.html?cms_code=CMS8616098823

골칫덩어리 스레드

고전적인 싱글턴을 이용해서 코드를 고쳤음에도 ChocolateBoiler에 있는 fill() 메소드에서 아직 초콜릿이 끓고 있는데 재료를 집어넣고 말았다. 무슨 일이 일어난 것일까??

  • 조금 전에 다중 스레드를 사용하도록 ChocolateBoiler 컨트롤러를 최적화시킨 것이 문제일까??
  • 스레드가 추가된 것 때문에 이런 문제가 생긴 것일까??

JVM의 입장

두 개의 스레드에서 여기에 있는 코드를 실행시킨다고 가정해보고 두 스레드가 다른 보일러 객체를 사용하게 될 가능성이 있는지 따져보자.

ChocolateBoiler boiler = ChocolateBoiler.getInstacne();
boiler.fill();
boiler.boil();
boiler.drain();
public static ChocolateBoiler getInstance() {
      if (uniqueInstance == null) {
          uniqueInstance = new ChocolateBoiler();
      }
      return uniqueInstance;
}
  • 바로 두 스레드가 동시에 getInstance() 메소드를 수행하게 되면 uniqueInstance null 상태라 각 스레드마다 ChocolateBoiler 인스턴스를 생성하여 리턴하여 결국 서로 다른 두 인스턴스가 만들어진다.

멀티스레딩 문제 해결 방법

문제를 해결하는 방법은 간단한데 바로 getInstance()를 동기화시키기만 하면 된다.

public static synchronized ChocolateBoiler getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new ChocolateBoiler();
        }
        return uniqueInstance;
}
  • synchronized 키워드를 추가하면 한 스레드가 메소드 사용을 끝내기 전까지 다른 스레드는 기다려야 한다.
  • 즉, 두 스레드가 이 메소드를 동시에 실행시키는 일은 일어나지 않는다.
  • 하지만 이렇게 하면 동기화로 인한 속도 문제가 생긴다.
    • 사실 동기화가 꼭 필요한 시점은 이 메소드가 시작되는 때 뿐이다.
    • 즉, 일단 uniqueInstance 변수에 Singleton 인스턴스를 대입하고 나면 굳이 이 메소드를 동기화된 상태로 유지시킬 필요가 없다.
    • 불필요한 오버헤드만 증가시킬뿐인 것

더 효율적인 방법은 없을까?

대부분의 자바 애플리케이션에서 싱글턴이 다중 스레드 환경에서 돌아갈 수도 있도록 만들어야 한다. 하지만 getInstance() 메소드를 동기화시키려면 대가를 치뤄야 한다. 다른 방법은 없을까??

1. getInstance()의 속도가 중요하지 않다면 그냥 둔다.

만약 getInstance() 메소드가 애플리케이션에 큰 부담을 주지 않는다면 그냥 놔둬도 된다. getInstance()를 동기화시키는게 굉장히 쉽고, 효율 면에서도 나쁘지 않을 수있다.

하지만 메소드를 동기화하면 성능이 100배 정도 저하된다는 것을 기억하자. 만약 getInstance()가 애플리케이션에서 병목으로 작용한다면 다른 방법을 생각해야 한다.

2. 인스턴스를 필요할 때 생성하지 말고, 처음부터 만들어 버린다.

애플리케이션에서 반드시 Singleton의 인스턴스를 생성하고, 그 인스턴스를 항상 사용한다면, 또는 인스턴스를 실행중에 수시로 만들고 관리하기가 성가시다면 다음과 같은 식으로 처음부터 Singleton 인스턴스를 만들어버리는 것도 괜찮은 방법이다.

public class Singleton {

    private static Singleton uniqueInstance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return uniqueInstance;
    }

}
  • 이런 접근법을 사용하면 클래스가 로딩될 때, JVM에서 Singleton의 유일한 인스턴스를 생성해 주고, JVM에서 유일한 인스턴스를 생성하기 전에는 그 어떤 스레드도 uniqueInstance 정적 변수에 접근할 수 없다.

3. DCL(Double Checking Locking)을 써서 getInstance()에서 동기화되는 부분일 줄인다.

DCL을 사용하면 , 일단 인스턴스가 생성되어 있는지 확인한 다음, 생성되어 있지 않았을 때만 동기화를 할 수 있다. 이렇게 하면 처음에만 동기화를 하고 나중에는 동기화를 하지 않도록 동작하여, 바로 원하던 동작이 수행된다.

public class Singleton {

        // 자바 5 이전 버전은 동기화 x
    private volatile static Singleton uniqueInstance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        if(uniqueInstance == null) { // 인스턴스가 있는지 확인하고, 없으면 동기화된 블럭으러 진입
            synchronized (Singleton.class) {
                if(uniqueInstance == null) { // 블록으로 들어온 후레도 다시 한번 널체크한 후, 인스턴스를 생성한다.
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }

}

싱글턴 관련 Q&A

Q) 모든 메서드와 변수가 static으로 선언된 클래스를 만들어도 결과적으로는 같지 않을까?

A) 맞다. 하지만 필요한 내용이 클래스에 다 들어있고, 복잡한 초기화가 필요없는 경우에만 해당 방법을 사용할 수 있다. 그리고 자바에서 정적 초기화를 처리하는 방법 때문에 복잡해질 수 있다. 특히 여러 클래스가 얽혀 있는 경우에는 지저분하고, 초기화 순서와 관련된 버그는 찾기 어렵기 때문에 해당 방식으로 싱글턴 비슷한 걸 만들어야 한다면 좋지 않을 수 있다.

Q) 클래스 로더와 관련된 문제는 없을까?

A) 클래스 로더마다 서로 다른 네임스페이스를 정의하기 때문에 클래스 로더가 두 개 이상이라면 같은 클래스를 여러 번 로딩할 수도 있다. 만약 싱글턴을 그런 식으로 로딩하면 인스턴스가 여러 개 만들어지는 문제가 발생할 수 있다. 따라서 클래스 로더를 여러 개 사용하면서 싱글턴을 사용한다면 조심해야 하고, 클래스 로더를 직접 지정해서 문제를 회피할 수도 있다.

Q) 전역 변수가 싱글턴보다 나쁜 이유는 무엇일까??

A) 자바의 전역 변수는 기본적으로 객체에 대한 정적 레퍼런스다. 전역 변수를 이런 식으로 사용한다면 게으른 인스턴스를 사용할 수 없는 단점과 싱글턴 패턴을 쓰는 두 가지 이유 중, 클래스의 인스턴스가 하나만 있을 수 있도록 할 수 없다. 전역 변수를 사용한다면 간단한 객체에 대한 전역 레퍼런스를 자꾸 만들게 도면서 네임스페이스를 지저분한게 만드는 경향이 생긴다.

핵심 정리

  • 어떤 클래스를 싱글턴 패턴을 적용하면 애플리케이션에 그 클래스의 인스턴스가 최대 한 개 까지만 있도록 할 수 있다.
  • 싱글턴 패턴을 이용하면 유일한 인스턴스를 어디서든지 접근할 수 있도록 할 수 있다.
  • 자바에서 싱글턴 패턴을 구현할 때는 private 생성자와 정적 메소드, 정적 변수를 사용한다.
  • 다중 스레드를 사용하는 애플리케이션에서는 속도와 자원 문제를 파악해보고 적절한 구현법을 사용해야 한다.
    • 사실상 멀티스레딩을 기본으로 가정해야한다.
  • DCL을 사용하는 방법은 자바 2 버전5보다 전에 나온 버전에서는 쓸 수 없다.
  • 클래스 로더가 여러 개 있으면 싱글턴이 제대로 작동하지 않고, 여러 개의 인스턴스가 생길 수 있다.
  • 1.2 버전보다 전에 나온 JVM을 사용하는 경우에는 가바지 컬렉터 관련 버그 때문에 싱글턴 레지스트리를 사용해야 할 수도 있다.

객체지향 도구 상자

  • 객체지향의 기초(4요소)
    • 캡슐화
    • 상속
    • 추상화
    • 다형성
  • 객체지향 원칙
    • 바뀌는 부분을 캡슐화한다.
    • 상속보다는 구성을 활용한다.
    • 구현이 아닌 인터페이스(super type)에 맞춰서 프로그래밍한다.
    • 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
    • 클래스는 확장에 대해서는 열려 있지만 변경에 대해서는 닫혀 있어야 한다. (OCP)
    • 추상화된 것에 의존하라. 구상 클래스에 의존하지 않도록 한다.
  • 객체지향 패턴
    • 스트레지티 패턴 : 알고리즘군을 정의하고 각각의 알고리즘을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 전략을 사용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.
    • 옵저버 패턴 : 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.
    • 데코레이터 패턴: 객체에 추가 요소를 동적으로 더할 수 있습니다. 데코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.
    • 추상 팩토리 패턴 : 서로 연관된, 또는 의존적인 객체들로 이루어진 제품군을 생성하기 위한 인터페이스를 제공한다. 구상 클래스는 서브 클래스에 의해 만들어진다.
    • 팩토리 메소드 패턴 : 객체를 생성하기 위한 인터페이스를 만든다. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하도록 한다. 팩토리 메소드를 이용하면 인스턴스를 만드는 일을 서브클래스로 미룰 수 있다.
    • 싱글턴 패턴 : 클래스 인스턴스가 하나만 만들어지도록 하고, 그 인스턴스에 대한 전역 접근을 제공한다.
728x90
728x90

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

  • 느슨한 결합을 이용하는 객체지향 디자인, 객체의 인스턴스를 만드는 작업이 항상 공개되어 있어야 하는 것은 아니며, 오히려 결합과 관련된 문제가 생길 수 있다. 팩토리 패턴을 이용하여 불필요한 의존성을 없애보자

추상 팩토리 패턴에서는 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있다.

원재료군

뉴옥과 시카고에서 사용하는 재료 종류는 서로 다르다. PizzaStore 분점이 점점 생기면서 해당 분점들은 또 다른 자신들만의 재료들을 사용해야 될 것이다.

이렇게 서로 다른 종유릐 재료들을 제공하기 위해 원재료군을 처리할 방법을 생각해야한다.

원재료 공장 만들기

따라서 이제 원재료를 생산하기 위한 공장을 만들어보자.

원재료 공장에서는 원재료군에 들어있는 반죽, 소스, 치즈 같은 각각의 원재료를 생산한다.

우선 모든 원재료를 생산할 팩토리를 위한 인터페이스를 정의해보자

public interface PizzaIngredientFactory {

    Dough createDough();

    Sauce createSauce();

    Cheese createCheese();

    Veggies[] createVeggies();

    Pepperoni createPepperoni();

    Clams createClam();

}
public class NYPizzaIngredientFactory implements PizzaIngredientFactory {

    @Override
    public Dough createDough() {
        return new ThinCrustDough();
    }

    @Override
    public Sauce createSauce() {
        return new MarinaraSauce();
    }

    @Override
    public Cheese createCheese() {
        return new ReggianoCheese();
    }

    ...
}

피자 클래스 변경

원재료 팩토리가 준비되고 Pizza 클래스에서 팩토리에서 생산한 원재료만 사용하도록 코드를 수정한다.

public abstract class Pizza {

    String name;
    Dough dough;
    Sauce sauce;
    Cheese cheese;
        ...

    public void box() {
        System.out.println("포장");
    }

    public void cut() {
        System.out.println("커팅");
    }

    public void bake() {
        System.out.println("굽기");
    }

    abstract void prepare();
}
  • prepare() 메서드를 제외한 다른 메서드들은 바뀌지 않는다.
public class CheesePizza extends Pizza {

    private final PizzaIngredientFactory pizzaIngredientFactory;

    public CheesePizza(PizzaIngredientFactory pizzaIngredientFactory) { // 생성자를 통해 원재료를 제공하는 팩토리를 주입받는다.
        this.pizzaIngredientFactory = pizzaIngredientFactory;
    }

    @Override
    void prepare() { // 팩토리가 작동하는 부분
        dough = pizzaIngredientFactory.createDough();
        sauce = pizzaIngredientFactory.createSauce();
        cheese = pizzaIngredientFactory.createCheese();
    }
}

앞선 예제에서 NYStyleCheesePizza , ChicagoStyleCheesePizza 클래스를 기억해보자. 그 두 클래스를 살펴보면 지역별로 다른 재료를 사용한다는 것만 빼면 다른 점이 없다.

  • 따라서 피자마다 클래스를 지역별로 따로 만들 필요가 없다. 지역별로 다른 점은 원재료 공장에서 처리하기 때문

이제 피자 코드에서는 팩토리를 이용하여 피자에서 쓰이는 재료를 만든다.

  • 만들어지는 재료는 어떤 팩토리를 쓰는지에 따라 달라지며 피자 클래스에서는 전혀 신경을 쓰지 않는다.
  • 이제 피자 클래스와 지역별 재료가 분리되어 있기 때문에 어떤 지역의 재료 팩토리를 사용하든 피자 클래스는 그대로 재사용할 수 있다.

마찬가지로 피자 가게를 수정해보자

public class NYPizzaStore extends PizzaStore {

    @Override
    protected Pizza createPizza(String type) {
        Pizza pizza = null;
        PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();

        if (type.equals("cheese")) {
            pizza = new CheesePizza(ingredientFactory);
        } else if (type.equals("greek")) {
            pizza = new GreekPizza(ingredientFactory);
        }
        return pizza;
    }
}
  • 뉴욕 피자 가게에서는 뉴욕 피자 원재료 공장을 주입시켜 준다.

정리

기존 팩토리 패턴에서 추상 팩토리라고 부르는 새로운 형식의 팩토리를 도입해서 서로 다른 피자에서 필요로 하는 원재료군을 생산하기 위한 방법을 구축했다.

  • 추상 팩토리를 통해서 제품군을 생성하기 위한 인터페이스를 제공할 수 있다.
  • 이 인터페이스를 이용하는 코드를 만들면 코드를 제품을 생산하는 실제 팩토리와 분리시킬 수 있다.
  • 이렇게 함으로써 서로 다른 상황별로 적당한 제품을 생산할 수 있는 다양한 팩토리를 구현할 수 있게 된다.

추상 팩토리 패턴 정의

제품군을 만들 때 쓸 수 있는 추상 팩터리 패턴에서는 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있다.

  • 추상 팩토리 패턴을 사용하면 클라이언트에서 추상 인터페이스를 통해서 일련의 제품들을 공급받을 수 있다.
  • 이 때, 실제로 어떤 제이품이 생산되는지 전혀 알 필요가 없다.
  • 따라서 클라이언트와 팩토리에서 생산되는 제품을 분리시킬 수 있다.

클래스 다이어그램

 

추상 팩토리 패턴과 팩토리 메서드 패턴의 차이

추상 팩토리 패턴에 있는 createDough(), createSauce() 같은 메서드는 전부 팩토리 메서드 같이 보인다.

그렇다면 추상 팩토리 패턴 뒤에는 팩토리 메서드 패턴이 숨어져 있는 것일까?

  • 각 메서드는 추상 메서드로 선언되어 있고, 서브 클래스에서 메소드를 오버라이드해서 객체를 만드는 방식이기 때문
  • 추상 팩터리가 일련의 제품들을 생성하는데 쓰일 인터페이스를 정의하기 위해 만들어진 것이므로, 해당 인터페이스에 있는 메서드는 구상 제품을 만드는 일을 맡고 있고, 추상 팩토리의 서브클래스를 만들어서 각 메서드의 구현을 제공한다.
  • 따라서 추상 팩토리 패턴에서 제품을 생성하기 위한 메서드를 구현하는데 있어서 팩토리 메서드를 사용하는것은 자연스러운 일이다.

하지만 팩토리 메서드 패턴은 상속을 통해 객체를 생성하고 추상 팩토리 패턴은 객체 구성을 통해 객체를 생성한다.

또한 추상 팩토리 패턴에서는 제품군에 제품을 추가하는 식으로 관련 제품들을 확대해야 하는 경우에 인터페이스를 수정해야 하지만 팩토리 메서드 패턴에서는 한 가지 제품만 생산하므로 복잡한 인터페이스도 필요하지 않고, 메서드도 하나만 있으면 된다.

  • 추상 팩토리 패턴은 클라이언트에서 서로 연관된 제품군을 만들어야 할 때
  • 팩토레 메소드 패턴은 클라이언트 코드와 인스턴스를 만들어야 할 구상 클래스를 분리시켜야하거나, 어떤 구상 클래스를 필요로 하게 될지 미리 알 수 없는 경우에 매우 유용하다.

핵심 정리

  • 팩토리를 쓰면 객체 생성을 캡슐화할 수 있다.
  • 간단한 팩토리는 엄밀히 디자인 패턴은 아니지만, 클라이언트와 구상 클래스를 분리시키기 위한 간단한 기법으로 활용 가능하다.
  • 팩토리 메소드 패턴에서는 상속을 활용한다. 객체 생성이 서브클래스에게 위임된다. 서브 클래스에서는 팩토리 메소드를 구현하여 객체를 생산한다.
  • 추상 팩토리 패턴에서는 객체 구성을 활용한다. 객체 생성이 팩토리 인터페이스에서 선언한 메소스들에서 구현된다.
  • 모든 팩토리 패턴에서는 애플리케이션의 구상 클래스에 대한 의존성을 줄여줌으로써 느슨한 결합을 도와준다.
  • 추상 팩토리 패턴은 구상 클래스에 직접 의존하지 않고도 서로 관련된 객체들로 이루어진 제품군을 만들기 위한 용도로 쓰인다.
  • DIP에 따르면 구상 형식에 대한 의존을 피하고 추상화를 지향할 수 있다.
  • 팩토리는 구상 클래스가 아닌 추상 클래스, 인터페이스에 맞춰 코딩할 수 있게 해주는 강력한 기법이다.

객체지향 도구 상자

  • 객체지향의 기초(4요소)
    • 캡슐화
    • 상속
    • 추상화
    • 다형성
  • 객체지향 원칙
    • 바뀌는 부분을 캡슐화한다.
    • 상속보다는 구성을 활용한다.
    • 구현이 아닌 인터페이스(super type)에 맞춰서 프로그래밍한다.
    • 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
    • 클래스는 확장에 대해서는 열려 있지만 변경에 대해서는 닫혀 있어야 한다. (OCP)
    • 추상화된 것에 의존하라. 구상 클래스에 의존하지 않도록 한다.
  • 객체지향 패턴
    • 스트레지티 패턴 : 알고리즘군을 정의하고 각각의 알고리즘을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 전략을 사용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.
    • 옵저버 패턴 : 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.
    • 데코레이터 패턴: 객체에 추가 요소를 동적으로 더할 수 있습니다. 데코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.
    • 추상 팩토리 패턴 : 서로 연관된, 또는 의존적인 객체들로 이루어진 제품군을 생성하기 위한 인터페이스를 제공한다. 구상 클래스는 서브 클래스에 의해 만들어진다.
    • 팩토리 메소드 패턴 : 객체를 생성하기 위한 인터페이스를 만든다. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하도록 한다. 팩토리 메소드를 이용하면 인스턴스를 만드는 일을 서브클래스로 미룰 수 있다.
728x90
728x90

팩토리 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

  • 느슨한 결합을 이용하는 객체지향 디자인, 객체의 인스턴스를 만드는 작업이 항상 공개되어 있어야 하는 것은 아니며, 오히려 결합과 관련된 문제가 생길 수 있다. 팩토리 패턴을 이용하여 불필요한 의존성을 없애보자

팩토리 메소드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다. 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것

“new”는 “구상 객체”를 뜻한다.

new를 사용하는 것은 구상 클래스의 인스턴스를 만드는 것이다. 당연히 인터페이스가 아닌 특정 구현을 사용하는 것, 앞에서 디자인 패턴을 공부하면서 구상 클래스를 바탕으로 코딩을 하게 되면 코드를 수정해야 할 가능성이 높아지고, 유연성이 떨어지는 것을 볼 수 있었다.

Duck duck = new MallardDuck();

일련의 구상 클래스들이 존재하는 경우, 다음과 같은 코드를 만들어야 하는 경우가 있다.

Duck duck;

// 컴파일시에는 어떤 것의 인스턴스를 만들어야 할지 알 수 없다.
if(picnic) {
        duck = new MallardDuck();
} else if() {
        duck = new DecoyDuck();
} else if() {
        duck = new RubberDuck();
}

이런 코드가 있다는 것은, 변경하거나 확장해야 할 때 코드를 다시 확인하고 추가 또는 제거해야 한다는 것을 뜻한다. 따라서 코드를 이런식으로 만들면 관리 및 갱신하기가 어려워지고, 오류가 생길 가능성이 높아지게 된다.

사실 “new” 자체에 문제가 있는 것은 아니다. 가장 문제가 되는 점은 “변경”이다. 뭔가 변경되는 것 때문에 new를

사용하는데 있어서 조심해야 하는 것이다.

그렇기 때문에 인터페이스에 맞춰서 코딩을 하면 다형성 덕분에 어떤 클래스든 특정 인터페이스만 구현하면 사용할 수 있기 때문에 여러 변경에 대해 유연함을 가질 수 있는 것이다. 반대로 구상 클래스를 많이 사용하면 새로운 구상 클래스가 추가될 때마다 코드를 고쳐야 하기 때문에 많은 문제가 생길 수 있다. 즉 OCP 원칙

바뀌는 부분을 찾아보자

피자 가게를 운영하고 있다고 생각해보자.

피자의 종류가 다양할 것이나, 새로운 피자 신메뉴를 출시하거나 메뉴가 사라질 수 있을 것이다.

따라서 oderPizza() 메소드에서 가장 문제가 되는 부분은 바로 인스턴스를 만들 구상 클래스를 선택하는 부분이다. 해당 부분 때문에 변화에 따라 코드를 변경할 수 밖에 없다. 이제 바뀌는 부분과 바뀌지 않는 부분을 파악했으니 캡슐화할 차례이다.

Pizza orderPizza(String type) {
        Pizza pizza = null; // 인터페이스

                // 바뀌는 부분
        if (type.equals("cheese")) {
            pizza = new CheesePizza();
        } else if (type.equals("greek")) {
            pizza = new GreekPizza();
        } else if (type.equals("pepperoni")) {
            pizza = new PepperoniPizza();
        }

                // 바뀌지 않는 부분
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }

객체 생성 부분을 캡슐화해보자

이제 객체를 생성하는 부분을 메소드에서 뽑아내어 피자 객체를 만드는 일만 전담하는 다른 객체에 집어넣어 보자.

피자를 만드는 일만 처리하는 객체를 집어 넣어 다른 객체에서 피자를 만들어야 하는 일이 있으면 해당 객체에게 부탁하는 객체 추가해보자. 새로 만들 객체에는 팩토리라는 이름을 붙이기로 하자. SimplePizzaFactory를 만들고 나면 orderPizza() 메소드는 새로 만든 객체의 클라이언트가 된다. 즉 새로 만든 객체를 호출하는 것. 피자 공장에 피자 하나 만들어 달라고 부탁한다고 생각하면 쉽다.

SimplePizzaFactory를 추가하자

피자 객체 생성을 전달한 클래스를 정의한다.

// 해당 클래스에서 하는 일은 클라이언트를 위해 피자를 만들어 주는 일 뿐이다.
public class SimplePizzaFactory {

    public Pizza createPizza(String type) {
        Pizza pizza = null;

        if (type.equals("cheese")) {
            pizza = new CheesePizza();
        } else if (type.equals("greek")) {
            pizza = new GreekPizza();
        } else if (type.equals("pepperoni")) {
            pizza = new PepperoniPizza();
        }
        return pizza;
    }

}

Q) 이렇게 하면 어떤 장점이 있는 것일까?? 얼핏 보면 아까 문제를 다른 객체로 넘겨 버린 것 처럼 보일 수 있어 보인다.

  • SimplePizzaFactory를 사용하는 클라이언트가 매우 많을 수 있는 점을 생각 하자
  • 피자 객체를 받아서 가격, 설명 등을 찾아서 활용하는 클래스 또는 피자 주문을 처리하는 클래스에서도 이 팩토리를 사용할 수 있을 것이다.
  • 따라서 피자리를 생성하는 작업을 한 클래스에 캡슐화시켜 놓으면 구현을 변경해야 하는 경우에 여기저기 다 들어가서 고칠 필요 없이 팩토리 클래스 하나만 고치면 된다.
  • 추후 클라이언트 코드에서 구상 클래스의 인스턴스를 만드는 코드를 없애는 작업 진행

Q) 비슷한 식으로 메소드를 정적 메소드를 선언한 디자인(정적 팩터리 메소드)과 차이점은 무엇일까?

  • 정적 팩토리 메서드를 사용하면 객체를 생성하기 위한 메소드를 실행시키기 위해서 객체의 인스턴스를 만들지 않아도 되기 때문에 간단한 팩토리를 정적 메소드를 정의하는 기법도 일반적으로 많이 쓰인다.
  • 하지만 서브클래스를 만들어서 객체 생성 메소드의 행동을 변경시킬 수 없다는 단점이 존재한다.

간단한 팩토리를 이용한 PizzaStore 수정

public class PizzaStore {

    SimplePizzaFactory simplePizzaFactory;

    public PizzaStore(SimplePizzaFactory simplePizzaFactory) {
        this.simplePizzaFactory = simplePizzaFactory;
    }

    Pizza orderPizza(String type) {
        Pizza pizza = simplePizzaFactory.createPizza(type);

        // 바뀌지 않는 부분
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }

}

PizzaStore

  • 팩토리를 사용하는 클라이언트
  • 팩토리를 통해 피자 인스턴스를 받게 된다.

SimplePizzaFactory

  • 피자 객체를 만드는 팩토리
  • 애플리케이션에서 유일하게 구상 Pizza 클래스를 직접 참조한다.

팩토리 메서드

피자 가게가 큰 성공을 거두어 여러 지점을 가지게 되었고, 지역별로 조금씩 다른 차이점이 존재했고, 각각 지역의 특성을 반영하여 피자를 만들어야 했다. 이런 차이점을 어떤 식으로 적용해야 할까??

간단하게 생각하면 SimplePizzaFactory를 빼고 지역별 피자 팩토리(NYPizzaFactory, ChicagoPizzaFactory)를 만든 다음, PizzaStore에서 해당하는 팩토리를 사용하도록 하면 될 것이다.

NYPizzaFactory nyFactory = new NYPizzaFactory();
PizzaStore nyStore = new PizzaStore(nyFactory);
nyStore.orderPizza("Veggie");

ChicagoPizzaFactory chicagoFactory = new ChicagoPizzaFactory();
PizzaStore chicagoStore = new PizzaStore(chicagoFactory);
chicagoStore.orderPizza("Veggie");

피자 가게 프레임워크 - 팩토리 메소드 선언

피자를 만드는 활동 자체를 전부 PizzaStore 클래스에 국한시키면서도 분점마다 고유의 스타일을 살릴 수 있도록 하기 위해서 createPizza() 메소드를 다시 PizzaStore에 집어 넣고 추상 메서드로 선언하고, 각 지역마다 고유의 스타일에 맞게 PizzaStore의 분점을 나타내는 서브클래스를 만들도록 할 것이다. 즉 피자의 스타일은 각 서브클래스에서 결정하는 것

public abstract class PizzaStore {

    Pizza orderPizza(String type) {

        Pizza pizza = createPizza(type); // 팩토리 객체가 아닌 메소드 호출

        // 바뀌지 않는 부분
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }

    // 팩토리 객체 대신 해당 "메소드" 사용
    // "팩토리 메소드"가 추상 메소드로 바뀌었다.
    abstract Pizza createPizza(String type);

}

서브클래스에서 결정되는 것

각 서브클래스에서 달라질 수 있는 것은 피자의 만들 때의 스타일 뿐이다. 이렇게 달라지는 점들을 createPizza() 메소드에 집어넣고 그 메소드에서 해당 스타일의 피자를 만드는 것을 모두 책임진다. PizzaStore의 서브클래스에서 createPizza() 메소드를 구현하도록 하면 PizzaStore 프레임워크에 충실하면서도 각각의 스타일을 제대로 구현할 수 있는 PizzaStore 서브클래스들을 구비할 수 있다.

  • PizzaStore에서 정의한 메서드를 서브클래스에서 고쳐서 쓸 수 없게 하고 싶다면 메서드를 final로 선언할 수도 있다.
  • ex) final Pizza orderPizza(String type)
public class NYPizzaStore extends PizzaStore {

    @Override
    Pizza createPizza(String type) {
        Pizza pizza = null;

        if (type.equals("cheese")) {
            pizza = new NYStyleCheesePizza();
        } else if (type.equals("greek")) {
            pizza = new NYStyleGreekPizza();
        }
        return pizza;
    }
}

public class ChicagoPizzaStore extends PizzaStore {

    @Override
    Pizza createPizza(String type) {
        Pizza pizza = null;

        if (type.equals("cheese")) {
            pizza = new ChicagoStyleCheesePizza();
        } else if (type.equals("greek")) {
            pizza = new ChicagoStyleGreekPizza();
        }
        return pizza;
    }
}
  • 슈퍼클래스에 있는 orderPizza() 메소드에서는 Pizza 객체를 가지고 여러 작업을 하긴 하지만, Pizza는 추상 클래스기 때문에 어떤 구상 클래스에서 작업이 처리되고 있는지 전혀 알 수없다.
    • 즉 PizzaStore와 Pizza는 서로 완전히 분리되어 있다.
    • 그렇다면 피자 종류를 결정하는 것은 누구일까??
  • orderPizza() 입장에서 볼 때는 PizzaStore 서브클래스에서 피자 종류를 결정한다고 할 수 있을 것이다.
    • 따라서 서브 클래스에서 실제로 뭔가를 “결정”하는 것이 아니라, 우리가 선택하는 PizzaStore의 서브클래스 종류에 따라 결정되는 것이지만, 만들어지는 피자의 종류를 해당 PizzaStore 서브클래스에서 결정한다고 할 수 있다.

팩토리 메소드

abstract Product factoryMethod(String type);

팩토리 메소드는 객체 생성을 처리하며, 팩토리 메소드를 이용하면 객체를 생성하는 작업을 서브클래스에 캡슐화시킬 수 있다. 이렇게 하면 슈퍼 클래스에 있는 클라이언트 코드와 서브클래스에 있는 객체 생성 코드를 분리시킬 수 있다.

  • abstract
    • 팩토리 메소드는 추상 메소드로 선언하여 서브클래스에서 객체 생성을 책임지도록 한다.
  • Product
    • 팩토리 메소드에서는 특정 객체를 리턴하며, 그 객체는 보통 수퍼 클래스에서 정의한 메소드 내에서 쓰이게 된다.
  • factoryMethod
    • 팩토리 메소드는 클라이언트 (ex : orderPizza method)에서 실제로 생성되는 구상 객체가 무엇인지 알 수 없게 만드는 역할도 한다.
  • type
    • 팩토리 메소드를 만들 때 매개변수를 써서 만들어낼 객체 종류를 선택할 수도 있다.

사용 code

public class Main {

    public static void main(String[] args) {
        PizzaStore nyStore = new NYPizzaStore();
        PizzaStore chicagoStore = new ChicagoPizzaStore();

        Pizza pizza = nyStore.orderPizza("cheese");
        pizza = chicagoStore.orderPizza("cheese");

    }

}

팩토리 메소드 패턴

팩토리 메소드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다. 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것

  • 모든 팩토리 패턴에서는 객체 생성을 캡슐화한다.
  • 팩토리 메소드 패턴에서는 서브 클래스에서 어떤 클래스를 만들지를 결정하게 함으로써 객체 생성을 캡슐화한다.
  • 즉 팩토리 메서드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다.
  • 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것

클래스 다이어그램을 살펴보자

Creator : 생산자

  • ex) PizzaStore Class
  • 제품을 가지고 원하는 일을 하기 위한 모든 메소드들이 구현되어 있다.
  • 하지만 제품을 만들어 주는 팩토리 메소드는 추상 메소드로 정의되어 있을 뿐, 구현되어 있진 않다.
  • Creator의 모든 서브 클래스에서 factoryMethod() 추상 메소드를 구현해야 한다.

ConreteCreator : 구상 생산자

  • 실제로 제품을 생산하는 factoryMethod()를 구현한다.

Product / ConreteProduct

  • 제품 클래스에서는 모두 똑같은 인터페이스 구현
  • 그래야 제품을 사용할 클래스(클라이언트)에서 구상 클래스가 아닌 인터페이스에 대한 레퍼런스를 써서 객체를 참조할 수 있기 때문

ConreteCreator → ConreteProduct

  • 구상 클래스 인스턴스를 만들어내는 일은 ConreteCreator가 책임진다.
  • 실제 제품인 ConreteProduct 객체를 만들어내는 방법을 알고 있는 클래스는 ConreteCreator 클래스 뿐

병렬 클래스 계층 구조

  • Product, Creator 클래스 둘 다 추상 클래스로 시작하고, 그 클래스를 확장하는 구상 클래스들을 가지고 있다.
  • 구체적인 구현은 구상 클래스들이 책임지고 있다.
  • 구상 생산자 클래스에는 특정 구상 제품군에 대한 모든 지식이 캡슐화 되어 있으며 팩토리 메소는 이러한 지식을 캡슐화 시키는데 있어서 가장 핵심적인 역할을 맡고 있다.

Simple Factory vs Factory Method Pattern

  • 뉴욕과 시카고 분점을 만들 때 간단한 팩토리를 사용했다고 할 수 있지 않을까??
    • 비슷하긴 하지만 방법이 조금 다르다.
    • 구상 클래스를 만들 때 createPizza() 추상 메소드가 정의되어 있는 추상 클래스를 확장해서 만들었다는 점이 중요한 차이
      • createPizza() 메소드에서 어떤 일을 할지는 각 분점에서 결정한다.
    • 간단한 팩토리를 사용할 때는 팩토리가 PizzaStore 안에 포함되는 별개의 객체였다는 큰 차이점이 존재한다.
  • simple factory
    • 일회용 처방에 불과하다.
    • 객체 생성을 캡슐화하는 방법을 사용하긴 하지만 생성하는 제품을 마음대로 변경할 수 없기 때문에 강력한 유연성을 제공하진 못한다.
  • factory method pattern
    • 어떤 구현을 사용할지를 서브클래스에서 결정하는 프레임워크를 만들 수 있다.
    • 강력한 유연성을 제공한다.

의존적인 PizzaStore

  • 팩토리를 사용하지 않는 PizzaStore 클래스는 피자 객체가 의존하고 있는 구상 피자 객체의 개수만큼 의존하게 된다.
  • 왜냐하면 객체 인스턴스를 직접 만들어 구상 클래스에 의존하기 때문이다.

https://msyu1207.tistory.com/entry/4장-헤드퍼스트-디자인-패턴-팩토리-패턴

  • 적용 전

https://msyu1207.tistory.com/entry/4장-헤드퍼스트-디자인-패턴-팩토리-패턴

  • 팩토리 메소드 패턴을 적용한 다이어 그램

DIP : 의존성 역전 원칙

  • 구상 클래스에 대한 의존성을 줄이는 것이 좋다는 내용을 정리해 놓은 객체지향 디자인 원칙
  • 이 원칙은 다음과 같이 일반화 시킬 수 있다.
    • 추상화된 것에 의존하도록 만들어라
    • 구상 클래스에 의존하도록 만들지 않아야 한다.
  • 이 원칙하고 “특정 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다”는 원칙이 똑같다고 생각할 수 있다.
    • 비슷하긴 하지만 dip에서는 추상화를 더 많이 강조한다.
    • 해당 원칙에는 고수준 구성요소가 저수준 구성요소에 의존하면 안된다는 것이 내포되어 있다.
    • 항상 추상화에 의존하도록 만들어야 한다.
  • 그럼 고수준, 저수준은 어떤 의미일까??
    • 고수준 구성요소
      • 고수준 구성요소는 다른 저수준 구성요소에 의해 정의되는 행동이 들어있는 구성요소를 뜻한다.
      • ex) PizzaStore
      • PizzaStore의 행동은 피자에 의해 정의되기 때문에 고수준 구성요소라고 할 수 있다.
    • 저수준 구성요소
      • 이 때 PizzaStore에서 사용하는 피자 객체들은 저수준 구성요소라고 할 수 있다.
  • 팩토리 패턴을 사용하지 않은 기존의 PizzaStore 클래스는 구상 피자 클래스들에 의존하고 있다.
  • dip 원칙에 의하면, 구상 클래스처럼 구체적인 것이 아닌 추상 클래스나 인터페이스와 같이 추상적인 것에 의존하는 코드를 만들어야 한다.
    • 이 원칙은 고수준 모듈과 저수준 모듈에 모두 적용될 수 있다.
  • dip 원칙을 지키는데 도움이 될만한 가이드라인
    1. 어떤 변수에도 구상 클래스에 대한 레퍼런스를 저장하지 않는다.
    2. 구상 클래스에서 유도된 클래스를 만들지 않는다.
    3. 베이스 클래스에 이미 구현되어 있던 메소드를 오버라이드 하지 않는다.
    • 해당 가이드들은 항상 지켜야 하는 규칙이 아니라 지향하는 바를 나타낸 것
    • 이 가이드라인을 완벽하게 따를 순 없다.
728x90
728x90

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

데코레이터 패턴을 이용하면 객체에 추가 요소를 동적으로 더할 수 있습니다. 데코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.

개요

OO커피는 단기간에 급속도로 성장한 대형 커피 전문점이다. 빠르게 성장한 만큼, 음료들을 모두 포괄하는 주문 시스템이 이제서야 개발되려고 하는 상황이다. 처음 시스템 시작할 무렵에 만들어진 클래스는 위의 사진과 같다.

Beverage : 음료를 나타내는 추상 클래스, 모든 음료는 해당 클래스의 서브클래스

  • description : 해당 인스턴스 변수는 서브클래스에서 설정되며, 음료의 설명이 저장된다.
  • cost() : 추상 메서드이며, 서브 클래스에서 해당 메서드를 구현해야 한다.

하지만 음료에는 스팀 우유, 두유, 모카, 휘핑 크림 등 옵션을 추가할 때마다 가격이 달라진다. 이러한 경우를 모두 고려한다면??

이처럼 클래스 개수가 폭발적으로 증가하게 된다. 만약 우유나 크림 가격이 인상된다면?? 한눈에 보기에도 이렇게 수많은 클래스를 관리하기는 힘들 것이다.

그러면 슈퍼 타입인 Beverage 클래스에 인스턴스 변수로 관리하면 안될까??

  • milk, soy, mocha, ... ,whip : 각 추가 요소에 해당하는 인스턴스 변수 추가
  • cost() : 각 음료 서브 클래스의 인스턴스마다 추가 사항에 해당하는 추가 가격까지 포함할 수 있도록 기본 음료 값을 가져와서 오버라이드 하기위해 추상 메소드가 아닌 구현 메소드로 수정
  • 부울 인스턴스 변수를 위한 게터, 세터
public class Beverage {

    String description;
    boolean hasMilk, hasSoy, hasMocha;
    double milkCost, soyCost, mochaCost;

    public double cost() {
        double condimentCost = 0;
        if (getHasMilk()) {
            condimentCost += milkCost;
        }
        if (getHasSoy()) {
            condimentCost += soyCost;
        }
        if (getHasMocha()) {
            condimentCost += mochaCost;
        }
        return condimentCost;
    }

    // get, set..
    public boolean getHasMilk() {
        return hasMilk;
    }

    public boolean getHasSoy() {
        return hasSoy;
    }

    public boolean getHasMocha() {
        return hasMocha;
    }

}
public class DarkRoast extends Beverage{

    public DarkRoast() {
        description = "다크 로스트";
    }

    @Override
    public double cost() {
        return super.cost() + 3500;
    }
}

아까와 같은 클래스 폭발을 막게되었다. 하지만 아직 확신이 서지 않는다. 어떤 문제점이 있을 수 있을까??

  • 음료에 추가되는 옵션(우유, 휘핑, 모카 등)의 가격이 바뀔때마다 코드를 수정해야 한다.
  • 음료에 추가되는 옵션의 종류가 많아지면 그 때마다 메소드를 추가하고, 슈퍼 클래스의 cost() 메소드를 수정해야 한다.
  • 새로운 음료가 추가되는 경우, 옵션이 없는 경우와 우유가 들어가지 않는 음료임에도 관련된 멤버들을 상속받게 된다.
  • 만약 샷이라는 옵션이 추가되고 샷을 두번 추가한 음료는 어떻게 해야할까??

상속은 객체지향 디자인의 강력한 요소 중 하나지만, 이처럼 상속을 사용한다고 해서 무조건 유연하고 관리하기 쉬운 디자인이 만들어지지 않는다. 그 이유는 서브 클래스를 만드는 방식으로 행동을 상속 받으면 해당 행동은 컴파일시에 완전히 결정되고 모든 서브클래스에서 슈퍼 클래스의 멤버들을 상속 받아야 하기 때문이다. 하지만 composite를 통해서 객체의 행동을 실행 중에 동적으로 설정하는 방법을 사용한다면, 즉 객체를 동적으로 구성하면, 기존 코드를 수정하는 대신 새로운 코드를 추가하는 방식으로 새로운 기능을 추가할 수 있다. 기존 코드는 수정되지 않으므로(변경에 대해서는 닫혀있으므로) 버그가 생기거나 사이드 이펙트를 방지하면서 새로운 기능을 추가(확장에 대해서는 열려있는)할 수 있는 것이다.

데코레이터 해보자!

이제 음료에서 추가되는 옵션이 있는 경우 해당 음료를 데코레이터 하는 방식으로 수정해보자. 만약 모카와 휘핑 크림을 추가한 다크 로스트 커피는 다음처럼 할 수 있을 것이다.

  1. DarkRoast 객체를 가져온다
  2. Mocha 객체로 장식한다.
  3. Whip 객체로 장식한다.
  4. cost() 메소드를 호출한다. 이때 추가 옵션의 가격을 계산하는 일은 해당 객체들에게 위임한다.

이 때 장식하고 위임하는 방법은 해당 객체를 래퍼 객체라고 생각하면 쉽다.

이렇게 가장 바깥쪽에 있는 데코레이터 객체에서 cost()를 호출하고, 해당 객체가 장식하고 있는 객체에게 가격을 위임한다. 위임한 객체에게 가격의 값을 얻으면, 자신의 가격을 더한 다음 리턴하는 것이다.

여기서 중요한 점은 데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 것 외에 원하는 추가적인 작업을 수행할 수도 있다는 저이다.

데코레이터 패턴

데코레이터 패턴에서는 객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는 것을 통해서 기느을 유연하게 확장할 수 있는 방법을 제공한다.

Compent

  • 각 구성요소는 직접 사용할 수도 있고 데코레이터로 감싸져서 쓰일 수도 있다.
  • ex) Beverage 클래스

ConcreteComponent

  • 해당 클래스에 새로운 행동을 동적으로 추가하게 된다.

Decorator

  • 데코레이터는 자신이 장식할 Component와 같은 인터페이스 또는 추상 클래스를 구현한다.
  • 각 데코레이터 안에는 Component 객체가 들어있다. 즉, 데코레이터에는 구성요소에 대한 레퍼런스가 들어있는 인스턴스 변수를 가진다.

ConcreteDecorator

  • ConcreteDecorator 에는 데코레이터가 감싸고 있는 Component 객체를 위한 인스턴스 변수가 있다.
  • Decorator는 Component의 상태를 확장할 수 있다.
  • Decorator에서 새로운 메소드를 추가할 수도 있다. 하지만 일반적으로 새로운 메소드를 추가하는 대신 Component의 메소드를 호출하기 전, 후에 별도의 작업을 처리하는 방식으로 새로운 기능을 추가한다.

Beverage 클래스 다이어그램

public abstract class Beverage {

    String description = "";

    public abstract double cost();

    public String getDescription() {
        return description;
    }
}

public class Espresso extends Beverage {

    public Espresso() {
        description = "에스프레소";
    }

    @Override
    public double cost() {
        return 3500;
    }
}

public abstract class CondimentDecorator extends Beverage {
    public abstract String getDescription();
}

public class Mocha extends CondimentDecorator {

    Beverage beverage;

    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public double cost() {
        return 500 + beverage.cost();
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", 모카";
    }
}
public class StarbuzzCoffee {

    public static void main(String[] args) {
        Beverage beverage = new Espresso();
        System.out.println(beverage.getDescription() + ": " + beverage.cost() + " won");

        Beverage beverage2 = new Espresso();
        beverage2 = new Mocha(beverage2);
        beverage2 = new Mocha(beverage2);
        System.out.println(beverage2.getDescription() + ": " + beverage2.cost() + " won");
    }

}

데코레이터 패턴을 적용한 코드는 아까의 문제점이 사라진 오류의 코드지만 저런형태로 관리하게 될경우 마지막 Soy를 빼먹는다던가 실수로 두번넣는 경우가 생기게됩니다. 팩토리 패턴과 빌더 패턴을 이용해서 더 쉽게 객체를 만드는 방법이 존재한다.

주의해야 할 점

  • 특정 ConcreteComponent 타입을 바탕으로 작업을 처리하는 코드에 데코레이터 패턴을 적용하면 제대로 작동하지 않는다. (ConcreteDecorator로 감싸져 있기 때문)
  • 만약 여러 단계의 데코레이터를 파고 들어가서 어떤 작업을 해야 한다면, 데코레이터 패턴의 의의와 어긋나는 것이다.
  • 데코레이터 패턴에서는 특정한 추상 구성요소를 지정할 필요가 없다. 인터페이스를 사용해도 무방하다.

마무리

  • 상속을 통한 확장은 디자인의 유연성 면에서 좋지 않을 수 있다.
  • 기존 코드를 수정하지 않고도 행동을 확장하는 방법이 필요하다 (OCP)
  • 합성과 위임을 통해서 실행중에 새로운 행동을 추가할 수 있다.
  • 상속 대신 데코레이터 패턴을 통해 행동을 확장할 수 있다.
  • 데코레이터 패턴에서는 구상 구성요소를 감싸주는 데코레이터를 사용한다.
  • 데코레이터 클래스의 형식은 그 클래스가 감싸고 있는 클래스의 형식을 반영한다.
  • 데코레이터에서는 자기가 감싸고 있는 구성요소의 메소드를 호출한 결과에 새로운 기능을 더함으로써 확장한다.
  • 구성요소를 감싸는 데코레이터의 개수에는 제한이 없다.
  • 구성요소의 클라이언트 입장에서는 데코레이터의 존재를 알 수 없다.
    • 따라서 클라이언트에서 구성 요소의 구체적인 타입에 의존하게 되는 경우는 다시 생각해봐야한다.
  • 데코레이터 패턴을 사용하면 객체들이 많이 추가될 수 있고, 코드가 복잡해질 수 있다.
728x90
728x90

옵저버 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.

개요

실제 기상 정보를 수집하는 장비인 기상 스테이션과 기상 스테이션으로부터 오는 데이터를 추적하는 객체인 WeatherData, 그리고 사용자에게 현재 기상 조건을 보여주는 디스플레이, 세 요소로 이루어진다.

WeatherData 객체에서는 기상 스테이션으로부터 데이터를 가져올 수 있다. 데이터를 가져온 후에는 디스플레이 장비에 세 가지 항목을 표시할 수 있다.

  • 현조 조건(온도, 습도, 압력)
  • 기상 통계
  • 기상 예보

WeatherData 객체를 사용하여 디스플레이 장비에서 위의 3가지 요소를 갱신해 가면서 보여주는 애플리케이션을 만들어보자

주어진 WeatherData 클래스와 상황

  • 세가지의 게터 메소드는 각각 가장 최근에 측정된 온도, 습도, 기압 값을 리턴하는 메소드
  • measurementsChanged 메소드를 현재 조건, 기상 통꼐, 기상 예측 3가지 디스플레이를 갱신할 수 있도록 구현해야 한다.
  • 시스템은 확장 가능해야 한다. 추후 디스플레이 항목들은 추가/제거될 수 있다.

초기 구현

public class WeatherData {

    // 인스턴스 변수 선언

    public void measurementsChanged() {
        float temp = getTemperature();
        float humidity = getHumidity();
        float pressure = getPressure();

        currentConditionsDisplay.update(temp, humidity, pressure);
        statisticsDisplay.update(temp, humidity, pressure);
        forecastDisplay.update(temp, humidity, pressure);

    }

}

위의 코드의 문제는 무엇일까??

  • 인터페이스가 아닌 구체적인 구현을 바탕으로 코딩하고 있다.
  • 새로운 디스플레이 항목이 추가될 때마다 코드를 변경해야 한다.
  • 실행중에 디스플레이 항목을 추가/제거할 수 없다.
  • 바뀌는 부분을 캡슐화하지 않았다.

구체적인 구현에 맞춰서 코딩되어 있기 때문에 코드를 고치지 않고는 다른 디스플레이 항목을 추가하거나 제거할 수 없고 디스플레이에 항목들을 갱신하여 업데이트하는 부분들은 바뀔 수 있는 부분이므로 캡슐화해야 한다.

이제 옵저버 패턴에 대해서 알아보자

옵저버 패턴

쉽게 생각해서 신문 구독 메커니즘과 비슷한다. 즉 출판사 + 구독자 = 옵저버 패턴 인 것

출판사를 주제 or 주체(subject), 구독자를 옵저버(observer)라고 부른다.

  • subject 객체에서 일부 데이터를 관리
  • subejct의 데이터가 달라지면 옵저버한테 소식과 데이터가 전달된다.
  • observer 객체들은 subject 객체를 구독(등록)하고 있으며 subject의 데이터가 바뀌면 갱신 내용을 전달받는다.

옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.

  • 일대다 관계는 subject와 observer에 의해 정의되고 observer는 subject에 의존한다.
  • 옵저버 패턴을 구현하는 방법에는 여러가지가 있지만 대부분 subject 인터페이스와 observer 인터페이스를 사용한 클래스 디자인을 바탕으로 한다.

Subject 인터페이스

  • 객체에서 옵저버로 등록하거나 옵저버 목록에서 탈퇴하고 싶을 때 이 인터페이스에 있는 메소드를 사용한다.

ConcreteSubject

  • Subject 역할을 하는 Concrete 클래스에서는 항상 subject 인터페이스를 구현해야 한다.
  • subject 클래스에서는 등록 및 해지를 위한 메소드 외에 상태가 바뀔때마다 모든 옵저버들에게 연락을 하기 위한 notifyObservers() 메소드도 구현해야 한다.
  • subject 클래스에는 상태를 설정하고 알아내기 위한 세터/게터 메소드가 있을 수도 있다.

Observer 인터페이스

  • observer가 될 가능성이 있는 객체에서는 반드시 observer 인터페이스를 구현해야 한다.
  • observer 인터페이스에는 subject의 상태가 바뀌었을 때 호출되는 update() 메소드 밖에 없다.

ConcreteObserver

  • Observer 인터페이스만 구현한다면 어떤 클래스든 옵저버 클래스가 될 수 있다.
  • 각 옵저버는 특정 주제 객체에 등록을 해서 연락을 받을 수 있다.

옵저버 패턴에서 상태를 저장하고 지배하는 것은 subject 객체이다. 따라서 상태가 들어있는 객체는 하나만 존재하고, 옵저버는 반드시 상태를 가지고 있어야 하는 것은 아니기 때문에 옵저버는 여러 개가 있을 수 있으며 subject 객체에서 상태가 바뀌었다는 알려주기를 기다리는, subject에 의존적인 성질을 가진다.

따라서 하나의 subject와 여러개의 observer가 연관된 일대다 관계가 성립하고 해당 의존을 통해 여러 객체에서 동일한 데이터를 제어하도록 할 수 있다.

느슨한 결합

추가로 옵저버 패턴의 장점은 subject와 observer가 느슨하게 결합되어 있는 디자인을 제공하는 것이다. 두 객체가 느슨하게 결합되어 있다는 것은, 해당 객체들이 상호작용을 하지만 서로에 대해 잘 모른다는 것을 의미한다.

subject가 observer에 대해 아는 것은 해당 옵저버가 observer 인터페이스를 구현한다는 것 뿐이다

  • 옵저버의 구상 클래스, 어떤 행동을 하는지 등, 나머지는 알 필요가 없다.

observer는 언제든지 추가 가능하다

  • subject는 observer 인터페이스를 구현하는 객체 목록에먼 의존하기 때문에 실행중에 한 옵저버를 다른 옵저버로 바꿔도 되고, 언제든지 새로운 옵저버를 추가하거나 삭제할 수 있다.

새로운 형식의 observer를 추가하려고 할 때도 subject를 변경할 필요가 없다.

  • observer 인터페이스에 의조하기 때문에 새로운 옵저버 구상 클래스가 생겨도 문제 없다.

subject와 observer는 서로 독립적으로 재사용할 수 있다.

  • 다른 용도로 활용할 일이 있어도 문제 없다. 느슨하게 결합되어 있기 때문

subject와 observer에 변경이 생겨도 서로에게 영향이 미치지 않는다.

  • 마찬가지로 느슨한 결합 덕분

옵저버 패턴 적용

  • Subject, Observer 인터페이스 생성
  • WeatherData는 Subject 인터페이스를 구현하도록 수정
  • 모든 기상 구성요소에서 Observer 인터페이스를 구현하도록 수정
    • subject 객체에서 갱신된 데이터를 전달할 수 있도록 메소드 제공
  • 모든 디스플레이 항목에서 구현하는 DisplayElement 인터페이스를 하나 더 추가
    • 측정값을 바탕으로 각각 다른 내용을 표시하는 메소드인 display() 메소드 추가
public interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObserver();
}

public interface Observer {
    void update(float temperature, float humidity, float pressure);
}

public interface DisplayElement {
    void display();
}
import java.util.ArrayList;
import java.util.List;

public class WeatherData implements Subject {

    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
        this.observers = new ArrayList<>();
    }

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

    @Override
    public void removeObserver(Observer o) {
        int i = observers.indexOf(o);
        if(i >= 0) {
            observers.remove(i);
        }
    }

    @Override
    public void notifyObserver() {
        observers.forEach(observer -> observer.update(temperature, humidity, pressure));
    }

    public void measurementsChanged() {
        notifyObserver();
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
}
public class CurrentConditionsDisplay implements Observer, DisplayElement {

    private float temperature;
    private float humidity;
    private Subject weatherData;

    public CurrentConditionsDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        this.weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }

    @Override
    public void display() {
        System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
    }
}
public class WeatherStation {

    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        CurrentConditionsDisplay conditionsDisplay = new CurrentConditionsDisplay(weatherData);
        weatherData.setMeasurements(70, 60, 30.4f);
        weatherData.setMeasurements(55, 50, 15.4f);
        weatherData.setMeasurements(90, 70, 40.4f);
    }

}

데이터 전달 시 두가지 방식

현재는 subject의 상태가 변경될 때마다 observer에게 알려주고(push) 있다. 옵저버 입장에서는 필요한 상황에서만 주체의 상태를 가져오는 방식(pull)이 더 편할 수도 있지 않을까??

이처럼 옵저버 패턴은 PUSH 방식과 PULL 방식으로 구분할 수 있다.

PUSH 방식 : 주제의 내용이 변경될 때마다 구독자에게 알려주는 방식

PULL 방식 : 구독자가 필요할 때마다 주제에게 정보를 요청하는 방식

또한 자바에서 몇 가지 API를 통해 자체적으로 옵저버 패턴을 지원한다. 일반적으로 java.util 패키지에 들어있는 Observer 인터페이스와 Observable 클래스이다. 해당 내장 클래스들은 푸시 방식과 풀 방식 모두 가능하다.

그렇다면 자바 내장 클래스를 이용하여 풀 방식으로 수정해보자

Pull 방식으로 수정(with 자바 내장 옵저버 패턴)

  • Observable은 인터페이가 아니라 클래스이므로 WeatherData 클래스에서 해당 클래스를 상속하면서 메소드들을 상속받는다.
  • setChange() 제공
    • 해당 메소드는 상태가 바뀌었다는 것을 밝히기 위한 용도로 사용된다. setChange() 메소드가 호출되지 않은 상태에서 notifiyObservers()가 호출되면 옵저버들에게 연락이 가지 않는다. 해당 메소드를 조건에 따라서 적절히 호출하여 옵저버들에게 연락이 가는 것을 제어할 수 있다.
  • Observer 인터페이스는 앞에서 만든 클래스와 거의 똑같다.

바뀐 WeatherData

import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import observer.after.Observer;
import observer.after.Subject;

public class WeatherDataObservable extends Observable {

    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherDataObservable() {
    }

    public void measurementsChanged() {
        setChanged();
        notifyObservers(); // pull 방식, push 방식인 경우 notifyObservers(Object arg);
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    // pull 방식이므로 옵저버가 주체 객체의 상태를 알아야하므로 필요하다.
    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}
import java.util.Observable;
import java.util.Observer;
import observer.after.DisplayElement;
import observer.after2.WeatherDataObservable;

public class CurrentConditionsDisplayObserver implements Observer, DisplayElement {

    private float temperature;
    private float humidity;
    Observable observable;

    public CurrentConditionsDisplayObserver(Observable observable) {
        this.observable = observable;
        observable.addObserver(this);
    }

    @Override
    public void update(Observable o, Object arg) {
        if(o instanceof WeatherDataObservable) {
            WeatherDataObservable weatherData = (WeatherDataObservable) o;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            display();
        }
    }

    @Override
    public void display() {
        System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
    }
}

java.util.Observable의 단점

Observer와 Observable은 Java SE 9 버전부터 Deprecated 되었다. 그 이유는 무엇일까?

  • Observer와 Observable이 제공하는 이벤트 모델이 제한적이다.
  • Observable의 notify는 순서를 보장할 수 없으며, 상태 변경은 1:1로 일치하지 않는다.
  • 더 풍부한 이벤트 모델은 java.beans 패키지가 제공하고 있다.
  • 멀티 스레드에서의 신뢰할 수 있고 순서가 보장된 메시징은 java.util.concurrent 패키지의 concurrent 자료 구조들 중 하나를 골라 쓰는 편이 낫다.
  • reactive stream 스타일 프로그래밍은 Flow API를 쓰기를 권한다.

Observable의 문제는 헤드 퍼스트 디자인 패턴에서도 지적하고 있다

  • Observable이 interface가 아니라 class이다.
    • 다른 클래스를 상속하는 클래스가 Observable을 상속할 수 없다. 따라서 재사용성에 제약이 생긴다.
    • 내장된 Observer API하고 잘 맞는 클래스를 직접 구현하는 것이 불가능하다.
  • Obserable 클래스의 핵심 메소드를 외부에서 호출할 수 없다.
    • setChanged() 메소드가 protected으로 선언되어 있기 때문이다.
    • 상속보다 구성을 사용한다는 디자인 원칙을 위배한다.

마무리 핵심 정리

  • 옵저버 패턴에서는 객체들 사이에 일대다 관계를 정의한다.
  • 주체 객체는 동일한 인터페이스를 써서 옵저버에 연락한다.
  • 주체와 옵저버는 서로 느슨한 결합
  • 주체에서 데이터를 보내는 푸시방식과 옵저버가 데이터를 가져오는 풀방식이 있다.
    • 풀 방식 추천
  • 옵저버들한테 연락을 돌리는 순서에 절대로 의존하면 안 된다.
    • 만약 의존하도록 했다면 잘못된 것, 느슨한 결합이라고 볼 수 없다.
728x90
728x90

전략 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

전략 패턴 은 알고리즘군을 정의하고 각각의 알고리즘을 캡슐화하며 교환해서 사용할 수 있도록 만든다. 전략을 사용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

개요

오리 연못 시뮬레이션 게임에서 오리는 헤엄도 치고 꽥꽥거리는 소리도 내는 매우 다양한 오리 종류를 보여줄 수 있다. 가장 처음에는 표준적인 객체지향 기법을 사용하여 Duck이라는 수퍼클래스를 만든 다음, 그 클래스를 확장하여 다른 모든 종류의 오리를 생성

예제 코드

public abstract class Duck {

    void quack() {
        System.out.println("duck quack");
    }

    void swim() {
        System.out.println("duck swim");
    }

    abstract void display();

}

class MallardDuck extends Duck {

    @Override
    void display() {
        System.out.println("MallardD Duck");
    }
}

class RedheadDuck extends Duck {

    @Override
    void display() {
        System.out.println("Redhead Duck");
    }
}

이제 오리들이 날아다닐 수 있도록 해야한다고 가정했을 경우 어떻게 해야할까??

public abstract class Duck {

    void quack() {
        System.out.println("duck quack");
    }

    void swim() {
        System.out.println("duck swim");
    }

    abstract void display();

    void fly() {
        System.out.println("duck fly");
    }

}

슈퍼클래스에 fly()메소드를 추가함으로써 모든 서브 클래스에서 fly()를 상속받도록 수정

class MallardDuck extends Duck {

    @Override
    void display() {
        System.out.println("Mallard Duck");
    }
}

class RedheadDuck extends Duck {

    @Override
    void display() {
        System.out.println("Redhead Duck");
    }
}

class RubberDuck extends Duck {

    @Override
    void quack() {
        System.out.println("rubber duck quack");
    }

    @Override
    void display() {
        System.out.println("Rubber Duck");
    }
}

하지만 해당 변경사항은 모든 오리가 날 수 있지 않다는 것을 간과했다. 고무로 된 오리 인형 클래스에도 비행 기능하게 된 것이다. 결론적으로 Duck 슈퍼클래스에 fly() 메소드가 추가되면서 일부 서브클래스에는 적합하지 않은 행동이 추가될 수 있는 사이드 이펙트가 발생하게 된 것이다.

RubberDuck의 quack() 메소드처럼 fly() 오버라이드를 해서 아무것도 하지 않게 한다면 가능은 하지만 어떤 부작용이 있을까??

상속의 문제점

이처럼 Duck 행동을 제공하는데 있어서 상속을 사용할 때의 단점은 많다.

  • 서브 클래스에서 코드가 중복
  • 모든 서브클래스의 행동을 알기가 어려움
  • 코드를 변경했을 때 원치 않는 서브클래스들에게 영향을 끼칠 수 있음
  • 실행시에 특징 변경 어려움
  • 모든 오리의 행동을 알기 힘듬

인터페이스?

그렇다면 상속 대신 Flyable, Quackable 인터페이스를 사용하는 방법은 어떨까??

서브클래스에서 Flyable, Quackable 인터페이스를 구현함으로써 고무오리의 비행기능 같은 문제점을 해결할 수 있지만, 코드 재사용성과 관리측면에서 더욱 큰 문제점이 발생하게 된다. (Java 8이하라고 가정)

문제를 명확하게 파악하기

디자인원칙1 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리킨다.

결국 가장 중요한 것은 달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 캡슐화함으로써 나머지 부분에 영향을 미치지 않도록 한다면 시스템의 유연성을 향상시키는 것이다.

즉 확장에는 열려있고 수정에 대해서는 닫혀있도록 하는 것!!

모든 패턴은 시스템의 일부분을 다른 부분과 독립적으로 변화시킬 수 있는 방법을 제공하기 위한 것이다.

*스트레티지 패턴 (Straetegy 패턴) *

Duck 예제에서 달라지는 부분 뽑아보기

Duck 클래스에서 나는 행동과 꽥꽥거리는 행동을 추출하여 행동을 나타낼 클래스 집합을 새로 추가

  • 최대한 유연하게 만들어야 한다.
  • 오리의 행동을 동적으로 바꾸고싶다
    • 세터 메소드를 포함시켜서 동적으로 바꿀 수 있도록 한다면 좋을 것이다.

디자인원칙2 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다

각 행동들을 인터페이스 (FlyBehavior, QuackBehavior)로 표현하고 행동을 구현할 때 Duck 클래스에서 구현하느 것이 아닌 구체적인 행동 클래스에서 구현함으로써 특정 행동은 Duck 국한되지 않는다.

public interface FlyBehavior {
    void fly();
}

public class FlyNoWay implements FlyBehavior{

    @Override
    public void fly() {
        System.out.println("날 수 없다");
    }
}

public class FlyWithWings implements FlyBehavior{

    @Override
    public void fly() {
        System.out.println("나는 방법을 구현");
    }
}
public interface QuackBehavior {

    void quack();
}

public class Quack implements QuackBehavior {

    @Override
    public void quack() {
        System.out.println("꽥");
    }
}

public class MuteQuack implements QuackBehavior {

    @Override
    public void quack() {
        System.out.println("소리 낼 수 없어요");
    }
}

public class Squeak implements QuackBehavior {

    @Override
    public void quack() {
        System.out.println("삑삑");
    }
}

이로써 비행과 꽥꽥거리는 행동들은 Duck 클래스 안에 숨겨져 있지 않으므로 다른 객체에서도 이러한 행동들을 재사용할 수 있다. 그리고 기존의 Duck 클래스들을 전혀 수정하지 않고도 새로운 행동을 추가할 수 있다.

상속의 단점을 해결하는 것 뿐만 아니라 재사용성을 가질 수 있다.

결과

이제 Duck 클래스는 행동을 다른 클래스에 위임함으로써 특정 행동을 할 수 있게 되었다.

public abstract class Duck {

    private FlyBehavior flyBehavior;
    private QuackBehavior quackBehavior;

    public Duck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
        this.flyBehavior = flyBehavior;
        this.quackBehavior = quackBehavior;
    }

    public void performFly() {
        flyBehavior.fly();
    }

    public void performQuack() {
        quackBehavior.quack();
    }

    public abstract void display();

    public void swim() {
        System.out.println("모든 오리는 수영한다.");
    }
}

class RubberDuck extends Duck {

    public RubberDuck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
        super(flyBehavior, quackBehavior);
    }

    @Override
    public void display() {
        System.out.println("Rubber Duck");
    }
}

class MallardDuck extends Duck {

    public MallardDuck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
        super(flyBehavior, quackBehavior);
    }

    @Override
    public void display() {
        System.out.println("Mallard Duck");
    }
}

class RedheadDuck extends Duck {

    public RedheadDuck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
        super(flyBehavior, quackBehavior);
    }

    @Override
    public void display() {
        System.out.println("Redhead Duck");
    }
}

결론

여태까지 사용했던 행동들을 일련의 행동이 아닌 알고리즘군으로 생각하고 똑같은 테크닉을 적용가능 하다.

ex) 지역별 세금 계산 방식 등등

각 오리에는 나는 행동과 꽥꽥하는 행동을 위임하기 위한 클래스들이 있다. 이처럼 두 클래스를 합치는 것을 구성(Composition)을 이용하는 것이라 한다. 오리 클래스에서는 행동을 상속받는 대신, 행동 객체로 구성됨으로써 행동을 부여받게 된다. 구성을 이용하면 유연성을 크게 향상시킬 수 있다. 캡슐화할 수 있도록 만들어주는 것 뿐만 아니라 실행시에 행동을 바꿀 수도 있게 해주기 때문이다.

디자인 원칙3: 상속보다는 구성을 활용한다.

참고 출처

728x90
728x90

디자인 패턴이란


  • 디자인 패턴 은 소프트웨어 디자인에서 일반적으로 발생하는 문제에 대한 일반적인 솔루션이다.
  • 이는 코드에서 반복되는 디자인 문제를 해결하기 위해 사용자 지정할 수 있는 미리 만들어진 청사진과 같다고 생각하면 좋다고 한다.
  • 패턴은 특정 문제를 해결하기 위한 일반적인 개념이다.
  • 패턴과 알고리즘 두 개념 모두 알려진 문제에 대한 일반적인 솔루션을 설명하기 때문에 비슷해 보이지만 다르다.
    • 알고리즘은 항상 어떤 목표를 달성할 수 있는 명확한 일련의 작업을 정의하고 요리법에 비유될 수 있다.
    • 패턴은 솔루션에 대한 보다 높은 수준의 설명이다. 프로그램 개발 시에 자주 부닥치는 애로 상황에 대한 일반적이고 재사용 가능한 추상화된 해결책이다.
      • 결과와 그 기능이 무엇인지 볼 수 있지만 정확한 구현 순서는 사용자에게 달려 있다.

디자인 패턴 장점


  1. 개발자(설계자) 간의 원활한 의사소통
    • 여러 디자인 패턴의 특성을 잘 알고 있어 문제해결 시 어떤 디자인 패턴을 사용하면 좋을지 해결책을 논의할 수 있다.
  2. 소프트웨어 구조 파악 용이
    • 디자인 패턴의 특성을 잘 알고 있기에 어떤 디자인 패턴이 설계할 때 사용되었는지 알면 소프트웨어 전체구조를 쉽게 파악 가능하다.
  3. 재사용을 통한 개발 시간 단축
    • 이미 만들어 놓은 디자인 패턴을 사용하므로 개발시간을 단축시킬 수 있다.
  4. 설계 변경 요청에 대한 유연한 대처
    • 사용자의 지속적인 추가 요청, 환경 변화 등의 설계 변경 요청에 쉽고 빠르게 대처 가능하다

디자인 패턴 단점


잘못된 대상을 문제로 할 수 있다.

  • 보통 패턴의 필요성은 추상화 능력이 부족한 컴퓨터 언어나 기술을 사용할 때 일어난다.
  • 이럴 때 패턴은 언어에 반드시 필요한 조잡한 인터페이스가 된다.

비효율적인 솔루션

  • 디자인 패턴의 아이디어는 이미 승인된 모범 사례를 표준화하려는 시도이다.
  • 원칙적으로 이것은 유익하게 보일 수 있지만, 실제로는 종종 불필요한 코드 중복을 초래한다.
  • "거의 만족스럽지 못한" 설계 패턴보다는 잘 구성된 구현을 사용하는 것이 더 효율적인 솔루션일 수 있다.

잘못되고 무분별한 사용

  • 패턴에 대해 배운 후 더 간단한 코드가 잘 작동하는 상황에서도 패턴을 어디에나 적용하려고 하면 비효율적일 수 있다.

패턴의 종류


패턴의 분류 기준

  • 목적(purpose)
    • 패턴이 무엇을 하는지에 따라 분류
    • 생성 패턴 : 객체의 생성 과정에 관여하는 패턴
    • 구조 패턴 : 클래스나 객체의 합성에 관한 패턴
    • 행동 패턴 : 클래스나 객체들이 상호작용하는 방법과 책임을 분산하는 방법을 정의하는 패턴
  • 범위(scope)
    • 패턴을 적용하는 범위에 따라 분류
    • 클래스 패턴 : 클래스와 서브클래스 간의 관련성을 다루는 패턴
      • 관련성은 주로 상속이며, 컴파일 타임에 정적으로 결정된다.
    • 객체 패턴 : 객체 관려성을 다루는 패턴으로서, 런타임에 변경할 수 있으며 더 동적인 성격을 가진다.
      • 대부분의 패턴들은 어느정도 상속을 이용한다.
      • 23개의 패턴중 대부분 객체 패턴이다.

)

생성 패턴(Creational Patterns)

인스턴스를 만드는 절차를 추상화하는 패턴, 다양한 객체 생성 메커니즘을 제공하여 기존 코드의 유연성과 재사용성을 향상시킨다.

  • 싱글톤 패턴(Singleton)
  • 추상팩토리 패턴(Abstract Factory)
  • 빌더 패턴(Builder)
  • 팩토리 메서드 패턴(Factory Method)
  • 원형 패턴(Prototype)

구조 패턴(Structural Patterns)

객체 및 클래스를 보다 큰 구조로 조립하는 동시에 이러한 구조를 유연하고 효율적으로 유지하는 패턴

  • 적응자 패턴(Adapter or Wrapper)
  • 브리지 패턴(Bridge)
  • 데코레이터 패턴(Decorator)
  • 퍼사드 패턴(Facade)
  • 프록시 패턴(Proxy)

행위 패턴(Behavioral Patterns)

어떤 처리의 책임을 어느 객체에 할당하는 것이 좋은지, 알고리즘을 어느 객체에 정의하는 것이 좋은지 다루는 패턴

  • 옵저버 패턴(Observer)
  • 상태 패턴(State)
  • 전략 패턴(Strategy)
  • 템플릿 메서드 패턴(Template method)
  • 비지터 패턴(Visitor)
  • 역할 사슬 패턴(Chain of Responsibility)
  • 커맨드 패턴(Command)
  • 인터프리터 패턴(Interpreter)
  • 이터레이터 패턴(Iterator)
  • 미디에이터 패턴(Mediator)

참고 문헌 및 출처


  • GoF의 디자인 패턴(개정판) / 에릭 감마, 리처드 헬름, 랄프 존슨, 존 블라시디스 공저 / 김정아 역
  • https://refactoring.guru/
728x90

+ Recent posts