템플릿 메서드
는 부모 클래스에서 알고리즘의 골격을 정의하지만, 해당 알고리즘의 구조를 변경하지 않고 자식 클래스들이 알고리즘의 특정 단계들을 오버라이드(재정의)할 수 있도록 하는 행동 디자인 패턴입니다.
즉, 변하지 않는 기능(템플릿)은 상위 클래스
에 만들어두고 자주 변경되며 확장할 기능은 하위 클래스
에서 만들도록 하여, 상위의 메소드 실행 동작 순서는 고정하면서 세부 실행 내용은 다양화 될 수 있는 경우에 사용됩니다.
추상 클래스
는 알고리즘의 단계들의 역할을 하는 메서드들을 선언하며, 이러한 메서드를 특정 순서로 호출하는 실제 템플릿 메서드도 선언합니다. 단계들은 abstract로 선언되거나 일부 디폴트 구현을 갖습니다.
구체 클래스
들은 모든 단계들을 오버라이드할 수 있지만 템플릿 메서드 자체는 오버라이드 할 수 없습니다.
디자인 패턴에서의 템플릿은 변하지 않는 것을 의미한다.
만약 축구와 야구를 자바 코드로 옮기고 그거에 맞는 행동을 정의하게 된다면 아래와 같을 겁니다.
class BasketBall {
public void run() {
System.out.println("달리다!");
}
public void defense() {
System.out.println("수비하다!");
}
public void shoot() {
System.out.println("손으로 슈팅하다!");
}
public void pass() {
System.out.println("손으로 패스하다!");
}
}
class Soccer {
public void run() {
System.out.println("달리다!");
}
public void defense() {
System.out.println("수비하다!");
}
public void shoot() {
System.out.println("발로 슈팅하다!");
}
public void pass() {
System.out.println("발로 패스하다!");
}
}
위 코드를 살펴보면 run( ), defense( )
메서드의 행위가 동일하고 shoot( ), pass( )
는 다른 것을 알 수 있습니다.
만약 스포츠 종목이 1,000개가 늘어난다고 하면 행위가 같은 메서드들을 불필요하게 작성해줘야 하고 그것을 유지보수 해줘야 할 것입니다.
그렇다면 공통화할 수 있는 메서드를 합쳐주고 행위가 다른 메서드는 따로 행위를 정의하는 것이 더 효율적이지 않을까요?
abstract class Sports {
public void run() {
System.out.println("달리다!");
}
public void defense() {
System.out.println("수비하다!");
}
public abstract void shoot();
public abstract void pass();
}
Sports
라는 추상 클래스를 만들어주고 행위가 달라야할 메서드는 추상 메서드로 선언해주었습니다.
class BasketBall extends Sports {
@Override
public void shoot() {
System.out.println("손으로 패스하다!");
}
@Override
public void pass() {
System.out.println("발로 패스하다!");
}
}
class Soccer extends Sports {
@Override
public void shoot() {
System.out.println("발로 슈팅하다!");
}
@Override
public void pass() {
System.out.println("발로 패스하다!");
}
}
이렇게 코드를 작성하면 행위가 달라져야 하는 메서드만 재정의해주면 됩니다.
템플릿 메서드 패턴
은 클라이언트들이 알고리즘의 특정 단계들만 확장할 수 있도록 하고 싶을 때, 그러나 전체 알고리즘이나 알고리즘 구조는 확장하지 못하도록 하려고 할 때 사용하면 됩니다.
훅(hook) 메소드
는 부모의 템플릿 메서드의 영향이나 순서를 제어하고 싶을때 사용되는 메서드 형태를 말합니다.
위의 그림에서 볼 수 있듯이 템플릿 메서드 내에 실행되는 동작을 step2( )
라는 메서드의 참, 거짓 여부에 따라 다음 스텝을 어떻게 이어갈지 정한다. 이를 통해 자식 클래스에서 좀 더 유연하게 템플릿 메서드의 알고리즘을 로직을 다양화 할 수 있다는 특징이 있다.
훅 메소드는 추상 메소드가 아닌 일반 메소드로 구현하는데, 선택적으로 오버라이드 하여 자식 클래스에서 제어하거나 아니면 놔두거나 하기 위해서입니다.
class Coffee {
public void makeBeverage() {
boilWater();
addEspresso();
pourInCup();
addSugarAndMilk();
}
public void boilWater() {
System.out.println("물을 끓임");
};
public void addEspresso() {
System.out.println("에스프레스 추가");
};
public void pourInCup() {
System.out.println("컵에 따름");
};
public void addSugarAndMilk() {
System.out.println("설탕과 우유를 넣음");
};
}
class Tea {
public void makeBeverage() {
boilWater();
addTeaBag();
pourInCup();
}
public void boilWater() {
System.out.println("물을 끓임");
};
public void addTeaBag() {
System.out.println("티백을 넣음");
};
public void pourInCup() {
System.out.println("컵에 따름");
};
}
위 코드를 보면 배운 바와 같이 코드의 중복이 보이며 이를 템플릿 메서드 패턴을 이용해 묶어주려고 합니다.
위 코드에서 다른 부분을 표시하면 아래와 같습니다.
boilWater( ), pourInCup( )
메서드는 행위가 같으니 공통된 메서드로 만들어줍니다.
addTeaBag( ), addEspresso( )
메서드는 행위는 다르지만 공통된 메서드로 만들어 줄 수 있을거 같습니다. 그러므로 추상 메서드 1개로 만들어봅니다.
addSugarAndMilk( )
메서드는 훅(hook)
을 활용하여 만들어주겠습니다.
abstract class Drink {
public final void makeDrink() {
boilWater();
addMainItem();
pourInCup();
if (wantOtherItem()) {
addOtherItem();
}
}
protected abstract void addMainItem();
protected abstract void addOtherItem();
private void boilWater() {
System.out.println("물을 끓임");
};
private void pourInCup() {
System.out.println("컵에 따른다.");
};
protected boolean wantOtherItem() {
return false;
};
}
Drink
라는 추상 클래스를 만들어줘서 설계한 대로
1.boilWater( ), pourInCup( ) -> 공통된 행위의 메서드
를 정의해주었습니다.
2. addTeaBag( ), addEspresso( )
는 자손(서브)클래스에서 재정의 해줄거므로 추상 메서드 addMainItem( )
로 만들었습니다.
3. 훅을 이용하기 위해서 설탕과 커피를 넣는 행위는 추상 메서드 addSugarAndMilk( )
로 만들어 주었고 위의 설명처럼 훅(hook) 메소드는 부모의 템플릿 메서드의 영향이나 순서를 제어하고 싶을때 사용되는 메서드인 wantOtherItem( )
를 만들어주었습니다.
훅 메서드를 어떻게 사용하는지 아래 코드에서 확인해보겠습니다.
class Coffee extends Drink {
@Override
public void addMainItem() {
System.out.println("에스프레소 추가");
}
@Override
public void addOtherItem() {
System.out.println("설탕과 우유를 추가");
}
@Override
protected boolean wantOtherItem() {
return true;
}
}
Coffee 클래스는 addMainItem( ), addOtherItem( )
을 재정의 해주었습니다.
그리고 wantOtherItem( )
을 보면 부모(슈퍼)클래스에서의 리턴값 true가 false 바꾸었습니다. 왜나하면 Coffee 클래스에서 음료를 만들라는 명령을 할때 설탕과 우유를 추가하는 행위도 추가해야 되기 때문입니다.
addOtherItem( )
이 실행된다면 메서드 행위도 재정의 해주었으므로 Coffee 클래스에 맞게 실행될 것입니다.
class Tea extends Drink {
@Override
public void addMainItem() {
System.out.println("티백 추가");
}
@Override
public void addOtherItem() {
}
}
Tea
클래스는 addMainItem( )
의 행위를 재정의하고 addOtherItem( )
은 재정의할 필요가 없으므로 남겨두었습니다.
public class Client {
public static void main(String[] args) {
Drink drinkCoffee = new Coffee();
drinkCoffee.makeDrink();
Drink drinkTea = new Tea();
drinkTea.makeDrink();
}
}
클라이언트에서 음료를 제조한다면 결과는 아래와 같을 것 입니다.
자바에서 템플릿 메서드 패턴 적용 예시:
java.io.InputStream
클래스는 템플릿 메서드 패턴을 따릅니다. InputStream은 추상 클래스이며, read() 메서드는 템플릿 메서드입니다. 이 메서드는 데이터를 읽어 오는 과정을 정의하고 있습니다. 하위 클래스인 FileInputStream, ByteArrayInputStream 등에서는 read() 메서드의 일부 단계를 구현하고 있습니다.
public abstract class InputStream {
public int read() throws IOException {
// 템플릿 메서드
// 1. 데이터를 읽어올 준비 작업
// ...
// 2. 추상 메서드 호출하여 실제 데이터 읽기
int b = read0();
// 3. 데이터 읽기 후 작업
// ...
return b;
}
// 하위 클래스에서 구현해야 하는 추상 메서드
protected abstract int read0() throws IOException;
}
Spring의 JdbcTemplate
클래스는 앞서 말씀드린 대로 템플릿 메서드 패턴을 따르고 있습니다. JdbcTemplate은 템플릿 메서드인 execute(), query(), update() 등을 제공하고 있습니다. 이 메서드들은 JDBC 코드 실행 과정의 골격을 정의하고 있으며, 개발자는 콜백 인터페이스를 구현하여 실제 코드 로직을 제공합니다.
public class JdbcTemplate extends JdbcAccessor {
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
// 템플릿 메서드
// 1. Connection 가져오기
// 2. Statement 생성
// 3. 콜백 실행 (action.doInStatement(stmt))
// 4. Statement, Connection 닫기
// ...
}
// 개발자가 구현해야 하는 콜백 인터페이스
public interface StatementCallback<T> {
T doInStatement(Statement stmt) throws SQLException, DataAccessException;
}
}
이처럼 템플릿 메서드 패턴은 알고리즘의 구조를 정의하고, 일부 단계는 하위 클래스나 콜백으로 구현하도록 합니다. 이를 통해 코드 재사용성과 유지보수성이 높아집니다.
팩토리 메서드
는 템플릿 메서드의 특수화라고 생각할 수 있습니다. 동시에 대규모 템플릿 메서드의 한 단계의 역할을 팩토리 메서드가 할 수 있습니다.
템플릿 메서드
는 상속을 기반으로 합니다. 이 메서드는 자식 클래스들에서 알고리즘의 부분들을 확장하여 변경할 수 있도록 합니다.
전략 패턴
은 합성을 기반으로 합니다. 전략 팬턴은 객체 행동의 일부분들을 이러한 행동에 해당하는 다양한 전략들을 제공하여 변경할 수 있습니다.
템플릿 메서드
는 클래스 수준에서 작동하므로 정적입니다. 전략 패턴
은 객체 수준에서 작동하므로 런타임에 행동들을 전환할 수 있도록 합니다.
참고
💠 템플릿 메소드(Template Method) 패턴 - 완벽 마스터하기
https://refactoring.guru/ko/design-patterns/template-method