디자인 패턴 공부하면서 제일 중요하게 생각하고 있는 점이 실제 사용예이다. 제일 엿같은점이 커피머신이니, 스파게티니, 공구상점이니 이딴 개같은 예제들을 끌어와 설명하고 있는 부분이라서. 아니, 반복해서 마주치는 문제를 OOP로 설계하는 보편적인 방법들을 설명하면서 어떻게 실사용예 없이 그럴수가 있지? 그냥 오픈소스라도 좀 끌어와서 설명해야하는거 아니냐.
Template Method도 실제 예가 없으면 그냥 가상클래스 상속받아 사용하는 generalization이 전부다. 의미부여가 안된다고. 여기에 스파게티 레시피 가져와서 설명해야겠냐고. 이러니까 사람들도 다 헷갈려서 그냥 generalization 예제들 들고와서 설명하지.
서두가 길었는데, 시작해보자. Template Method는 알고리즘 교체에 가장 적합한 패턴으로 보인다. 예를들자면, 코드 에디터에서 java나 kotlin으로 코드를 짜고 run을 실행하는 경우를 생각해보자.
java나 코틀린으로 코드를 짜고 컴파일러가 바이트코드를 생성하면 어떤 언어를 썼는지 상관없이, 생성한 바이트코드는 JVM에서 실행가능하다. 에디터에서 자체적인 전처리(preProcess()
)를 해준다고 가정하면 run()
실행시, preProcess()-compile()-runOnJVM()
의 세단계가 필요하다. 코드로 쓰면 다음과 같다.
public abstract class MyEditorCompiler {
public void run(){
preProcess();
compile();
runOnJVM();
}
private void preProcess(){
System.out.println("Common preprocessing in editor");
}
protected abstract void compile();
protected abstract void runOnJVM();
}
preProcess()는 컴파일러와 무관하고 공통이므로 여기에서 정의했다. 자체적인 문법검사같은 lint기능등이 해당될 수 있겠다.
compile()
과 runOnJVM()
은 abstract로 서브클래스에서 정의된다.
바이트 코드를 생성하는 컴파일 단계에서는 Oracle의 JDK를 이용할 수도 있고, 안드로이드처럼 OpenJDK를 이용할 수도 있다. Kotlin도 제공되는 컴파일러로 JVM에서 동작할 동일한 바이트코드가 생성된다. 위의 abstract 클래스를 상속받아 이 3개의 컴파일러를 구현해보자.
public class Oracle extends MyEditorCompiler {
@Override
protected void compile() {
System.out.println("Oracle compiler compile...");
}
@Override
protected void runOnJVM() {
System.out.println("Run on Oracle JVM");
}
}
public class OpenJDK extends MyEditorCompiler {
@Override
protected void compile() {
System.out.println("OpenJDK compiler compile...");
}
@Override
protected void runOnJVM() {
System.out.println("Run on OpenJDK JVM");
}
}
public class Kotlin extends MyEditorCompiler {
private String JVM = "OpenJDK";
public String getJVM() {
return JVM;
}
public void setJVM(String JVM) {
this.JVM = JVM;
}
@Override
protected void compile() {
System.out.println("Kotlin compiler compile...");
}
@Override
protected void runOnJVM() {
System.out.println("Run on " + getJVM() + " JVM");
}
Kotlin은 Oracle이나 OpenJDK가 제공하는 JVM을 이용하므로, 별개로 JVM을 설정하고 그 위에서 동작하는걸 가정했다.
사용은 다음과 같이 될 수 있다.
enum COMPILER{ORACLE, OPENJDK, KOTLIN}
public class Main {
public static void main(String[] args) {
COMPILER compiler = COMPILER.KOTLIN;
MyEditorCompiler myEditor = null;
switch(compiler){
case ORACLE:
myEditor = new Oracle();
break;
case OPENJDK:
myEditor = new OpenJDK();
break;
case KOTLIN:
myEditor = new Kotlin();
break;
}
myEditor.run();
}
}
이와같이, 클라이언트가 사용하도록 노출된 run()
메쏘드에서 어떤식으로 실행할지 형태만 잡아주고(preProcess()-compile()-runOnJVM()
) 그 구현은 상속받은 클래스들이 수행하는게 Template Method Pattern이다. run()
메쏘드가 Template만 있는 형태라서 붙여진 이름이다.
UML 다이어그램으로 표현하면 다음과 같다.

이미지 참조 : http://programmersnotes.info/2009/03/03/difference-between-adapter-and-template-method-pattern/