본문 바로가기
Study/디자인 패턴

[C#/Unity][디자인패턴] 템플릿 메서드 패턴(Template Method Pattern)

by 스테디코디스트 2023. 12. 28.
반응형

1. 템플릿 메서드 패턴이란?

- 기능의 뼈대(템플릿)와 실제 구현을 분리하는 패턴.

- 알고리즘의 구조를 메서드에 정의하고, 하위 클래스에서 알고리즘 구조의 변경헚이 알고리즘을 재정의하는 패턴.

- 상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법.

- 변하지 않는 기능을 슈퍼클래스에 만들어두고, 자주 변경되면 확장할 기능은 서브클래스에 만든다.

 

2. 템플릿 메서드 패턴을 사용하는 경우

- 알고리즘이 단계별로 나누어 지거나, 같은 역할을 하는 메서드이지만 여러곳에서 다른 형태로 사용이 필요한 경우 사용.

- 알고리즘의 특정 단계들만 확장할 수 있도록 하고 싶지만 알고리즘의 구조는 확장하지 못하도록 하고 싶은 경우

 

3. 템플릿 메서드 패턴의 장점

- 중복을 줄이고, 코드의 재사용성을 확보할 수 있다.

- 핵심 알고리즘의 관리가 쉽다.

- 상위 클래스의 메서드만 보더라도 전체 동작을 이해하기 쉽다.

- 추상 메서드 재정의를 통한 서브 클래스 확장에 유리하다.

 

4. 템플릿 메서드 패턴의 단점

- 알고리즘에 따라 구현이 제한될 수 있다.

- 자식 클래스를 통해 default 단계 구현을 억제하여 리스코프 치환 원칙을 위반할 수 있다.

- 단계들이 더 많아질수록 유지가 더 어렵다.

- 상위 클래스에서 기술을 적게하면 하위 클래스에서 모두 재정의 해야하므로 복잡도가 증가하고, 코드의 중복이 발생할 수 있다.

 

5. 코드 구현

1) 구조

- Abstract Class

: 템플릿 메소드를 구현하고, 템플릿 메소드에서 돌아가는 추상 메소드를 선언한다.

- Concrete Class

: Abstract 클래스를 상속받아 추상 메소드를 구체적으로 구현한다.

- hook 메서드

: 부모의 템플릿 메서드의 영향이나 순서를 제어하고 싶을 때 사용되는 메서드 형태

: hook 메서드를 통해 자식클래스에서 좀 더 유연하게 템플릿 메서드의 알고리즘 로직을 다양화 할 수 있다.

: 선택적으로 오버라이드 하여 자식클래스에서 제어하거나 놔두도록 하기 위해 추상 메서드가 아닌 일반메서드로 구현한다.

 

