들어가기 전에
프로젝트를 진행하면서, 가독성 있게 코드를 작성하기 위해 메소드를 구성하다보니, 각각 다른 API에 대해 비슷한 형태의 로직이 중복적으로 발생하였습니다. 이 로직 중에는 공통 코드(예: 객체 만드는 코드)가 존재하기 때문에 중복을 제거하고, 추후 유지보수가 편할 수 있도록 Template Method Pattern을 적용하고자 합니다. 이번 포스팅에서는 소스코드 리팩토링 시 사용할 Template Method Pattern에 대해 알아보도록 하겠습니다.
Template Method Pattern
템플릿 메소드 패턴은 알고리즘 골격을 정의합니다. 템플릿 메소드를 사용하면 알고리즘의 일부 단계를 서브클래스에서 구현할 수 있으며, 알고리즘 구조는 그대로 유지하면서 알고리즘의 특정 단계를 서브클래스에서 재정의할 수도 있습니다.
템플릿이라는 이름답게 템플릿 메소드 패턴은 알고리즘의 틀(템플릿)을 만든다고 생각하면 되며, 템플릿은 일련의 단계로 알고리즘을 정의한 메소드입니다.
아래 소스코드는 기본적인 템플릿 메소드 패턴 형식으로, AbstactClass 내에 템플릿 메소드가 존재하고, 해당 메소드 내에서 수행하는 일련의 단계 중 일부는 구체클래스에서 override하여 사용함을 확인할 수 있습니다.
abstract class AbstractClass { // parent class
// you cant override this method because it's not `open`
fun templateMethod() { // the Template Method, so the algorithm with ordered steps
println("running template method")
primitiveOperation1() // calling algorithm steps
concreteOperation()
primitiveOperation2()
}
abstract fun primitiveOperation1() // algorithm step to be overridden by concrete class
private fun concreteOperation() { // algorithm step not to be overridden
println("doing concrete operation ")
}
abstract fun primitiveOperation2() // another step
}
class ConcreteClass : AbstractClass() { // concrete class
override fun primitiveOperation1() { // step implementation for concrete class
println("doing concrete operation 1")
}
override fun primitiveOperation2() {
println("doing concrete operation 2")
}
}
class AnotherConcreteClass : AbstractClass() { // another concrete class
override fun primitiveOperation1() {
println("doing concrete operation 1") // not cool duplicated implementation
}
override fun primitiveOperation2() {
println("doing another concrete operation 2")
}
}
- 위 예시는 Kotlin 예시인지라, templateMethod() 앞에 final을 붙이지 않습니다. JAVA라고 한다면, final을 붙여 서브클래스가 오버라이딩하지 못하게 막아주어야 합니다.
위 기본적인 예시를 좀 더 쉽게 볼 수 있도록, 바닐라라떼와 밀크티 만들기에 템플릿 메소드 패턴을 적용하면 아래와 같습니다.
Template Mathod Pattern 예시
템플릿 메소드 속 후크 알아보기
후크(hook)란, 추상 클래스에서 선언되지만 기본적인 내용만 구현되어있거나 아무 코드도 들어있지 않은 메소드입니다.
abstract class AbstractClass {
final void templateMethod() {
primitiveOperation1();
primitiveOperation2();
concreteOperation();
hook();
}
abstract void primitiveOperation1();
abstract void primitiveOperation2();
final void concreteOperation() {
// concreteOperation() 코드
// 구상 단계는 추상 클래스 내에 정의되며, final로 선언하였기 때문에 서브클래스에서 오버라이딩이 불가능합니다.
}
void hook() {} // 구상 메소드이지만 아무런 기능도 하지 않음
}
위에서 볼 수 있듯, 후크는 추상 클래스에 선언되지만 아무런 코드가 들어있지 않을 수 있습니다. 후크는 여러 사용법이 있겠지만, 아래와 같은 상황일 때 사용될 수 있습니다.
public abstract class CaffeineBeverageWithHook {
final void prepareRecipe() { // templateMethod
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) {
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("물 끓이기");
}
void pourInCup() {
System.out.println("컵에 따르기");
}
boolean customerWantsCondiments() { // 특별한 의미 없는 메소드 구현
return true; // 이 메소드는 서브클래스에서 필요할 때 오버라이드할 수 있는 메소드이므로 hook입니다.
}
}
먼저, 위와 같이 abstract class 내에서는 별다른 의미 없는 메소드를 구현합니다. 그후, 후크를 활용하기 위해 abstract class를 상속 받은 서브클래스를 작성합니다.
public class CoffeeWithHook extends CaffeineBeverageWithHook {
public void brew() {
System.out.println("커피 내리기");
}
public void addCondiments() {
System.out.println("샷 추가로 넣기");
}
public boolean customerWantsCondiments() {
String answer = getUserInput();
if(answer.toLowerCase().startsWith("y")) { // 샷 추가를 할 지 여부 결정
return true;
} else {
return false;
}
}
private String getUserInput() {
String answer = null;
System.out.print("샷 추가가 필요한가요? (y/n)");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try {
answer = in.readLine();
} catch (IOException ioe) {
System.out.println("IO 오류");
}
if(answer == null) {
return "n";
}
return answer;
}
}
- customerWantsCondiments() 메소드는 서브 클래스에서 구현한 내용에 따라 반환값이 달라지며, 그에 따라 addCondiments() 메소드 수행여부가 결정됩니다.
템플릿을 만들 때 추상 메소드를 쓸 지, 후크를 사용할 지 어떻게 결정할까?
서브클래스가 알고리즘의 특정 단계를 제공해야한다면, 추상 메소드를 쓰는게 맞습니다. 하지만, 해당 특정 단계가 선택적으로 적용된다면 후크를 사용하는 것이 편리합니다.
참고 자료
- Kotlin Template Method
- 헤드 퍼스트 디자인 패턴