디자인 패턴

템플릿 메서드 패턴 (Template Method Pattern)

개발정리 2022. 8. 13. 22:29

템플릿 메서드 패턴


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

  • 추상 클래스는 템플릿을 제공하고 하위 클래스는 구체적인 알고리즘을 사용한다.

 

예로, 아래와 같은 구조의 알고리즘이 있다 가정하자. 

  1. 파일을 읽은 뒤
  2. 적절히 파싱한다.
  3. 그리고 출력한다.

 

위 알고리즘을 템플릿으로 제공하고 각각의 기능 (1, 2, 3) 들 중 달라질 수 있는 부분이 있다면 서브 클래스는 달라지는 부분만 직접 구현한다.

 

 

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

 

 

 

템플릿 메서드 패턴 적용 전


 

간단하게 숫자로만 이루어진 .txt 파일을 읽어서 해당 파일의 숫자들을 모두 덧셈한 결과를 알려주는 기능을 구현해보자. 

 

numbers.txt 파일은 아래와 같다.

1
2
3
4
5

 

 

Client

public class Client {
  public static void main(String[] args) {
    FileProcessor fileProcessor = new FileProcessor("number.txt");
    int result = fileProcessor.process();
    System.out.println(result);
  }
}

파일의 Path 를 FileProcessor 에게 주고, process 메서드를 호출하면 연산 결과를 받을 수 있다.

 

 

FileProcessor

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

Client 코드를 실행하면 15 가 출력된다. 

이 때 덧셈이 아닌 모든 숫자들을 곱셈해주는 기능이 필요하면 어떻게 구현할 수 있을까?

아래와 같이 새로운 클래스를 정의 한 후 기존의 FileProcessor 에서 기능을 가져와 새로 곱셈으로 바꿔줄 수 있다.

 

 

MultiplyFileProcessor

public class MultiplyFileProcessor {
  private String path;

  public MultiplyFileProcessor(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);
    }
  }
}

 

 

문제점

FileProcessor 과 MultiplyFileProcessor 의 코드를 보면 연산자 부분만 다르고 나머진 모두 똑같다. 

이러한 중복되는 코드를 템플릿 메서드 패턴을 적용하여 없애보자.

 

 

 

템플릿 메서드 패턴 적용 후


 

먼저 FileProcessor 추상 클래스를 만든다.

해당 AbstractClass 는 공통되는 로직을 그대로 가지고, 달라지는 부분만 추상 메서드로 분리한다.

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)); // 아래 추상 메서드 호출
      }
      return result;
    } catch (IOException e) {
      throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
    }
  }

  protected abstract int getResult(int result, int number);
}

 

 

이제 각 ConcreteClass 들은 이를 상속하여 getResult 메서드만 구현하면 된다. 

먼저 뎃셈을 하는 PlusFileProcessor 를 보자.

public class PlusFileProcessor extends FileProcessor {

  public PlusFileProcessor(String path) {
    super(path);
  }

  @Override
  protected int getResult(int result, int number) {
    return result += number;
  }
}

 

 

곱셈을 하는 MultiplyFileProcessor 는 아래와 같다.

public class MultiplyFileProcessor extends FileProcessor {
  public MultiplyFileProcessor(String path) {
    super(path);
  }

  @Override
  protected int getResult(int result, int number) {
    return result *= number;
  }
}

 

중복되는 코드들은 AbstractClass 에 모아두고 다른 부분만 각 클래스들이 구현하도록 하여 가독성을 높이고 중복은 제거시켰다.

 

 

Client 를 보면 아래와 같다.

public class Client {
  public static void main(String[] args) {
    PlusFileProcessor plusFileProcessor = new PlusFileProcessor("numbers.txt");
    int result1 = plusFileProcessor.process();
    System.out.println(result1);

    MultiplyFileProcessor multiplyFileProcessor = new MultiplyFileProcessor("numbers.txt");
    int result2 = multiplyFileProcessor.process();
    System.out.println(result2);
  }
}

 

 

 

템플릿 콜백 패턴


템플릿 메소드 패턴 방식을 이용하여 기존의 문제점을 해결하였지만 이 패턴과 유사한 템플릿 콜백 패턴도 존재한다.

 

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

  • 상속 대신 익명 내부 클래스 또는 람다식을 사용할 수 있다.

 

 

 

 

아래와 같이 Callback 인터페이스인 Operator 를 만든다.

@FunctionalInterface
public interface Operator {
  int getResult(int result, int number);
}

 

 

FileProcessor추상 클래스가 아닌 일반 클래스가 되고, process 는 Operator 타입을 인자로 받아 Operator 의 getResult 메서드를 호출하도록 구현한다.

public class FileProcessor {
  private String path;

  public FileProcessor(String path) {
    this.path = 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 = operator.getResult(result, Integer.parseInt(line)); // operator 의 메서드를 호출
      }
      return result;
    } catch (IOException e) {
      throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
    }
  }
}

 

 

Client 는 아래와 같이 구현 할 수 있다.

public class Client {
  public static void main(String[] args) {
    FileProcessor plus = new FileProcessor("numbers.txt");
    int result1 = plus.process(((result, number) -> result += number));
    System.out.println(result1);

    FileProcessor multiply = new FileProcessor("numbers.txt");
    int result2 = plus.process(((result, number) -> result *= number));
    System.out.println(result2);
  }
}

 

 

이는 클래스를 만들지 않고 위임을 사용할 수 있는 좋은 방법이다.

물론, 클래스로 Operator 를 Implements 한 클래스를 사용한 구현도 가능하다.

 

 

 

 

장점과 단점


장점

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

 

단점

  • 리스코프 치환 법칙을 위반할 여지가 있다.
    • 하위 클래스에서 process 를 오버라이드하여 재정의 할 수 있기 때문이다.
  • 알고리즘 구조가 복잡할수록 템프릿을 유지하기 어려워진다. 

 

 

 

실무에선 어떻게 쓰이나?


자바의 서블릿 

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

 

 

스프링의 Configuration

public class TemplateInSpring {

    public static void main(String[] args) {
        // TODO 템플릿-콜백 패턴
        // JdbcTemplate
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.execute("insert");

        // RestTemplate
        RestTemplate restTemplate = new RestTemplate();

        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        headers.set("X-COM-PERSIST", "NO");
        headers.set("X-COM-LOCATION", "USA");

        HttpEntity<String> entity = new HttpEntity<String>(headers);
        ResponseEntity<String> responseEntity = restTemplate
                .exchange("http://localhost:8080/users", HttpMethod.GET, entity, String.class);
    }

    @Configuration
    class SecurityConfig extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests().anyRequest().permitAll();
        }
    }
}

WebSecurityConfigureAdapter 를 extends 하고 configure 메서드를 오버라이드하면 우리가 스프링 Config 의 거대한 알고리즘 중 일부를 우리가 구현하게 되는 것이다.

 

 

 

참고


'디자인 패턴' 카테고리의 다른 글

비지터 패턴 (Visitor Pattern)  (0) 2022.08.13
전략 패턴 (Strategy Pattern)  (1) 2022.08.13
상태 패턴 (State Pattern)  (0) 2022.08.13
메멘토 패턴 (Memento Pattern)  (0) 2022.08.12
중재자 패턴 (Mediator pattern)  (0) 2022.08.11