10. 템플릿 메소드 패턴

AlmondGood·2022년 7월 23일
0

디자인패턴

목록 보기
11/16

템플릿 메소드 패턴(Template Method Patteren)

일정한 디자인의 물건을 만들어 내는 것이 형틀, 즉 템플릿입니다.
카카오 스토어에 촤라락 늘어져 있는 라이언 인형들이 바로 템플릿에 맞춰 제작된 것이죠.

템플릿 메소드 패턴이란, 알고리즘의 템플릿을 만들어 사용하는 패턴입니다.

알고리즘 내에 반복되는 부분을 분리해서 일정한 동작을 하도록 만들되,
어떤 동작들은 클래스에 따라 약간의 변형이 가능해야 합니다.


카페에서 라떼와 홍차를 만들 때를 예시로 들겠습니다.

라떼 클래스입니다.

class Latte {
  
    void prepareLatte() {

		// 아래 메소드들이 순서대로 실행됩니다.
        boilWater(); 		// 물 끓이기
        brewCoffeeGrinds(); // 커피 내리기
        pourInCup();		// 컵에 담기
        addSugarAndMilk();	// 설탕과 우유 추가하기
    }

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

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

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

    void addSugarAndMilk() {
        System.out.println("설탕과 커피 추가");
    }
}

홍차 클래스입니다.

class Tea {

    void prepareTea() {

    	// 아래 메소드들이 순서대로 실행됩니다.
        boilWater();		// 물 끓이기
        steepTeaBag();		// 차 우리기
        pourInCup();		// 컵에 담기
        addLemon();			// 레몬 추가하기
    }

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

    void steepTeaBag() {
        System.out.println("티백을 우려내는 중");
    }

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

    void addLemon() {
        System.out.println("레몬 추가");
    }
}

재료가 조금 바뀌는 것 외에는 모든 동작이 반복됩니다.
그렇다면 굳이 클래스 별로 동작을 만들어 둘 필요는 없겠죠.

반복되는 메소드들만 모아 실행하는 클래스를 만들겠습니다.




템플릿 메소드 패턴 구현

템플릿 메소드가 있는 클래스입니다.

추상 클래스로 만들어 수정될 일이 없는 메소드직접 구현하고,
수정 가능성이 있는 메소드서브 클래스에서 세부 사항을 정하도록(구현하도록) 합니다.

// 음료 클래스
abstract class Beverage {

	// 템플릿 메소드
    // final로 선언하여 외부에서 수정이 불가
    final void prepareBeverage() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

	// 수정 가능성이 없는 메소드
    void boilWater() {
        System.out.println("물 끓이는 중");
    }

    // 상속받는 클래스에 구현을 위임합니다.
    abstract void brew();

	// 수정 가능성이 없는 메소드
    void pourInCup() {
        System.out.println("컵에 따르는 중");
    }
	// 상속받는 클래스에 구현을 위임합니다.
    abstract void addCondiments();
}

알고리즘을 구현한 템플릿 메소드(prepareBeverage())는 final로 선언하여 오버라이딩하지 못하게 만들고,
공통 사항인 boilWater(), pourInCup()은 여기서 구현하고,
변경 사항이 있는 brew(), addCondiments()
서브 클래스로 구현을 위임**했습니다.

class Latte extends Beverage{

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

    @Override
    void addCondiments() {
        System.out.println("설탕과 우유 추가");
    }
}

class Tea extends Beverage{

    @Override
    void brew() {
        System.out.println("티백을 우려내는 중");
    }

    @Override
    void addCondiments() {
        System.out.println("레몬 추가");
    }
}

메인 함수입니다.

public static void main(String[] args) {

        Latte latte = new Latte();
        Tea tea = new Tea();

        latte.prepareBeverage();
        // 물 끓이는 중
		// ** 필터로 커피를 내리는 중
		// 컵에 따르는 중
		// ** 설탕과 우유 추가

        tea.prepareBeverage();
        // 물 끓이는 중
		// ** 티백을 우려내는 중
		// 컵에 따르는 중
		// ** 레몬 추가
}

추상 클래스를 상속받았기 때문에 필요한 부분만 오버라이딩하고,
공통적인 부분은 그대로 사용했습니다.


Hook()

만약 음료에 설탕이나 레몬을 추가하고 싶지 않다면 어떻게 해야될까요?
추가하고 싶다면 addCondiments()를 호출하고, 아니라면 호출하지 않으면 되겠죠.

abstract class Beverage {

    final void prepareBeverage() {
        boilWater();
        brew();
        pourInCup();

        // 오버라이딩된 hook()의 결과값에 따라 실행 여부가 결정됩니다.
        // true -> 첨가물 추가 / false -> 첨가물 X
        if (hook() == true) {
        	addCondiments();
        }
    }

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

    abstract void brew();

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

    // 오버라이딩해서 사용합니다.
    // 사용자의 입력에 따라 추가를 원하면 true,
    // 원하지 않는다면 false를 반환하게 만들면 됩니다.
    void hook() {
    	return true;
    }
}

의미없는 내용을 담은 메소드를 구현해 하위 클래스에서 오버라이딩하면
다양한 용도로 사용할 수 있게 됩니다.
이는 선택적인 것으로, 사용해도 되고, 사용하지 않아도 됩니다.

hook는 알고리즘이 실행되는 데 필수적인 요소가 되어서는 안 됩니다.
알고리즘에 끼어들지만, hook메소드 없이도 정상적으로 작동을 해야한다는 뜻이죠.


기존 방식과 비교

기존개선 후
동일한 알고리즘이 두 개의 클래스에서 실행됨알고리즘이 한 곳에 집중되어 불필요한 중복 제거
코드 재사용성 저하코드 재사용성 상승
새로운 기능 추가 시 불편새로운 기능 추가에 유연



의존성 부패

A라는 추상 클래스가 B 클래스의 메소드를 직접 호출하고, B는 또 A의 구성 요소를 필요로 하고 또 A는 ......

이런 식으로 고수준(추상 클래스 A)의 구성 요소와 저수준(클래스 B) 구성 요소가 서로 의존할 때 '의존성이 부패했다' 고 말합니다.

이것은 SOLID 원칙 중 DIP, 의존성 역전 원칙과 관련이 있습니다.

만약 추상 클래스가 B를 변수로 가져 B의 메소드를 호출해야 한다면, 의존성이 높아지게 됩니다.
위의 예시를 활용하자면,

abstract class Beverage {
	
	// 의존성 부패 ------------------
	Latte latte;
    Tea tea;

    Beverage(Latte latte, Tea tea) {
    	this.latte = latte;
        this.tea = tea;
    }

	// 똑같은 알고리즘이 나눠지게 되었습니다.
    // 기존의 방식과 다를 바가 없습니다.
    final void prepareLatte() {
        boilWater();
        latte.brew();
        pourInCup();
        latte.addCondiments();
    }

    final void prepareTea() {
        boilWater();
        tea.brew();
        pourInCup();
        tea.addCondiments();
    }
    // 의존성 부패 ------------------

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

    abstract void brew();

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

    }
}

분명 세부 메소드는 하위 클래스에서 구현했음에도 불구하고 추상 클래스가 하위 클래스에 강하게 영향을 받게 됩니다.


그렇기 때문에 의존성 역전 원칙에서

고수준의 클래스는 구체적이고 저수준의 클래스에 의존해서는 안 된다.

라고 하는 것입니다.



참고 자료

https://develogs.tistory.com/19

https://pizzasheepsdev.tistory.com/11?category=849060

profile
Zero-Base to Solid-Base

0개의 댓글