티스토리 뷰

 

서비스를 개발하다 보면 같은 로직을 다른 방식으로 구현해야 하는 경우가 많다. 예를 들어 할인 정책을 등급에 따라 다르게 적용하거나 지도 서비스에서 최적의 경로를 찾을 때 비용이 적게 드는 경로 또는 소요 시간이 짧은 경로를 찾는 등, 할인 적용 또는 경로 찾기라는 알고리즘의 구현을 다르게 해야 하는 경우이다. 이와 같이 알고리즘의 큰 틀, 구조는 같으면서 일부, 세부적인 동작만이 다른 경우, 변화에 유연한 구조와 낮은 코드 중복도를 위해 적용할 수 있는 디자인 패턴이 있다. 바로 템플릿 메소드 패턴과 전략 패턴인데, 이번 글에서는 템플릿 메소드 패턴에 대해 자세히 알아보고자 한다. 

 

 

템플릿 메소드 패턴과 전략 패턴은 디자인 패턴 분류 중 행위(Behavioral) 패턴에 속한다. 행위 패턴은 '객체들 간 상호 작용하는 방법이나 책임 분배 방법을 정의하는 패턴' 이라고 다소 어렵게 정의되어 있는데, 단순히 '하나의 작업을 수행하기 위해 여러 객체들 간의 효율적인 협력 방법을 정의한 패턴'이라고 생각하면 된다. 즉 기존에는 하나의 객체가 한 작업을 모두 처리하였지만 이는 SOLID 원칙에 위배되었고, 따라서 여러 객체들을 등장시키고 각 객체들이 자신에게 할당된 책임을 수행함으로써 '작업 처리'라는 공통의 목표를 효율적으로 달성하는 것이 바로 행위 패턴의 목적이라 할 수 있다. 템플릿 메소드 패턴과 전략 패턴 모두 결국 하나의 작업을 수행하지만, 단지 이를 위해 등장하는 객체들의 구성이나 객체 간 책임 분배 방법이 조금씩 다를 뿐이다. 이제 템플릿 메소드 패턴의 개념에 대해 알아보고 간단한 예제를 가지고 코드로도 구현해보자. 

 

 

 

Template Method Pattern

디자인 패턴은 패턴을 구현하기 위한 실제 클래스들의 구조 보다도 의도, 즉 해당 패턴을 어떤 상황에, 왜 사용해야 하는지 이유를 아는 것이 더 중요하다. 이런 의미에서 템플릿 메소드 패턴의 목적은 다음과 같다.

한 작업에서 알고리즘의 골격을 정의하고 일부 단계의 구현은 하위 클래스로 연기합니다. 템플릿 메서드를 사용하면 하위 클래스가 알고리즘의 구조를 변경하지 않고도 알고리즘의 특정 단계를 재정의할 수 있습니다.

 

즉 상위 클래스(추상 클래스)에서는 알고리즘의 전반적인 구조나 순서가 정의되며, 하위 클래스(구체 클래스)의 메소드에서는 해당 알고리즘의 구체적인 처리를 구현한다. 이렇게 설계된 클래스들은 알고리즘 구현의 변경에 보다 유연한 구조가 된다.

 

 

템플릿 메소드 패턴을 구성하는 주요 클래스에는 Abstract Class, Concrete Class 2가지가 있다. 두 클래스의 역할과 클래스 간 관계를 나타내는 UML 클래스 다이어그램은 다음과 같다.

 

  Abstract Class
- 알고리즘의 뼈대를 제공하는 template method를 정의
- template method는 다른 메소드들에 대한 호출 순서를 정의하며 일반적으로 final로 선언
- template method 이외의 메소드들은 알고리즘의 각 step에 대응되며 abstract일 수도, 기본적으로 정의되어 있을 수도 있음

  Concrete Class
- Abstract Class를 상속한 구체 클래스로, Abstract Class 내의 template method를 제외한 메소드들을 재정의함으로써 알고리즘의 실제 구현을 정의

*template method: 말그대로 템플릿(양식, 틀)처럼 동작하는 메소드

 

 

템플릿 메소드 패턴의 UML 클래스 다이어그램

 

 

이제 템플릿 메소드 패턴을 코드로 직접 구현해보자. 그전에 패턴을 적용할 문제 상황은 다음과 같다.

- 등급 별로 다른 할인 정책을 적용하는 서비스
- BRONZE, SILVER, GOLD 등급이 존재
- BRONZE는 2만원 이상 구매 시 3천원 할인
- SILVER는 1만원 이상 구매 시 3천원 할인
- GOLD는 구매 금액 상관 없이 50% 할인

 

일단 템플릿 메소드 패턴을 적용하지 않고 할인 정책을 구현한 코드는 다음과 같다.

