ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 템플릿 메서드 패턴 (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
Designed by Tistory.