# 템플릿 메소드(Template method) 패턴

# 템플릿 메소드 패턴이란 무엇인가요?

정의

알고리즘 구조를 서브 클래스가 확장할 수 있도록 템플릿으로 제공하는 방법.

템플릿 메서드 패턴은 여러 클래스에서 공통으로 사용하는 메서드를 상위 클래스에서 정의하고, 하위 클래스마다 다르게 구현해야 하는 세부적인 사항을 하위 클래스에서 구현하는 패턴을 말한다.

  • 코드의 중복 제거를 위해 흔히 사용하는 리팩토링 기법으로 볼 수 있다.

image

  • AbstractClass의 templateMethod()는 알고리즘의 구조를 표현하는 메소드이다.

# 템플릿 메소드 패턴을 언제 사용해야 할까요?

상황가정

파일을 읽은 뒤, 수행하고자 하는 로직을 실행하고, 출력하는 상황이 주어졌다.
number.txt 파일에 존재하는 숫자를 모두 더하는 로직, 모두 곱하는 로직.

만약에 우리가 그냥 작성하면 어떻게 될까?

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

@AllArgsConstructor
public class FileProcessor {
    private String 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);
        }
    }
}
  • 위의 코드는 파일을 읽고, 더하고, 결과값을 반환하는 예제이다.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

@AllArgsConstructor
public class MultiplyFileProcessor {
    private String 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);
        }
    }
}
  • 위의 코드는 파일을 읽고, 곱하고, 결과값을 반환하는 예제이다.
public class Client {
    public static void main(String[] args) {
        FileProcessor fileProcessor = new FileProcessor("number.txt");
        int result = fileProcessor.process();
        System.out.println(result);
    }
}
  • 파일 스트림을 열고 닫는 행위와 오류 처리가 중복되는 것을 확인할 수 있다.
  • 따라서 공통된 로직을 한군데로 묶고, 달라지는 부분만 추상 메서드로 분리하는 것이 합리적이다.

# 템플릿 메소드 패턴을 적용해보기

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

@AllArgsConstructor
public abstract class FileProcessor {
    private String path;

    public final int process(Operator operator) {
		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(path + "에 해당하는 파일이 없습니다.", e);
        }
    }

    protected abstract int getResult(int result, int number);
}
  • FileProcessor라는 Abstract클래스를 만드는데, 이때 덧셈과 곱셈을 하던 로직은 getResult() 로 분리한다.
@AllArgsConstructor
public class Plus implements FileProcessor {
    @Override
    public int getResult(int result, int number) {
        return result += number;
    }
}
@AllArgsConstructor
public class Multiply extends FileProcessor {
    @Override
    protected int getResult(int result, int number) {
        return result *= number;
    }
}
  • 각 로직마다 FileProcessor를 상속하여, int getResult(int result, int number)를 직접 구현한다.
public class Client {
    public static void main(String[] args) {
        FileProcessor fileProcessor = new Multiply("number.txt");
        int result = fileProcessor.process();
        System.out.println(result);
    }
}
  • 사용시는 비슷하게 사용된다.

# 템플릿 메소드 패턴의 장단점

장점

  • 템플릿 코드를 재사용하고 중복 코드를 줄일 수 있다.
  • 템플릿 코드를 변경하지 않고 상속을 받아서 구체적인 알고리즘만 변경할 수 있다.

단점

  • 리스코프 치환 원칙을 위반할 수도 있다. : 상속을 받은 자식 클래스가 부모가 의도한 대로 동작해야 한다. → 파이널로 정의해서 메소드 변경을 막는다.
  • 알고리즘 구조가 복잡할수록 템플릿을 유지하기 어려워진다.

# 템플릿 패턴의 예시

  1. 자바의 서블릿
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MyHello extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
}
  • HttpServlet을 extends하고 doGet또는 doPost메서드를 오버라이드 하고, 서블릿이 자신의 로직을 수행한다.
  • 이후 doGet, doPost를 호출할 때, 위의 클래스를 참조하여 doGet, doPost 메서드를 실행한다.( 이때 해당 코드에 대한 제어권은 우리에게 없다. DI )
  1. 스프링의 Configuration
import java.util.Arrays;

@Configuration
class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().permitAll();
    }
}
  • WebSecurityConfigureAdapter를 extends하고 configure메서드를 오버라이드하면 우리가 스프링 Config의 거대한 알고리즘들 중 우리가 일부를 구현하게 되는 것이다.

# 출처

  • https://victorydntmd.tistory.com/298
  • https://kingchan223.tistory.com/329