디자인 패턴 - 템플릿 메소드 패턴

이주오·2022년 8월 1일
0

디자인 패턴

목록 보기
12/12

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

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

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

개요 - 커피와 홍차 만들기

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

커피 만드는 법

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

0개의 댓글