상위클래스(추상클래스)는 템플릿을 제공하고, 하위클래스는 구체적인 로직을 제공한다. 하위클래스에서 제공하는 로직이 상위클래스에서 그대로 호출이 되기 때문에 일종의 IoC(Inversion of Control) 라고 볼 수 있다.
# 언제써야해?
코드가 몇줄만 다르고 거의 모든 부분이 동일할 때
실행순서를 보장하고 싶을 때
# 코드 예시
다음과 같은 FileProcessor 가 있다고 하자.
> FileProcessor.java
public abstract 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("there's no file in path : " + path);
}
}
}
지금은 단순히 라인의 수를 덧셈연산 하지만, 곱셈연산을 해달라는 요청이 있다고 가정하자. 그렇다면 10번째 줄만 result *= Integer.parseInt(line) 바뀌면 되는데
이를 위해 다른 중복코드를 담아 processMultiply() 메서드를 생성하거나, MutliplyFileProcessor 클래스를 만들어야 한다.
public abstract class FileProcessor { // 추상클래스로 new 키워드를 막아 하위클래스구현을 강제한다.
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 = getResult(result, Integer.parseInt(line));
}
return result;
} catch (IOException e){
throw new IllegalArgumentException("there's no file in path : " + path);
}
}
// 하위클래스에서만 접근가능하도록 protected
protected abstract int getResult(int result, int number);
}
> Plus.java (Template Method Pattern - SubClass)
public class Plus extends FileProcessor{
public Plus(String path){super(path);}
@Override
protected int getResult(int result, int number) {
return result +=number;
}
}
public class Multiply extends FileProcessor{
public Multiply(String path){super(path);}
@Override
protected int getResult(int result, int number) {
return result *=number;
}
}
> ClientSide
public class Client {
public static void main(String[] args) {
FileProcessor fileProcessor = new Plus("number.txt");
int result = fileProcessor.process();
System.out.println(result);
}
}
public class FileProcessor {
private String path;
public FileProcessor(String path){this.path = path;}
public int process(Operator operator){
try(BufferedReader reader = new BufferedReader(new FileReader(path))){
int result = 0;
String line = null;
while((line = reader.readLine()) != null){
result = operator.getResult(result, Integer.parseInt(line));
}
return result;
} catch (IOException e){
throw new IllegalArgumentException("there's no file in path : " + path);
}
}
}
public interface Operator{
abstract int getResult(int result, int number);
}
> ClientSide
public class Client {
public static void main(String[] args) {
FileProcessor fileProcessor = new FileProcessor("number.txt");
int result1 = fileProcessor.process(new Operator() {
@Override
public int getResult(int result, int number) {
return result+=number;
}
});
// 구현메서드가 1개일 때
int result2 = fileProcessor.process(((result, number) -> result+=number));
System.out.println(result1);
System.out.println(result2);
}
}
# 템플릿 메소드 패턴의 장점
1. 템플릿 코드(부모)를 재사용하고 중복코드를 줄일 수 있다. 2. 템플릿 코드를 변경하지 않고 상속을 받아서 구체적 로직만 변경할 수 있다.
# 템플릿 메소드 패턴의 단점
1. 리스코프 치환원칙을 위반할 수도 있다. 2. 알고리즘이 복잡할 수록 템플릿을 유지하기 어려워진다.
# 리스코프 치환원칙
상속구조에서 상위클래스를 받은 하위클래스에 의해 상위클래스 동작의 의도가 바뀌면 안된다.
예제 코드에서는 하위클래스에서 process() 를 오버라이딩하여 재작성할 수 있다. 이는 final로 막을 수 있으나, 구현하기로 한 코드 stub은 막을 수 없다.
즉, 사용자가 코드에 대해 이해가 없으면 본 의도가 깨질 수 있다.
# 알고리즘이 복잡할 수록 템플릿의 본래 의도를 유지하기 힘들어지는 예시
public abstract 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 = getResult(result, Integer.parseInt(line));
somethingDo(result);
somethingDo2(result);
result+=1;
somethingDo3(result);
}
return result;
} catch (IOException e){
throw new IllegalArgumentException("there's no file in path : " + path);
}
}
protected abstract void somethingDo(int result);
protected abstract void somethingDo2(int result);
protected abstract void somethingDo3(int result);
protected abstract int getResult(int result, int number);
}
구현해야할 코드 stub 이 많아질수록, 또 그 사이에 끼여져있는 로직이 많아질 수록 본래 의도와 다르게 로직이 진행될 수 있다.
# 템플릿 메서드 패턴 또 다른 예시
public abstract class Car {
//overrding 가능
protected void turnOn(){
System.out.println("시동을 켠다");
};
//하위 클래스에 delegate (overriding 필수)
public abstract void drive();
//overrding 가능
protected void turnOff(){
System.out.println("시동을 끈다.");
}
//HookMethod (필요하다면 재정의해서 사용할 수 있음, 재정의하지 않으면 그냥 넘어가는 메서드들)
public void beforeTurnOn(){};
public void afterTurnOn(){};
public void beforeTurnOff(){};
public void afterTurnOff(){};
// 템플릿 메서드 (overriding 불가)
public final void driving(){
beforeTurnOn();
turnOn();
afterTurnOn();
drive();
beforeTurnOff();
turnOff();
afterTurnOff();
}
}
> Spring Filter
여기에 대해서 할 말이 있는데, 필자가 처음 Filter에 대해 잘 몰랐을 때 [API 호출전 로직] 부분에 if(condition){chain.doFilter(req,res); } 를 호출 했었다.
if(condition){chain.doFilter(req,res); } 과 if(condition){chain.doFilter(req,res); return;} 의 차이는 엄청 크다.
즉, 클라이언트 입장에서는 라이브러리 이해도에 따라 기대하는 동작이 다르게 나올 수 있다.
@Component
public class TestFilter implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// API 호출 전 로직
chain.doFilter(request, response);
// API 호출 후 로직
}
}