Computer Basis/디자인패턴 및 설계이론

[디자인패턴] 템플릿 메소드(template-method) 패턴 & 템플릿 콜백(template-callback) 패턴

DevPing9_ 2022. 5. 22. 03:01

# 템플릿 메소드 패턴 (상속)

어떠한 로직을 서비 클래스가 확장할 수 있도록 템플릿으로 제공하는 방법

상위클래스(추상클래스)는 템플릿을 제공하고, 하위클래스는 구체적인 로직을 제공한다.
하위클래스에서 제공하는 로직이 상위클래스에서 그대로 호출이 되기 때문에
일종의 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 클래스를 만들어야 한다.

이렇게 소수의 변경사항 때문의 전체코드가 중복될 때 해당 패턴을 고려해봄직하다.

 

> FileProcessor.java (Template Method Pattern - SuperClass)

중복라인을 추상메서드로 추출하여 하위클래스에게 Implementation 을 강제한다.
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;
    }
}

> Multiply.java (Template Method Pattern - SubClass)

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);
    }
}

 


# 템플릿 콜백 패턴 (위임)

콜백으로 상속 대신 위임을 사용하는 탬플릿 패턴

> FileProcessor.java (Template Callback Pattern - Class)

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);
        }
    }
}

> Operator.java (Template Callback Pattern - Interface)

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 호출 후 로직
    }
}

 

 

[Spring Security] Filter 에 대한 오해와 진실 (doFilter의 정확한 작동방식에 대한 심층분석과 Filter의 흐

# [만연한 오해]  doFilter() 기준으로 위쪽이 API 호출 전처리 과정이고, 아래쪽이 API 호출 후처리 과정이다. (틀린 말은 아니나 상황에 따라 아닌 경우가 있으므로, 이에대해 확실히 알고 넘어갈 필

developer-ping9.tistory.com

 

728x90