-
템플릿 메서드(Template Method) 패턴언어/디자인패턴 2024. 5. 26. 22:31
템플릿 메서드 패턴이란?
- 여러 클래스에서 공통으로 사용하는 메서드를 템플릿화하여 상위 클래스에서 정의하고, 하위 클래스마다 세부 동작 사항을 다르게 구현하는 패턴이다.
- 변하지 않는 기능은 상위 클래스에 만들어두고 자주 변경되어 확장할 기능은 하위 클래스에서 만들도록 하는 것이다.
- 상속이라는 기술을 극대화하여, 알고리즘의 뼈대를 맞추는 것에 초점을 둔다.
- 공통된 부분에서 다른 부분 혹은 확장 가능한 부분을 제공하는 느낌! (공통을 따로 뺀다는 느낌보다 공통은 그대로 두고, 바뀌는 부분만 새로 만들도록 한다!)
AbstractClass(추상 클래스) : 템플릿 메소드를 구현하고, 템플릿 메소드에 돌아가는 추상 메서드를 선언한다. 이 추상 메서드는 하위클래스 ConcreateClass에서 구현된다.
ConcreteClass(구현 클래스): AbstractClass를 상속하고 추상 메서드를 구현한다. ConcreteClass에서 구현한 메소드는 AbstractClass의 템플릿 메소드에서 호출된다.
hook메서드
- 부모의 템플릿 메서드의 영향이나 순서를 제어하고 싶을때 사용되는 메서드 형태를 말한다.
- step2()라는 메서드의 참과 거짓 여부에 따라 다음 스텝을 어떻게 이어갈지 지정한다. 이를 통해 자식 클래스에서 좀 더 유연하게 템플릿 메서드 알고리즘 로직을 다양화할 수 있다.
- 훅은 일반 메서드로 구현해여 선택적으로 오버라이드 하도록 한다.
- 이는 흐름제어용도가 아닌 하위 클래스 확장지점이 될 수도 있음을 의미
class Client { public static void main(String[] args) { // 1. 템플릿 메서드가 실행할 구현화한 하위 알고리즘 클래스 생성 AbstractTemplate templateA = new ImplementationA(); // 2. 템플릿 실행 templateA.templateMethod(); } } ]
abstract class AbstractTemplate { // 템플릿 메소드 : 메서드 앞에 final 키워드를 붙이면 자식 클래스에서 오버라이딩이 불가능함. // 자식 클래스에서 상위 템플릿을 오버라이딩해서 자기마음대로 바꾸도록 하는 행위를 원천 봉쇄 public final void templateMethod() { // 상속하여 구현되면 실행될 메소드들 step1(); step2(); if(hook()) { // 안의 로직을 실행하거나 실행하지 않음 // ... } step3(); } boolean hook() { return true; } // 상속하여 사용할 것이기 때문에 protected 접근제어자 설정 protected abstract void step1(); protected abstract void step2(); protected abstract void step3(); }
class ImplementationA extends AbstractTemplate { @Override protected void step1() {} @Override protected void step2() {} @Override protected void step3() {} } class ImplementationB extends AbstractTemplate { @Override protected void step1() {} @Override protected void step2() {} @Override protected void step3() {} // hook 메소드를 오버라이드 해서 false로 하여 템플릿에서 마지막 로직이 실행되지 않도록 설정 @Override protected boolean hook() { return false; } }
템플릿 메서드 사용 시기 및 주의점
사용시기
- 완전히 동일한(거의 비슷한)절차를 가진 코드 일부 코드는 절차 중 일부 과정의 구현만 다르고 나머진 똑같은 경우 즉, 알고리즘의 특정 단계만 확장하고, 전체 알고리즘이나 구조는 확장하지 않을 때
- DB혹은 LDAP를 사용해서 사용자를 인증하는 클래스가 있다고 가정해보자
- DB에서 사용자 인증하나 LDAP를 이용하나 사용자 정보 받고-> 인증확인하고 -> 실패 혹은 성공하면 어떻게할지 절차가 같다.
- DB혹은 LDAP를 사용해서 사용자를 인증하는 클래스가 있다고 가정해보자
- 동일한 기능은 상위 클래스에서 정의하면서 확장, 변화가 필요한 부분만 하위 클래스에서 구현할 때
주의점
- 알고리즘의 제공된 골격에 유연성 제한될 수 있음
- 구조가 복잡할수록 템플릿 로직 형태를 유지하기 어렵다.
- 추상 메서드가 많아지면 클래스의 생성 및 관리가 어려워질 수 있다.
- 상위 클래스에서 선언된 추상 메소드를 구현시, 어느 타이밍에 호출되는지 로직을 이해해야한다.
- 상위 클래스를 수정하면, 모든 서브 클래스가 수정될 수도 있다.
전략 패턴과 차이점
- 전략 패턴은 합성을 통해 해결 vs 템플릿 메서드 패턴은 상속을 통해 해결책을 제시
- 전략 패턴은 클라이언트와 객체 간의 결합이 느슨한 반면, 템플릿 메서드는 더 밀접하게 결합된다.
- 전략 패턴은 전체 알고리즘 자체를 변경할 수 있지만, 템플릿 메서드는 일부만 변경되고 나머지는 변경되지 않는다. (템플릿 종속성)
- 단일 상속만 가능한 자바에서는 전략 패턴이 더 자주 사용된다.
전략 패턴과 탬플릿 메서드 조합
- 템플릿 메서드와 전략 패턴을 함께 사용하면, 상속이 아닌 조립의 방식으로 템플릿 메서드 패턴을 활용할 수 있다.
- 스프링 프레임워크의 Template로 끝나는 클래스들이 이를 적용한다.
- 이 클래스들은 템플릿 메서드를 실행할 때 변경되는 부분을 실행할 객체 파라미터를 통해 전달받는 방식으로 구현되어 있다.
public <T> T execute(TransactionCallBack<T> action){ //트랜잭션 상태 TransactionStatus status = this.transactionManager.getTransaction(this); T result; try{ result = action.dolnTransaction(status); }//..생략 this.transactionManager.commit(status); return result; }
- execute()메서드는 트랜잭션의 시작/커밋/롤백 등의 흐름을 제공하는 템플릿 메서드이다.
- dolnTransaction 메서드가 변경 포인트 이다.
- 앞서 본 템플릿 메서드와 차이점이 있는데
- 하위 타입에서 재정의하지 않고, 전략을 파라미터로 전달받는다.
- 따라서 TransactionTemplate의 execute() 메서드를 사용하는 코드는 dolnTransaction을 구현한 클래스를 넘겨야한다.
예시
상황에 따라 다양한 연산 알고리즘 적용하기
- 파일에서 숫자값을 읽어와서 연산한 결과를 알려주는 기능을 구현해보자
class Client { public static void main(String[] args) { // 파일경로 설정 FileProcessor fileProcessor = new FileProcessor("number.txt"); // 덧셈한 결과값 얻기 int result = fileProcessor.process(); System.out.println(result); } } class FileProcessor { private String path; // 생성자로 부터 파일경로를 받아와 저장 public FileProcessor(String path) { this.path = path; } public int process() { try (BufferedReader reader = new BufferedReader(new FileReader(path))) { int result = 0; String line = null; // 파일에 있는 각 라인에 있는 숫자값들을 모두 덧셈 while ((line = reader.readLine()) != null) { result += Integer.parseInt(line); } return result; } catch (IOException e) { throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e); } } } ]
- 해당 로직에서 곱셈 혹은 나누는 추가 연산 기능이 필요하다고 하면 어떻게 해야할까?
- 이럴 때 템플릿 메서드 패턴을 사용하면 좋다!
- 공통 부분 빼고, 계산하는 로직만 다르다!
abstract class FileProcessor { private String path; // 생성자로 부터 파일경로를 받아와 저장 public FileProcessor(String path) { this.path = path; } // 템플릿 메소드 (오버라이딩 못하게 final 처리) public final int process() { try (BufferedReader reader = new BufferedReader(new FileReader(path))) { int result = getResult(); String line = null; while ((line = reader.readLine()) != null) { result = caculate(result, Integer.parseInt(line)); } return result; } catch (IOException e) { throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e); } } protected abstract int caculate(int result, int number); protected abstract int getResult(); }
- 이와 같이 템플릿으로 변경하고, 하위 클래스에서 caculate와 getResult()만 구현하도록 한다.
할리우드 원칙 준수
- 할리우드 원칙이란, 고수준 모듈(추상 클래스, 인터페이스)에 의존하고, 고수준 모듈에서 연락하라는 원칙이다.
- 객체 끼리 이상하게 얽혀 의존성이 복잡해지는 것을 의존성 부패라고 하는데, 헐리우드 원칙을 적용하면 의존성 부패를 방지할 수 있다.
//메서드 명만 다르고 하는 짓은 똑같은 클래스 class LowerA { void print(int num) { System.out.println(num); } int calculate(int n1, int n2) { return n1 + n2; } } class LowerB { void echo(int variable) { System.out.println(variable); } int operation(int n1, int n2) { return n1 * n2; } }
- 아래와 같이 상속을 활용할 수 있다.
abstract class Higher { void print(int num) { System.out.println(num); } abstract int calculate(int n1, int n2); } class LowerA extends Higher { @Override int calculate(int n1, int n2) { return n1 + n2; } } class LowerB extends Higher { @Override int calculate(int n1, int n2) { return n1 * n2; } }
- 템플릿 메서드 패턴의 핵심은 추상 클래스를 통한 코드 통합과 고수준 의존 유도이다!
- 도입에 말했듯 상속 기술의 극대화!
참고자료
https://product.kyobobook.co.kr/detail/S000001062523
'언어 > 디자인패턴' 카테고리의 다른 글
전략 패턴 (Strategy) (0) 2024.05.26