2) 소스코드(C#)

사용자의 입력을 받아 해당 숫자 값들을 연산해주는 기능을 구현

 

- 패턴 적용 전

using System;

namespace StudyCSharp
{
    class PlusInputProcessor
    {
        string _input;

        public PlusInputProcessor(string input)
        {
            this._input = input;
        }

        public int process()
        {
            int result = 0;

            string s = "";

            foreach(char c in _input)
            {
                s += c;

                // 공백인 경우
                if (c == ' ')
                {
                    // 해당 문자열을 숫자로 바꾼 값을 result에 더함
                    result += int.Parse(s); 

                    // 문자열 초기화
                    s = " ";
                }
            }

            return result;
        }
    }

    class MultiplyInputProcessor
    {
        string _input;

        public MultiplyInputProcessor(string input)
        {
            this._input = input;
        }

        public int process()
        {
            int result = 1;

            string s = "";

            foreach (char c in _input)
            {
                // 공백인 경우
                if (c == ' ')
                {
                    // 해당 문자열을 숫자로 바꾼 값을 result에 곱함
                    result *= int.Parse(s);

                    // 문자열 초기화
                    s = " ";

                    continue;
                }

                s += c;
            }

            return result;
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("숫자를 입력하세요(입력을 끝내려면 0을 누르세요)");

            // 사용자의 입력을 받아옴.
            string input = UserInput();

            Console.WriteLine("------------------------------------------------");
            Console.Write("덧셈 결과 : ");

            // 덧셈
            PlusInputProcessor plusProcessor = new PlusInputProcessor(input);

            int plus = plusProcessor.process();
            Console.WriteLine(plus);

            Console.WriteLine("------------------------------------------------");
            Console.Write("곱셈 결과 : ");

            // 곱셈
            MultiplyInputProcessor multiplyProcessor = new MultiplyInputProcessor(input);

            int multiply = multiplyProcessor.process();
            Console.WriteLine(multiply);

            Console.WriteLine("------------------------------------------------");
        }

        static string UserInput()
        {
            string input = "";

            while (true)
            {
                string curInput = Console.ReadLine();

                // 0이 입력되면 입력 끝
                if (curInput == "0")
                    break;

                input += curInput + " "; // 공백으로 문자 분리
            }

            return input;
        }
    }
}

 

- 실행 결과

 

- 패턴 적용 후

using System;

namespace StudyCSharp
{
    abstract class InputProcessor
    {
        private string _input; // 사용자의 입력을 받아와 저장

        public InputProcessor(string input)
        {
            this._input = input;
        }

        protected abstract int Calculate(int result, int input);

        protected abstract int GetResult();

        public int process()
        {
            string s = "";
            int result = GetResult();
            
            foreach(char c in _input)
            {
                // 공백인 경우
                if (c == ' ')
                {
                    // 해당 문자열을 숫자로 바꾼 값과 연산
                    result = Calculate(result, int.Parse(s));

                    // 문자열 초기화
                    s = " ";

                    continue;
                }

                s += c;
            }

            return result;
        }
    }

    class PlusInputProcessor : InputProcessor
    {
        // 부모의 생성자를 그대로 사용
        public PlusInputProcessor(string input) : base(input) { }

        protected override int Calculate(int result, int input)
        {
            return result += input;
        }

        protected override int GetResult()
        {
            return 0;
        }
    }

    class MultiplyInputProcessor : InputProcessor
    {
        // 부모의 생성자를 그대로 사용
        public MultiplyInputProcessor(string input) : base(input) { }

        protected override int Calculate(int result, int input)
        {
            return result *= input;
        }

        protected override int GetResult()
        {
            return 1;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("숫자를 입력하세요(입력을 끝내려면 0을 누르세요)");

            // 사용자의 입력을 받아옴.
            string input = UserInput();

            Console.WriteLine("------------------------------------------------");
            Console.Write("덧셈 결과 : ");

            // 덧셈
            PlusInputProcessor plusProcessor = new PlusInputProcessor(input);

            int plus = plusProcessor.process();
            Console.WriteLine(plus);

            Console.WriteLine("------------------------------------------------");
            Console.Write("곱셈 결과 : ");

            // 곱셈
            MultiplyInputProcessor multiplyProcessor = new MultiplyInputProcessor(input);

            int multiply = multiplyProcessor.process();
            Console.WriteLine(multiply);

            Console.WriteLine("------------------------------------------------");
        }

        static string UserInput()
        {
            string input = "";

            while (true)
            {
                string curInput = Console.ReadLine();

                // 0이 입력되면 입력 끝
                if (curInput == "0")
                    break;

                input += curInput + " "; // 공백으로 문자 분리
            }

            return input;
        }
    }
}

 

- 실행 결과

 

cf) 소스코드 2(C#) - 할리우드 원칙 준수

※ 할리우드 원칙(Hollywood Princlple) 

: 고수준 모듈(추상클래스, 인터페이스)에 의존하고 고수준 모듈에서 연락(메서드의 실행)을 하라는 원칙

: 의존성 부패(dependency rot)를 방지할 수 있음

    ▷ 의존성 부패란 객체끼리 이상하게 얼키고 설켜, 의존성이 복잡하게 꼬여있는 상태를 의미함.

 

- 저수준 모델만 사용

// 저수준 모듈 1
class Lower1
{
    // 1. 함수명만 다름, 내부 코드는 동일
    public void print(int num)
    {
        Console.WriteLine(num);
    }

    // 2. 함수명, 내부 코드 모두 다름(로직이 비슷함)
    public int calculate(int n1, int n2)
    {
        return n1 + n2;
    }
}

// 저수준 모듈 2
class Lower2
{
    public void printf(int num)
    {
        Console.WriteLine(num);
    }

    public int operation(int n1, int n2)
    {
        return n1 * n2;
    }
}

 

- 고수준 모델 사용(중복 코드만 제거)

using System;

namespace StudyCSharp
{
    // 고수준 모듈
    class Higher
    {
    	// 1. 함수명 통일 후 상속받아 사용
        public void print(int num)
        {
            Console.WriteLine(num);
        }
    }

    // 저수준 모듈 1
    class Lower1 : Higher
    {
        public int calculate(int n1, int n2)
        {
            return n1 + n2;
        }
    }

    // 저수준 모듈 2
    class Lower2 : Higher
    {
        public int operation(int n1, int n2)
        {
            return n1 * n2;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            int n1 = 14; 
            int n2 = 7;

            // 업캐스팅 -> 고수준 모듈 타입으로 인스턴스화
            Higher obj = new Lower1();

            // 상위 클래스로 메서드가 통일되어 있으므로 실행에 문제 없음
            obj.print(1500);

            // 내부 로직이 달라 상위 클래스로 묶지 못한 경우
            // -> 다운 캐스팅을 통해 실행해야함.
            int sum = ((Lower1)obj).calculate(n1, n2);
            Console.WriteLine("{0} x {1} = {2}", n1, n2, sum);

            // 다른 자식 클래스로 교체
            obj = new Lower2();

            // 마찬가지로 다운 캐스팅을 통해 실행해야함.
            int mul = ((Lower2)obj).operation(n1, n2);
            Console.WriteLine("{0} x {1} = {2}", n1, n2, mul);
        }
    }
}

 

- 실행 결과

 

- 패턴 사용(추상 클래스를 통한 코드 통합 및 고수준 모델의 의존 유도)

using System;

namespace StudyCSharp
{
    // 고수준 모듈
    abstract class Higher
    {
    	// 1. 함수명 통일 후 상속받아 사용
        public void print(int num)
        {
            Console.WriteLine(num);
        }
        
        // 2. 추상클래스로 변경 및 추상 메서드로 선언 -> 오버라이드(재정의)하여 사용
        public abstract int calculate(int n1, int n2);
    }

    // 저수준 모듈 1
    class Lower1 : Higher
    {
        public override int calculate(int n1, int n2)
        {
            return n1 + n2;
        }
    }

    // 저수준 모듈 2
    class Lower2 : Higher
    {
        public override int calculate(int n1, int n2)
        {
            return n1 * n2;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            int n1 = 14; 
            int n2 = 7;

            // 업캐스팅 -> 고수준 모듈 타입으로 인스턴스화
            Higher obj = new Lower1();
            Console.WriteLine("{0} + {1} = {2}", n1, n2, obj.calculate(n1, n2)); 

            obj = new Lower2();
            Console.WriteLine("{0} x {1} = {2}", n1, n2, obj.calculate(n1, n2));
        }
    }
}

 

- 실행 결과

 

cf2) 소스코드 3(C#) - hook 메서드 응용

※ hook 메서드를 이용해 추가적인 로직을 수성할 수 있다.

 

- 패턴 적용 전

using System;

namespace StudyCSharp
{    
    class Coffee
    {
        public void Recipe()
        {
            boilWater();
            Console.WriteLine("...\n");
            brewCoffeeGrinds();
            Console.WriteLine("...\n");
            pourInCup();
            Console.WriteLine("...\n");
            addSugarAndMilk();

            Console.WriteLine("\n");
        }

        public void boilWater()
        {
            Console.WriteLine("물 끓이는 중");            
        }

        public void brewCoffeeGrinds()
        {
            Console.WriteLine("필터를 통해 커피를 우려내는 중");
        }

        public void pourInCup()
        {
            Console.WriteLine("컵에 따르는 중");
        }

        public void addSugarAndMilk()
        {
            Console.WriteLine("설탕과 우유 추가");
        }
    }

    class Tea
    {
        public void Recipe()
        {
            boilWater();
            Console.WriteLine("...\n");
            steepTeaBag();
            Console.WriteLine("...\n");
            pourInCup();
            Console.WriteLine("...\n");

        }

        public void boilWater()
        {
            Console.WriteLine("물 끓이는 중");
        }

        public void steepTeaBag()
        {
            Console.WriteLine("차를 우리는 중");
        }

        public void pourInCup()
        {
            Console.WriteLine("컵에 따르는 중");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Coffee coffee = new Coffee();
            Tea tea = new Tea();

            Console.WriteLine("<<Coffee 레시피>>");
            coffee.Recipe();

            Console.WriteLine("<<Tea 레시피>>");
            tea.Recipe();
        }
    }
}

 

- 실행 결과

 

- 패턴 적용 후

using System;

namespace StudyCSharp
{
    // 추상 클래스
    abstract class Beverage
    {
        // 템플릿 메서드
        public void Recipe()
        {
            boilWater();
            Console.WriteLine("...\n");
            brew();
            Console.WriteLine("...\n");
            pourInCup();
            Console.WriteLine("...\n");

            if (UseBeverageCustom())
                beverageCustom();

            Console.WriteLine("\n>>> 완성!\n");
        }

        public abstract void brew();

        public abstract void beverageCustom();

        // hook 메서드
        public virtual bool UseBeverageCustom()
        {
            return false;
        }

        public void boilWater()
        {
            Console.WriteLine("물 끓이는 중");
        }

        public void pourInCup()
        {
            Console.WriteLine("컵에 따르는 중");

        }
    }

    class Coffee : Beverage
    {
        public override void brew()
        {
            Console.WriteLine("필터를 통해 커피를 우려내는 중");
        }

        public override void beverageCustom()
        {
            Console.WriteLine("설탕과 우유가 추가되었습니다!");
        }

        public override bool UseBeverageCustom()
        {
            Console.Write("커피에 우유와 설탕을 넣으시겠습니까? (y/n) : ");

            string answer = Console.ReadLine().ToString();

            if (answer == "Y" || answer == "y")
            {
                Console.WriteLine("\n우유와 설탕을 추가 중");
                Console.WriteLine("...\n");
                return true;
            }
            else
            {
                Console.WriteLine("\n우유와 설탕을 추가하지 않았습니다.");
                return false;
            }
        }
    }

    class Tea : Beverage
    {
        public override void brew()
        {
            Console.WriteLine("차를 우리는 중");
        }

        public override void beverageCustom() { }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Coffee coffee = new Coffee();
            Tea tea = new Tea();

            Console.WriteLine("<<Coffee 레시피>>");
            coffee.Recipe();

            Console.WriteLine("<<Tea 레시피>>");
            tea.Recipe();
        }
    }
}

 