public class DiscountPolicy {

    int getDiscountedAmount(int amount, Grade grade) {
        if (grade == Grade.BRONZE) {
            if (amount >= 20000) return amount - 3000; //3000원 할인
        }
        else if (grade == Grade.SILVER) {
            if (amount >= 10000) return amount - 3000; //3000원 할인
        }
        else if (grade == Grade.GOLD) {
            if (true) return amount - (amount / 2); //기존 금액의 50% 할인
        }
        return amount;
    }
}

public enum Grade {
    BRONZE, SILVER, GOLD
}

위 코드의 문제점은 먼저 DiscountPolicy라는 클래스의 책임이 너무 많다는 것이다. 언뜻 보면 'DiscountPolicy가 할인 정책을 적용하는 책임만을 가지고 있으니 괜찮지 않나?'라고 생각할 수도 있겠지만, getDiscountedAmount 메소드 내에서 if문을 통해 BRONZE의 할인 정책, SILVER의 할인 정책, GOLD의 할인 정책을 모두 처리한다. 만약 각 등급의 할인 정책을 처리하는 코드가 매우 길어진다면? 게다가 또 다른 등급이 추가된다면? DiscountPolicy 클래스는 매우 매우 비대해질 것이다. 또한 이로 인해 BRONZE의 할인 정책을 변경해도, SILVER의 할인 정책을 변경해도, GOLD의 할인 정책을 변경해도 또는 새로운 등급의 할인 정책을 추가해도 모두 DiscountPolicy 코드를 변경해야 하기 때문에 변경에 따른 영향의 범위가 넓어질 수도 있다.

 

 

다시 말해 위 코드는 변경에 유연하지 않다. 이는 템플릿 메소드 패턴을 적용함으로써 해결할 수 있는데, 다음 순서에 따라 패턴을 적용한 코드를 작성해보자.

 

 

1. 할인 정책 알고리즘을 여러 steps로 분리하고, 각 step은 모든 등급에서 공통적인지 등급에 따라 다르게 동작하는지를 분류

할인 정책 알고리즘은 크게 '할인 정책 적용 가능 여부 검사 → 할인 금액 계산 → 할인 적용 최종 금액 계산'의 3가지 단계로 구성된다. 여기서 '할인 정책 적용 가능 여부 검사' 단계와 '할인 금액 계산' 단계는 각 등급에 따라 다르게 동작하지만, '할인 적용 최종 금액 계산' 단계는 기존 금액 - 할인 금액 연산만을 수행하므로 모든 등급에서 공통적이다.

 

 

2. Abstract Class를 생성해 알고리즘의 구조를 나타내는 template method를 정의하고 알고리즘의 각 step에 대응되는 여러 abstract method를 선언 (이때 template method는 하위 클래스에서 오버라이딩하지 못하도록 final로 선언)

위에서 분리한 각 step은 Abstract Class 내에서 하나의 메소드에 대응된다. 여기서 '할인 정책 적용 가능 여부 검사' 단계는 isDiscountable 메소드로, '할인 금액 계산' 단계는 calculateDiscountAmount 메소드로, '할인 적용 최종 금액 계산' 단계는 applyDiscountAmount 메소드로 선언할 것이다. 

 

사실 알고리즘을 여러 steps로 나누었을 때, 각 step은 2가지 타입으로 나뉠 수 있다. 즉 isDiscountable, calculateDiscountAmount 단계와 같이 알고리즘의 구현마다 달라지는 step과 applyDiscountAmount 단계와 같이 모든 알고리즘의 구현에서 공통된 step으로 나뉜다. 전자의 경우 모든 하위 클래스에서 구현되어야 하므로 abstract 메소드로 선언해주며, 후자는 모든 하위 클래스에서 동일한 로직이므로 그냥 Abstract Class에서 정의해준다. 물론 후자 메소드 또한 하위 클래스에서 재정의될 수 있다. 

 

마지막으로 각 step을 순서대로 호출하는 template method를 정의해준다. 이 템플릿 메소드는 하위 클래스에서 재정의하지 못하도록 final로 선언하기도 한다. 

public abstract class DiscountPolicy {

    //템플릿 메소드
    public final int discount(int amount) {
        if (isDiscountable(amount)) {
            int discountAmount = calculateDiscountAmount(amount);
            return applyDiscountAmount(amount, discountAmount);
        }
        return amount;
    }

    //등급마다 다른 로직
    public abstract boolean isDiscountable(int amount);
    public abstract int calculateDiscountAmount(int amount);

    //공통된 로직
    public int applyDiscountAmount(int amount, int discountAmount) {
        if (amount < discountAmount) return 0;
        return amount - discountAmount;
    }
}

 

 

