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();
}
}
}
- 실행 결과
<참고 사이트>