행동 관련 : 템플릿 메서드 패턴

    들어가기 전

    이 글은 백기선님의 인프런 강의를 공부하며 작성한 글입니다. 

     


    템플릿 메서드 패턴

    • GOF : 알고리즘 구조를 서브 클래스가 확장할 수 있도록 템플릿으로 제공하는 방법
      1. 특정 알고리즘을 템플릿으로 제공함. 
      2. 이 템플릿을 이용하는 구체적인 방법을 상속받는 클래스가 정하는 형태임. 
    • Component
      • AbstractClass 
        • 추상 메서드를 가지고 있어야 함.
        • 템플릿 메서드 역할을 하는 메서드가 있어야 함. 
        • 추상 메서드를 템플릿 안에서 호출하도록 함. 
      • ConcreteClass : 추상 메서드만 구현하도록 함. 
    • 클라이언트는 Concrete Class를 구현해서 이용하면 됨. 

     

     


    패턴이 필요한 상황

    // +로 구현해주세요.
    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);
            }
        }
    }
    
    // *로 구현해주세요
    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는 현재 result += Integer.parseInt()를 하고 있음. 그런데 만약 곱, 빼기 연산을 해야하는 요구사항이 들어올 수 있음.

    +가 아니라 *, -의 추가 기능을 구현하기 위해서 기존 코드를 복사해서 해당 부분만 바꾸면 동작은 한다. 그렇지만 다음 문제점이 남아있다.

    1. 기존 코드의 상당 부분이 중복됨. 
    2. 기존 알고리즘 구조의 변경이 필요하다면, 생성된 클래스만큼 수정해야 함. 

    만약 이런 상태라면 Template Method 패턴을 이용해서 두 가지 문제점을 같이 해결해 볼 수 있다.

     


    Template Method 패턴 적용하기 

    FileProcessor 클래스에서 대부분의 작업은 동일한데, result += Integer.parseInt() 이 메서드의 사용방법만 바뀌는 상황을 가정한다. 이 경우에 Template과 구현 부분은 다음으로 나눌 수 있다.

    • 구현  : result += Integer.parseInt()
    • Template : 나머지

    이 부분을 고려해서 코드를 분리하면 다음과 같다. 

    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 = doProcess(result, line);
                }
                return result;
            } catch (IOException e) {
                throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
            }
        }
    
        // 상속해서 써야하기 때문에 protected
        protected abstract int doProcess(int result, String line);
    }

    FileProcessor 클래스는 다음과 같이 생성된다.

    • 추상 클래스로 생성.
    • 추상 메서드를 Template 메서드인 process() 안에서 호출.
    • 추상 메서드 doProcess() 선언 

    그리고 이 Template 추상 클래스를 구현하는 자식 구현체들은 다음과 같다.

    public class Plus extends FileProcessor{
    
        public Plus(String path) {
            super(path);
        }
    
        @Override
        protected int doProcess(int result, String line) {
            result += Integer.parseInt(line);
            return result;
        }
    }

    Plus 클래스는 다음과 같이 생성됨.

    • 추상 클래스 FileProcessor를 상속받고, 추상 메서드를 구현함. 
    • 부모 클래스의 추상 메서드를 제외한 어떠한 메서드도 오버라이딩 하지 않음. 
    // 기존 코드
    public class Client {
        public static void main(String[] args) {
            FileProcessor fileProcessor = new FileProcessor("number.txt");
            int result = fileProcessor.process();
            System.out.println(result);
        }
    }
    
    
    // Template Method 패턴 사용
    public class Client {
    
        public static void main(String[] args) {
            FileProcessor fileProcessor = new Multiply("number.txt");
            int result = fileProcessor.process((sum, number) -> sum += number);
            System.out.println(result);
        }
    }

    이를 사용하는 클라이언트의 코드는 변경점이 없다.

     


    Template Callback 패턴

    Template Method 패턴은 템플릿 클래스를 '상속'받아야 하기 때문에 Template과 구현체 클래스의 결합이 강하게 이루어진다. 이 부분을 개선하기 위해 Template Callback 패턴을 사용을 고려해 볼 수 있다. 

    • Callback 객체를 통해 위임을 이용해 상속을 제거함. 
    • Template Callback 관련
      • Template Callback은 Interface를 사용해서 정의한다. 
      • Template 클래스는 구체적인 클래스가 된다.
      • Template을 제외한 구현체 부분을 처리하기 위해 Callback 객체를 넘기고, Callback 객체가 구체적인 작업을 처리함.
      • Callback을 생성자를 통해서 전달, 메서드를 통해서 전달할 수 있음. 
    • Strategy 패턴과의 차이점은?
      • Strategy 패턴은 여러 Callback을 가질 수 있음. 
      • Template Callback 패턴은 1개의 Callback만 가짐.
    // Callback용 인터페이스 정의
    public interface Operator {
        int doProcess(int result, String line);
    }
    
    // Callback 구현
    public class MultiPly implements Operator {
        @Override
        public int doProcess(int result, String line) {
            result *= Integer.parseInt(line);
            return result;
        }
    }

    Callback 인터페이스와 구현체를 생성한다. ..

    public class FileProcessor {
        private String path;
        private final Operator function;
    
    	// Callback을 생성자로 주입 받음.
        public FileProcessor(String path, Operator function) {
            this.path = path;
            this.function = function;
        }
    
    	// 메서드에 Callback을 주입할 수도 있음.
        public int process() {
            try(BufferedReader reader = new BufferedReader(new FileReader(path))) {
                int result = 0;
                String line = null;
                while((line = reader.readLine()) != null) {
                    result = this.function.doProcess(result, line);
                }
                return result;
            } catch (IOException e) {
                throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
            }
        }
    }

    FileProcessor는 Callback 객체를 전달받아서 처리한다. 이 때 Callback 객체는 두 가지 방법으로 전달할 수 있다. 

    • 생성자로 전달
    • 메서드로 전달

     위 코드에서는 생성자로 전달했다. 

     


    Template Method 패턴의 장/단점

    • 장점
      • 템플릿 코드를 재사용하고 중복 코드를 줄일 수 있음. 
      • 상속을 통해 구체적인 알고리즘만 변경할 수 있음. 
    • 단점
      • 리스코프 치환 원칙을 위반할 수도 있음.
      • 알고리즘 구조가 복잡할수록 템플릿을 유지하기 어려워짐.

    리스코프 치환 원칙은 상속받은 자식 클래스가 부모 클래스가 원래 하려던 동작을 그대로 유지해야한다는 것을 의미한다. 만약 상위 클래스에서는 '도형을 돌린다'라는 의도를 작성했는데, 자식 클래스에서 '도형을 숨긴다'로 변경되는 경우 '리스코프 치환 원칙'을 위배하는 것이다. 

    리스코프 치환 원칙을 위배한 예시를 들어보면, 위의 FileProcessor에서 추상 메서드가 아닌 process() 메서드를 오버라이딩 하고, 항상 -1을 반환하도록 수정하는 경우가 있을 수 있다. 

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

    행동 관련 : Strategy(전략) 패턴  (0) 2023.11.30
    행동 관련 : State 패턴  (0) 2023.11.30
    구조 관련 : 프록시 패턴  (0) 2023.11.26
    구조 관련 : 파사드 패턴  (0) 2023.11.25
    행동 관련 : 옵저버 패턴  (1) 2023.11.25

    댓글

    Designed by JB FACTORY