3. Abstract Class를 상속한 Concrete Class를 생성하고 Abstract Class의 template method를 제외한 메소드들을 재정의함으로써 알고리즘의 실제 구현을 정의 

마지막으로 DiscountPolicy 추상 클래스를 상속한 구체 클래스 BronzeDiscountPolicy, SilverDiscountPolicy, GoldDiscountPolicy를 생성한다. 각 클래스에서 isDiscountable, calculateDiscountAmount 메소드들을 재정의함으로써 등급에 따라 다르게 적용되는 할인 정책을 구현해준다.

//BRONZE 할인 정책
public class BronzeDiscountPolicy extends DiscountPolicy {

    @Override
    public boolean isDiscountable(int amount) {
        return amount >= 20000;
    }

    @Override
    public int calculateDiscountAmount(int amount) {
        return 3000;
    }
}

//SILVER 할인 정책
public class SilverDiscountPolicy extends DiscountPolicy {

    @Override
    public boolean isDiscountable(int amount) {
        return amount >= 10000;
    }

    @Override
    public int calculateDiscountAmount(int amount) {
        return 3000;
    }
}

//GOLD 할인 정책
public class GoldDiscountPolicy extends DiscountPolicy {

    @Override
    public boolean isDiscountable(int amount) {
        return true;
    }

    @Override
    public int calculateDiscountAmount(int amount) {
        return amount / 2;
    }
}

 

 

등급별 할인 정책을 적용한 코드와 실행 결과는 다음과 같다.

public class DiscountPolicyApplication {

    public static void main(String[] args) {
        BronzeDiscountPolicy bronze = new BronzeDiscountPolicy();
        SilverDiscountPolicy silver = new SilverDiscountPolicy();
        GoldDiscountPolicy gold = new GoldDiscountPolicy();

        int bronzeResult = bronze.discount(10000); //2만원 이상부터 할인 정책 적용 가능
        int silverResult = silver.discount(10000);
        int goldResult = gold.discount(10000);
        System.out.println("bronzeResult = " + bronzeResult);
        System.out.println("silverResult = " + silverResult);
        System.out.println("goldResult = " + goldResult);
    }
}

실행 결과

bronzeResult = 10000
silverResult = 7000
goldResult = 5000

 

 

 

장단점

정리하자면 거의 동일한 구조에 약간의 차이가 있는 구현 로직이 여러 개 있을 때 템플릿 메소드 패턴을 사용할 수 있으며, 이는 공통된 구조는 상위 클래스로 뽑아내고 일부 차이가 나는 부분은 하위 클래스에서 확장함으로써 적용할 수 있다.

 

이렇게 템플릿 메소드 패턴을 사용하면 중복 코드를 상위 클래스로 뽑아내기 때문에 코드 중복도가 낮아지며, 변경의 범위를 최소화하면서 클라이언트가 알고리즘의 일부 로직을 재정의할 수 있어 OCP 원칙을 따르게 된다. 위 예제의 경우 BRONZE의 할인 정책만을 변경하는 경우 BronzeDiscountPolicy 클래스만을 변경해주며, 새로운 등급의 할인 정책을 정의하는 경우에는 DiscountPolicy를 상속한 새로운 클래스를 생성해주면 된다. 즉 기존 다른 클래스들에는 영향을 주지 않으면서 변경 사항을 보다 쉽게 적용할 수 있게 된다.

 

 

그러나 템플릿 메소드 패턴은 상속을 이용해서 구현되기 때문에 상속에 의한 부작용 문제를 그대로 안고 있다. 예를 들어 알고리즘의 구조를 정의한 상위 클래스의 템플릿 메소드를 변경하거나 이외 메소드들의  시그니처를 변경한 경우 모든 하위 클래스들에까지 영향을 미칠 수 있다. 이는 상속을 위해 자식 클래스가 부모 클래스에 의존하고 있기 때문에 발생하는 문제이다. 추가적으로 템플릿 메소드 패턴 적용 전과 비교해 생성된 클래스의 수가 더 많아져 어플리케이션의 전체 구조의 복잡도는 올라갈 수 있다. 따라서 알고리즘의 각 구현이 간단하면서 변경될 가능성이 매우 낮은 경우에는 템플릿 메소드 패턴을 적용하여 여러 클래스를 생성하기 보단, 하나의 클래스에서 이를 모두 처리하도록 하는 것이 더 좋다.

 

 

 

실제 적용 사례

이 템플릿 메소드 패턴은 자바, 스프링 등에서 매우 많이 적용되어 있다. 템플릿 메소드 패턴이 실제로는 어떻게 구현되어 있는지 보는 것이 앞으로의 어플리케이션 아키텍처 설계에 조금이라도 도움이 되지 않을까 하여 일부 코드를 가져와 봤다. (이해를 위해 일부 변경된 코드이다.)

 

