소프트웨어 설계/디자인 패턴

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

나맘임 2025. 4. 25. 13:31

들어가며

템플릿 메서드 패턴은 디자인 패턴 종류 중 행동 패턴에 속한 것으로 상위 클래스에서 알고리즘의 정의를 미리 만들어둬서 하위 클래스들이 알고리즘의 구조를 바꾸지 못하게 함과 동시에 override를 할 수 있도록 하는 디자인 패턴입니다.

다른 디자인 패턴과 동일하게 코드를 재사용성을 높이고, 커스터마이징이 가능성을 열어뒀지만 전체적인 프로세스의 흐름은 동일하게 가져갈 수 있도록 해줍니다.

 

주요 키워드

추상 클래스

템플릿 메서드를 정의합니다.

(일반적으로 final을 붙여 하위 클래스에서 이를 수정하지 못하도록 합니다)

모든 하위 클래스가 공통으로 실행할 로직의 순서를 정의해둔 추상 메서드를 가지고 있습니다.

템플릿 메서드

중심에 있는 메서드로 주요 로직(알고리즘)의 실행 순서(시퀀스)를 수행하는 메서드입니다.

추상 또는 훅(Hook) 메서드

추상 메서드는 반드시 하위 클래스에 의해서 구현되어야 합니다.

추가로 추상 클래스에 훅 메서드를 넣어 사용할 수도 있습니다.템플릿 메서드의 영향이나 순서를 제어하고 싶을때 사용되는 메서드 형태를 의미합니다.

 

하위 클래스에서 override할 구체적인 로직

하위 클래스는 템플릿 메서드에 사용되는 추상 메서드를 반드시 구현해야 합니다.

 

 

이런 디자인 패턴은 이론적인 이야기보다 코드를 보면 이해가 빠릅니다.

바로 예시로 들어가보시죠!

음료를 만드는 Maker가 있다고 가정해보겠습니다.

이 기계는 다양한 종류가 있습니다.

커피 메이커, 홍차 메이커등이 있어요.

그러면 이들에게 동일한 로직이 있습니다.

 

1. 물을 끓인다.

2. brew 한다(커피는 내리고, 차는 티백이나 찻잎을 넣고 끓이겠죠)

3. 컵에 붓습니다.

4. 사용자가 원한다면 설탕, 우유 등을 넣을 수도 있습니다.

 

이 로직을 Maker 클래스를 만들 때마다 새로 작성하는 것은 너무 비효율적입니다.

그럴 때, 이 템플릿 메서드 패턴을 사용하면 좋습니다.

abstract class BeverageMaker {
    // Template method
    public final void makeBeverage(boolean wants) {
        boilWater();
        brew();
        pourInCup();
        if (customerWantsCondiments(wants)) { // Hook method
            addCondiments();
        }
    }

    abstract void brew();
    abstract void addCondiments();

    void boilWater() {
        System.out.println("Boiling water");
    }

    void pourInCup() {
        System.out.println("Pouring into cup");
    }
    boolean customerWantsCondiments(boolean wants) {
        return wants;
    }
}

 

추상 클래스이자 상위 클래스가 될 BeverageMaker를 만듭니다. 그리고 이를 단순히 상속받으면 중복 코드를 줄일 수 있게 됩니다.

makeBeverage()라는 템플릿 메서드를 만듭니다. 이는 모든 하위 클래스가 동일하게 작동하는데 사용됩니다.

그렇기때문에 final로 선언합니다. final로 선언하지 않으면 하위 클래스에서 이를 무시하고 override를 할 수 있기 때문입니다. 하위 클래스에서 상위 클래스를 덮어버리면 템플릿 메서드를 쓸 이유가 없겠죠..

 

여기서 추상 메서드인 brew()와 addCondiments()는 하위 클래스에서 반드시 선언해야 합니다. 템플릿 메서드에서 요구하기 때문이죠. customerWantsCondiments()가 바로 훅입니다. 하위 클래스에서 이 메서드를 사용해서 템플릿 메서드의 제어 흐름을 바꿀 수 있게 됩니다. if 문과 같은 걸 이용해서 말이죠.

class CoffeeMaker extends BeverageMaker {
    void brew() {
        System.out.println("Brewing coffee grounds");
    }

    void addCondiments() {
        System.out.println("Adding sugar and milk");
    }
}

 

하위 클래스에선 단순히 추상 메서드만 구현하면 됩니다.

public class Main {
    public static void main(String[] args) {
        BeverageMaker beverageMaker = new CoffeeMaker();
        beverageMaker.makeBeverage(true);
    }
}

 

외부에선 상위 클래스인 BeverageMaker로 선언하고 내부 인스턴스는 하위 클래스 중 하나로 만들어주면 됩니다.

그리고 템플릿 메서드인 makeBeverage를 불러온다면, 우리가 설계한대로 makeBeverage()가 작동하게 됩니다.

 

그러면 이걸 실제로 쓰는 예시도 봐야겠죠??

대표적인 예시로 자바의 AbstractList가 있습니다.

리스트를 구현하는 뼈대를 제공하는 API로 자바를 쓰신다면 반드시 만나게 되는 ArrayList 또한 AbstractList를 상속받아 구현하고 있습니다.

 

먼저 AbstractList의 템플릿 메서드부터 봅시다.

   public boolean contains(Object o) {
        Iterator<E> it = iterator();
        if (o==null) {
            while (it.hasNext())
                if (it.next()==null)
                    return true;
        } else {
            while (it.hasNext())
                if (o.equals(it.next()))
                    return true;
        }
        return false;
    }

List 안에 들어있는 것 중에 찾아야할 것이 있는지 확인하는 contains() 를 예시로 들 수 있습니다.

보시면 iterator() 메소드를 사용하고 있죠??

이게 바로 추상 메서드로 제공되고 있습니다.

    public abstract Iterator<E> iterator();

 

그렇기에 contains() 메서드는 위 BeverageMaker 클래스의 makeBeverage() 템플릿 메서드와 똑같은 상황인 겁니다!

ArrayList에선 이 iterator()를 구현하고 있는지 잠깐 보겠습니다.

    public Iterator<E> iterator() {
        return new Itr();
    }
    
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        // prevent creating a synthetic constructor
        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        public void forEachRemaining(Consumer<? super E> action) {
            Objects.requireNonNull(action);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i < size) {
                final Object[] es = elementData;
                if (i >= es.length)
                    throw new ConcurrentModificationException();
                for (; i < size && modCount == expectedModCount; i++)
                    action.accept(elementAt(es, i));
                // update once at end to reduce heap write traffic
                cursor = i;
                lastRet = i - 1;
                checkForComodification();
            }
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

아주 복잡하지만, 자기가 필요한대로 iterator()를 구현하고 있는 겁니다.

 

즉, 템플릿 메서드는 특정 로직을 정해놓고 하위 클래스가 이를 구현하도록 하여 특정 로직을 수행할 수 있게 만든 것입니다.

 

 

참고

https://refactoring.guru/design-patterns/template-method

 

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%ED%85%9C%ED%94%8C%EB%A6%BF-%EB%A9%94%EC%86%8C%EB%93%9CTemplate-Method-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90