Template Method Pattern
템플릿 메서드(Template Method) 패턴은 여러 클래스에서 공통으로 사용하는 메서드를
템플릿화 하여 상위 클래스에서 정의하고, 하위 클래스마다 세부 동작 사항을 다르게 구현하는 패턴이다.
즉, 변하지 않는 기능(템플릿)은 상위 클래스에 만들어두고
자주 변경되며 확장할 기능은 하위 클래스에서 만들도록 하여,
상위의 메서드 실행 동작 순서는 고정하면서 세부 실행 내용은 다양화될 수 있는 경우에 사용된다.
템플릿 메서드 패턴은 상속이라는 기술을 극대화하여, 알고리즘의 뼈대를 맞추는 것에 초점을 둔다.
이미 수많은 프레임워크에서 많은 부분에 템플릿 메서드 패턴 코드가 우리도 모르게 적용되어 있다.
Template Method Pattern 구조
- AbstractClass (추상 클래스)
템플릿 메서드를 구현하고, 템플릿 메서드에서 돌아가는 추상 메서드를 선언한다.
이 추상 메서드는 하위 클래스인 ConcreteClass 역할에 의해 구현된다. - ConcreteClass (구현 클래스)
AbstractClass를 상속하고 추상 메서드를 구체적으로 구현한다.
ConcreteClass에서 구현한 메서드는 AbstractClass의 템플릿 메서드에서 호출된다.
이때 Abstract Class에서는
고정된 메서드와 하위 클래스마다 세부 동작을 다르게 설정해야 하는 추상 메서드를 가질 수 있다.
만약 상속받는 하위 클래스에서 모두 다른 동작을 구현해야 한다면 추상 메서드로 정의한다.
만약 상속받는 하위 클래스에서 모두 같은 코드를 구현해야 한다면
굳이 추상 메서드로 선언하지 않고 메서드 로직을 작성하면 된다.
이를 통해 하위 클래스의 공통로직은 ConcreteOperation()으로 처리하고,
하위클래스의 다른 메서드만 primitiveOperation()인 추상 메서드로 선언하여
알고리즘의 뼈대를 구성할 수 있다. 이에 대한 예시 코드는 다음과 같다.
public abstract class AbstractClass {
// 메서드를 final 키워드를 통해 불변 메서드로 사용
final void templateMethod(){
primitiveOperation1();
primitiveOperation2();
concreteOperation();
}
// 하위 클래스에서 번형되는 로직은 추상 메서드로 선언
abstract void primitiveOperation1();
abstract void primitiveOperation2();
// 하위 클래스에서 변형되지 않는 로직은
// final 메서드로 선언
final void concreteOperation(){
System.out.println("Concrete Operation");
}
}
// AbstractClass 객체를 구현하는 구현체
public class ConcreteClass1 extends AbstractClass {
// 추상 메서드를 구현하여 내부 알고리즘 생성
@Override
void primitiveOperation1() {
System.out.println("This is ConcreteClass1");
System.out.println("Execute primitiveOperation1");
}
// 추상 메서드를 구현하여 내부 알고리즘 생성
@Override
void primitiveOperation2() {
System.out.println("This is ConcreteClass1");
System.out.println("Execute primitiveOperation2");
}
}
// AbstractClass 객체를 구현하는 구현체
public class ConcreteClass2 extends AbstractClass {
// 추상 메서드를 구현하여 내부 알고리즘 생성
@Override
void primitiveOperation1() {
System.out.println("This is ConcreteClass2");
System.out.println("Execute primitiveOperation1");
}
// 추상 메서드를 구현하여 내부 알고리즘 생성
@Override
void primitiveOperation2() {
System.out.println("This is ConcreteClass2");
System.out.println("Execute primitiveOperation2");
}
}
public class Client {
public static void main(String[] args) {
// ConcreteClass1 객체를 생성
AbstractClass class1 = new ConcreteClass1();
// ConcreteClass1의 templateMethod()를 실행
class1.templateMethod();
// ConcreteClass2 객체를 생성
AbstractClass class2 = new ConcreteClass2();
// ConcreteClass2의 templateMethod()를 실행
class2.templateMethod();
}
}
Hook의 사용
훅(Hook) 메서드는 부모의 템플릿 메서드의 영향이나 순서를 제어하고 싶을 때
사용되는 메서드 형태를 말한다. 위의 그림에서 보듯이 템플릿 메서드 내에 실행되는
동작을 flag를 통해 메서드 flag의 true, false 유무에 따라 다음 스텝을 어떻게 이어나갈지 지정한다.
이를 통해 자식 클래스에서 좀 더 유연하게 템플릿 메서드의 알고리즘 로직을 다양화할 수 있다.
훅 메서드는 추상 메서드가 아닌 일반 메서드로 구현하는데,
선택적으로 오버라이드 하여 자식 클래스에서 제어하거나 아니면 놔두거나 하기 위해서이다.
이를 통해 Abstract Class를 상속받는 하위 클래스에 대해
final로 선언한 Template Method들을 flag에 따라 유연하게 실행할 수 있다.
Hook을 적용하는 예시 코드를 살펴보면 다음과 같다.
public abstract class AbstractClass {
final void templateMethod(){
primitiveOperation1();
primitiveOperation2();
concreteOperation();
// hook()의 return 값에 따라 메서드를 실행
if (hook()) hookMethod();
}
abstract void primitiveOperation1();
abstract void primitiveOperation2();
final void concreteOperation(){
System.out.println("Concrete Operation");
}
final void hookMethod(){
System.out.println("This is Hook Method");
}
// 기본 상위 클래스에서는 true를 return하고
// 하위 클래스에서 오버라이드하여 변경
boolean hook(){
return true;
}
}
public class ConcreteClass1 extends AbstractClass {
private final Scanner scanner = new Scanner(System.in);
@Override
void primitiveOperation1() {
System.out.println("This is ConcreteClass1");
System.out.println("Execute primitiveOperation1");
}
@Override
void primitiveOperation2() {
System.out.println("This is ConcreteClass1");
System.out.println("Execute primitiveOperation2");
}
// 하위 클래스에서 hook()을 오버라이드하여 재정의한다.
// 이를 통해 flag값을 제어하여 final에 존재하는 메서드 실행 유뮤를 조정
@Override
public boolean hook(){
String answer = getUserInput();
return answer.toLowerCase().startsWith("y");
}
public String getUserInput() {
return scanner.next();
}
}
hook을 사용하는 기본 구조는 다음과 같다.
상위 클래스에서는 true를 기본적으로 return 하도록 구현하고,
하위 클래스에서 선택적으로 오버라이드하여 구현할 수 있다.
이를 통해 훨씬 더 유연하게 템플릿 안에 존재하는 알고리즘들을 조정할 수 있다.
하지만 hook()을 오버라이드하여 사용한다는 것은 하위 클래스에서 상위 클래스의
기본 구현 로직을 억제한다는 것으로 이는 LSP 원리에 부합하지 않는다.
따라서 너무나 많은 hook()을 남용하는 것은 바람직하지 않다.
Template Method Pattern 특징
패턴 적용 시기
- 클라이언트가 알고리즘의 특정 단계만 확장하고,
전체 알고리즘이나 해당 구조는 확장하지 않도록 할 때 - 동일한 기능은 상위 클래스에서 정의하면서 확장,
변화가 필요한 부분만 하위 클래스에서 구현할 때
패턴 장
- 클라이언트가 대규모 알고리즘의 특정 부분만 재정의하도록 하여,
알고리즘의 다른 부분에 발생하는 변경 사항의 영향을 덜 받도록 한다. - 상위 추상클래스로 로직을 공통화하여 코드의 중복을 줄일 수 있다.
- 서브 클래스의 역할을 줄이고, 핵심 로직을 상위 클래스에서 관리함으로써 관리가 용이해진다
패턴 단점
- 알고리즘의 제공된 골격에 의해 유연성이 제한될 수 있다.
- 알고리즘 구조가 복잡할수록 템플릿 로직 형태를 유지하기 어려워진다.
- 추상 메서드가 많아지면서 클래스의 생성, 관리가 어려워질 수 있다.
- 상위 클래스에서 선언된 추상 메서드를 하위 클래스에서 구현할 때,
그 메서드가 어느 타이밍에서 호출되는지 클래스 로직을 이해해야 할 필요가 있다. - 로직에 변화가 생겨 상위 클래스를 수정할 때, 모든 서브 클래스의 수정이 필요할 수도 있다.
- 하위 클래스를 통해 기본 단계 구현을 억제하여 LSP 원칙을 위반할 여지가 있다.
Template Method Pattern 적용 예시
Template Method Pattern은 이미 여러 방면에서 활용되고 있으며,
Spring Framework에서도 HttpServlet, Configuration 등에서 사용된다.
또한 데이터베이스 연결을 관리할 때 단순히 드라이버 로드와 연결 / 해제를 넘어,
커넥션 풀링(Connection Pooling), 트랜잭션 관리, 예외 처리 등 다양한 요소를 고려하게 된다.
이러한 데이터베이스 연결을 Template Method Pattern을 통해 적용할 수 있다.
데이터베이스에 연결하여 데이터를 처리하는 과정은 흐름의 순서가 정해져 있고,
연결과정은 동일하지만 쿼리문을 가지고 있는 비즈니스 로직만 달라진다.
따라서 비즈니스 로직을 추상 메서드로 정의하고, 다른 변하지 않는 알고리즘들은
final로 선언하여 Template Method Pattern을 적용시킬 수 있다.
public abstract class DBConnectionTemplate {
// 템플릿 메서드: DB 처리의 전체 흐름을 정의
final void process() {
try {
// 1. 드라이버 로드
loadDriver();
// 2. DB 커넥션 열기
Connection connection = openConnection();
// 3. 트랜잭션 시작
beginTransaction(connection);
// 4. 실제 비즈니스 로직 수행
executeBusinessLogic(connection);
// 5. 트랜잭션 커밋
commitTransaction(connection);
} catch (SQLException e) {
// 예외 발생 시 트랜잭션 롤백
handleException(e);
} finally {
// 6. DB 연결 닫기
closeConnection();
}
}
// 추상 메서드: 하위 클래스에서 구현할 부분
protected abstract void loadDriver();
protected abstract Connection openConnection() throws SQLException;
protected abstract void executeBusinessLogic(Connection connection) throws SQLException;
// 공통 메서드: 트랜잭션 관리 및 예외 처리
final void beginTransaction(Connection connection) throws SQLException {
System.out.println("트랜잭션 시작");
connection.setAutoCommit(false);
}
final void commitTransaction(Connection connection) throws SQLException {
System.out.println("트랜잭션 커밋");
connection.commit();
}
final void handleException(SQLException e) {
System.out.println("예외 발생: " + e.getMessage());
e.printStackTrace();
}
final void closeConnection() {
System.out.println("DB 연결을 닫습니다.");
}
}
DBConnectionTemplate 클래스는 DB 연결 과정에서 발생할 수 있는 다양한 과정을
템플릿 메서드로 정의한다. 이는 실무에서 자주 사용하는 트랜잭션 관리, 예외 처리,
커넥션 해제를 자동화하여 데이터베이스 연결에 대한 알고리즘 뼈대를 맞추는 것이다.
또한 데이터베이스의 종류가 여러 개 일수 있으므로, 드라이버 로드와 커넥션 연결 또한
추상 메서드로 정의하여 하위 클래스에서 해당 동작을 구현하도록 한다.
하위 클래스는 MySQL에 적합한 방식으로 구체적인 연결 및 비즈니스 로직을 정의한다.
이 구조를 사용하면 다른 DB 시스템에 대해서도 쉽게 확장할 수 있습니다.
트랜잭션 관리 및 예외 처리를 포함하여 발생할 수 있는 복잡한 상황을 또한 제어할 수 있다.
이 예시는 다양한 DB 시스템을 효율적으로 처리할 수 있는 패턴을 보여준다.
추가적인 요구 사항에 맞게 DBConnectionTemplate 클래스를 확장하거나 커스터마이징 할 수 있다.
참고자료
'IT > Java' 카테고리의 다른 글
[Java] Java State Pattern (2) | 2024.09.16 |
---|---|
[Java] Java Composite Pattern (11) | 2024.09.16 |
[Java] Java Iterator Pattern (0) | 2024.09.15 |
[Java] Java Adapter Pattern (1) | 2024.09.15 |
[Java] Java Singleton Pattern (2) | 2024.09.14 |