HttpServlet

public abstract class HttpServlet extends GenericServlet {

    //템플릿 메소드
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String method = req.getMethod();
        if (method.equals(METHOD_GET)) {
            doGet(req, resp);
        } else if (method.equals(METHOD_HEAD)) {
            doHead(req, resp);
        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);
        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);
        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);
        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);
        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);
        } else {
            String errMsg = lStrings.getString("http.method_not_implemented");
            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
        }
    }
    
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String msg = lStrings.getString("http.method_get_not_supported");
        sendMethodNotAllowed(req, resp, msg);
    }
    
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String msg = lStrings.getString("http.method_post_not_supported");
        sendMethodNotAllowed(req, resp, msg);
    }
}

어떤 추상 클래스의 한 메소드 내에서 다른 메소드들을 매우 많이 호출하고 있다면, 특히 호출되는 메소드가 추상 메소드라면 해당 메소드는 template method일 가능성이 높다. 서블릿 클래스 중 웹 요청을 처리하는 HttpServlet 추상 클래스 내 service 메소드는 내부에서 do-로 시작하는 다른 메소드들을 많이 호출하고 있어 HttpServlet에는 템플릿 메소드 패턴이 적용되었다고 볼 수 있다. 

 

이 경우 템플릿 메소드인 servicefinal로 선언되어 있지 않기 때문에 하위 클래스에서 service 메소드도 재정의할 수 있다. 또한 do- 메소드들을 abstract로 선언하지 않고 HttpServlet에서 기본 동작을 정의해주어 만약 HttpServlet을 상속한 서블릿에서 do- 메소드들을 재정의하지 않으면 매핑된 요청에 대해 405 'Method Not Allowed' 오류를 공통적으로 발생시키도록 하였다. 반면 HttpServlet을 상속한 서블릿이 doGet 메소드를 오버라이딩한다면 해당 서블릿은 매핑되는 GET 요청에 대해 오류를 발생시키지 않고 기존과 다르게 동작할 것이다.

 

 

스프링의 Abstract- 추상 클래스

public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport implements HandlerMapping, Ordered, BeanNameAware {

    //템플릿 메소드
    @Override
    public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        return getHandlerInternal(request);
    }

    protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception;
}

스프링 MVC의 구성 요소 클래스들 중 Abstract-로 시작하는 추상 클래스들에는 대부분 템플릿 메소드 패턴이 적용되어 있다. 실제 AbstractHandlerMapping 클래스 내 getHandler 메소드는 final로 선언된 템플릿 메소드이며, getHandler 메소드 내에서 호출되는 getHandlerInternalabstract로 선언되어 AbstractHandlerMapping을 상속한 하위 클래스에서 구현되어야 한다.

 

 

public abstract class AbstractHandlerMethodAdapter extends WebContentGenerator implements HandlerAdapter, Ordered {

    //템플릿 메소드
    @Override
    public final boolean supports(Object handler) {
        return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
    }
    
    protected abstract boolean supportsInternal(HandlerMethod handlerMethod);

    //템플릿 메소드
    @Override
    public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return handleInternal(request, response, (HandlerMethod) handler);
    }
    
    protected abstract ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception;
}

마지막으로 하나 더 살펴보자면 AbstractHandlerMethodAdapter 추상 클래스에는 final로 선언된 supports, handle 템플릿 메소드가 있으며, 해당 메소드 내부에서 호출되는 abstract로 선언된 supportsInternal, handleInternal 메소드가 있다. AbstractHandlerMethodAdapter 추상 클래스를 상속한 하위 클래스는 이 supportsInternal, handleInternal 메소드를 구현함으로써 Adapter의 동작 방식을 변경할 수 있다. 참고로 이와 같이 추상 클래스 내에 2개 이상의 템플릿 메소드가 존재할 수 있다.

 

 

 

템플릿 메소드 패턴이 상속에 기반하여 알고리즘의 실제 구현을 변경하는 디자인 패턴인 반면, 전략 패턴은 객체 간 조립을 기반으로 알고리즘의 실제 구현을 동적으로 변경할 수 있도록 한다. 즉 전략 패턴에는 상속 관계가 없기 때문에 템플릿 메소드 패턴이 가진 문제점을 일부 해결해주는데 이에 대해서는 다음 글에서 자세히 알아보기로 하자.

 

 

 

끝.

 

 

 

참고 

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

https://www.geeksforgeeks.org/template-method-design-pattern/

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8

https://www.baeldung.com/java-template-method-pattern

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함