- 실행 결과


<참고 사이트>

 

 

템플릿 메서드 패턴으로 모순 없는 상태 보장하기

시작하기 전에 안녕하세요. LINE Pay의 iOS 개발을 맡고 있는 정지인입니다. LINE Pay iOS의 결제 기능을 리팩토링하는 데에 적용했던 템플릿 메서드 패턴을 이용한 계약 기반 프로그래밍 기법에 대해

engineering.linecorp.com

 

💠 템플릿 메소드(Template Method) 패턴 - 완벽 마스터하기

Template Method Pattern 템플릿 메서드(Template Method) 패턴은 여러 클래스에서 공통으로 사용하는 메서드를 템플릿화 하여 상위 클래스에서 정의하고, 하위 클래스마다 세부 동작 사항을 다르게 구현하

inpa.tistory.com

 

 

템플릿 메서드 패턴

/ 디자인 패턴들 / 행동 패턴 템플릿 메서드 패턴 다음 이름으로도 불립니다: Template Method 의도 템플릿 메서드는 부모 클래스에서 알고리즘의 골격을 정의하지만, 해당 알고리즘의 구조를 변경하

refactoring.guru

 

 

[디자인 패턴] 템플릿 메소드(Template Method) 패턴이란?

안녕하세요? 제이온입니다. 저번 시간에는 전략 패턴에 대해서 알아 보았습니다. 오늘은 템플릿 메소드 패턴을 설명하겠습니다. 템플릿 메소드(Template Method) 패턴 GoF의 디자인 패턴에 의하면, 템

steady-coding.tistory.com