2017년 1월 24일 화요일

Template Method 패턴

자바 디자인 패턴 4 - Template Method

http://iilii.egloos.com/3806897
1. Template Method 패턴은..

전체적인 로직에는 큰 차이가 없지만 일부분만 바뀌는 비스무레한 몇 가지 클래스가 있다고 칩시다. 일부분을 위해서 전체를 새로 작성할 필요는 없지요. Template Method에서는 전반적인 구현은 상위클래스(주로 Abstract로 만듭니다.)에서 담당하고 부분적인 곳의 구체적인 구현은 하위클래스가 담당합니다.

2. 예제

------------- 템플릿 메쏘드가 있는 Abstract Class ---------------
package ch04_TemplateMethod;
public abstract class Worker {
    protected abstract void doit();    public final void work(){
        System.out.println("출근");
        doit();
        System.out.println("퇴근");
    }
}
------------- Abstract Class 구현체 1 ---------------------
package ch04_TemplateMethod;
public class Designer extends Worker {
    @Override
    protected void doit() {
        System.out.println("열심히 디자인");
    }
}
------------- Abstract Class 구현체 2 ---------------------
package ch04_TemplateMethod;
public class Gamer extends Worker {
    @Override
    protected void doit(){
        System.out.println("열심히 껨질");
    }
}
------------- 테스트 코드 ---------------------
package ch04_TemplateMethod;
public class Test {
    public static void main(String[] args) {
        Worker designer = new Designer();
        designer.work();
        Worker gamer = new Gamer();
        gamer.work();
    }
}

Worker 클래스의 work()는 내부적으로 abstract 메쏘드인 doit()을 호출하고 있습니다. work() 안에서 전반적인 로직이 수행되고, 로직 중 각각의 특성을 탈 수 있는 부분을 doit() 안에서 해결합니다. doit()은 실제 구현체에서 알아서 구현하면 됩니다.
work() 를 final로 구현한 것은 하위 클래스에서 전체적인 로직 변경을 하지 못하도록 하는 것입니다.

3. Template Method 사용시 고려사항

Template Method는 위험성을 어느 정도 내포하고 있습니다. 바로 전체적인 프로세스가 바뀌는 것입니다. 상위 클래스에서 변동이 일어날 경우 하위 클래스가 안전하리라는 보장은 할 수 없습니다. 상위 클래스에 abstract method가 하나만 추가되어도 모든 하위 클래스는 변경이 불가피합니다. 나중에 발생하는 작은 변경이 큰 재난을 일으킬 수 있습니다. 이것은 상속이 가지는 위험성입니다.
그래서 Template Method 패턴을 사용할 때는 상위클래스에 대한 심사숙고가 반드시 필요합니다. 
일반적으로는 전체적인 프로세스를 담당하는 로직을 final 메쏘드로 정의하기도 하지만, 프로세스 자체의 변경을 고려해 상속의 여지를 남겨두기 위해 final 메쏘드로 정의하지 않기도 합니다.
또 한가지는 하위 클래스의 메쏘드들은 외부에서 직접 호출되지 않고 상위 클래스의 Template Method에서 호출됩니다. 그래서 주로 protected 로 선언됩니다. 그런 이유로 외부의 호출과 구체적인 구현체의 메쏘드가 실행되기까지의 과정을 쉽게 파악하기가 어렵습니다. 문제가 생겼을 때 추적이 어려울 수도 있다는 것이죠.

4. JAVA API에 있는 Template Method

아마 JDK안에 가장 많이 들어 있는 패턴 중 하나가 Template Method일 겁니다.
Servlet 을 개발할 때, HttpServlet을 상속 받아 doGet() 과 doPost() 를 구현합니다. HttpServlet의 service() 에서 하위구현체의 doGet()이나 doPost() 등으로 분기를 시킵니다.(그 외에도 do머시기하는 메쏘드가 있지만 생략합니다.) Template Method의 전형적인 사용법 중 하나입니다. 분기를 담당하는 부분은 상위클래스에 구체적으로 구현하고, 분기된 이후의 행동은 하위구현체에 떠 넘기는 방법이죠.

HashSet이라는 Set의 구현체를 아시죠? 얘는 Set이니까 중복 데이터를 요소로 가지지 않습니다. 그럼 '같다 다르다'의 기준은 뭘까요? 내용이 같으면 될까요? 아니면, 실제 레퍼런스가 같아야 할까요?
이름에 나와 있듯 hash 값이 일단 같아야 합니다. hash값은 Object의 hashCode() 메쏘드를 이용하여 체크합니다. 그리고 나서 또 Object의 equals() 메쏘드를 호출합니다. 그래서 Set에 넣을 요소에 대해서는 hashCode() 메쏘드와 equals() 메쏘드가 잘 구현되어있어야 합니다. 그렇지 않으면, 중복된 데이터가 삽입될 수 있습니다.
HashSet의 경우는 위에서 설명한 Template Method와는 좀 다릅니다. 상위클래스와 하위클래스의 상관관계 같은 게 없죠. 다만 나도 모르는 사이에 내가 구현한 메쏘드가 호출될 수 있기 때문에 Template Method가 될 수 있습니다.(HashSet의 add(SomeClass) 메쏘드는 SomeClass의 hashCode()와 equals()를 호출합니다. )
정확히 말하면 Set.add(SomeClass) 메쏘드는 Set.contains(SomeClass) 메쏘드를 호출하고 그 안에서 다시 SomeClass.hashCode()와 SomeClass.equals(SomeClass) 를 호출합니다. ( 중간에 좀 더 복잡한 과정이 있지만, 설명할 필요는 없으므로 생략합니다. )
여기서 UI관련 각종 EventListener들도 그런 맥락에서 Template Method라고 볼 수 있습니다. event 발생에서 리스너까지의 과정을 알 필요가 없습니다. 대략 어떤 이벤트를 발생시키면, 어떤 이벤트 리스너한테 전달된다 정도만 알면 되죠. 이벤트 핸들링에는 그 외에도 여러가지 패턴들이 적용되어 있습니다.

http://choipattern.blogspot.kr/2013/08/template-method.html



  • 템플릿이란 무엇인가?

템플릿이란 문자 모양으로 구멍이 뚫려있는 얇은 플라스틱 판을 말합니다.

그 구멍을 따라 펜으로 그리면 손으로도 반듯한 문자를 쓸 수 있습니다.

템플릿의 구멍을 보면 어떤 모양의 문자인지는 알 수 있지만,

실제로 어떤 문자가 될지는 필기구에 의해 결정됩니다.

펜을 사용하면 펜으로 쓴 문자가 되고, 연필을 사용하면 연필로 쓴 문자가 됩니다.

그러나 어떤 필기구를 사용해도 쓰여진 문자는 템플릿 구멍의 형태와 동일합니다.





  • Template Method 패턴이란

이번 장에서 배울 Template Method 패턴은 템플릿의 기능을 가진 패턴입니다.

상위 클래스쪽에 템플릿에 해당하는 메소드가 정의되어 있고,

그 메소드의 정의 안에는 추상 메소드가 사용되고 있습니다.

따라서 상위 클래스의 프로그램만 보면 추상 메소드를 어떻게 호출 하고 잇는지 알 수 있지만,

최종적으로 어떤 처리가 수행되는지는 알 수 없습니다.



추상 메소드를 실제로 구현하는 것은 하위 클래스입니다.,

하위 클래스 측에서 메소드를 현하면 구체적인 처리가 결정됩니다.

서로 다른 하위 클래스가 서로 다른 구현을 실행하면 서로 다른 처리가 실행될 것입니다.

그러나 어떤 하위 클래스에서 어떤 구현을 하더라도 처리의 뼈대를 결정하고,

하위 클래스에서 그 구체적인 내용을 결정하는 디자인 패턴을 


Template Method 패턴


이라고 부릅니다. 



Template Method 패턴을 사용한 예제 프로그램을 살펴봅시다.

여기에서 작성할 예제 프로그램은 ' 문자나 문자열을 5회 반복해서 표시하기' 입니다.

여기에서는 AbstractDisplay, CharDisplay, StringDisplay, Main 이라는 네 가지 클래스가 등장합니다.

AbstractDisplay 클래스에서는 display 메소드가 정의되어 있고

display 메소드 안에서는 open. print, close 라는 세 가지 메소드가 사용되고 있습니다.

Open, print, close 라는 세 개의 메소드도 AbstractDisplay 클래스 안에서 선언되고있지만 실체가없는 추상메소드입니다.


여기에서는 추상 메소드를 사용하고 잇는 display 메소드가 템플릿 메소드가 됩니다.


open, print, close 메소드를 실제로 구현하고 있는 것은 AbstractDisplay 클래스의 하위 클래스인 CharDisplay 클래스나 StringDisplay 클래스 입니다.

Main 클래스는 동장 테스트를 위한 클래스입니다.



AbstractDisplaysy      -         메소드 display만 구현되고 있는 추상 클래스

CharDisplay               -         메소드 Open, Print, Close 를 구현하고 있는 클래스

StringDisplay             -         메소드 Open, Print, Close 를 구현하고 있는 클래스

Main                         -         동작 테스트용 클래스










  • AbstractDisplay 클래스

AbstractDisplay 클래스는 open, print, close, display 라는 메소드를 가지고 있습니다.

이중에서 open, print, close 는 추상 메소드이고 display 메소드만이 구현되고 있습니다.

AbstractDisplay 클래스에 쓰여있는 display 메소드의 정의를 읽어 보면

다음과 같은 처리를 실행하고 있습니다.


open 메소드를 호출
print 메소드를 5회 호출
close 메소드를 호출


그러면 open, print, close의 각 메소드는 무엇을 하고 있을까요?

AbstractDisplay 클래스를 읽어 보면, 이 세 가지 메소드는 추상 메소드이기 때문에

AbstractDisplay가 '실제로 무엇을 하고 있는지 AbstractDisplay 클래스만 보고서는 알 수 없습니다.

실제로 무엇을 하고 있는가는 open,print, close 를 구현하는 하위 클래스에게 맡기고 있습니다.



Public abstract class AbstractDisplay {

         public abstract void open();
         public abstract void print();
         public abstract void close();
         
         public final void display()  {

                open();

                for (int i = 0; i < 5; i++)  {
            
                      print();

                }

                 close();
         }

]












  • CharDisplay 클래스

여기까지 이해가 되었다면 이번에는 하위 클래스의 하나인 CharDisplay 클래스를 살펴봅시다.

상위 클래스인 AbstractDisplay 클래스에서 추상 메소드였던 open, print, close 가 모두 구현되어 있기 때문에

CharDisplay 클래스는 추상 클래스가 아닙니다.

CharDisplay 클래스의 open, print, close 는 다음과 같은 처리를 실행하고 있습니다.



open        :       문자열 "<<" 표시한다

print         :       생성자에서 주어진 1문자를 표시한다

close       :       문자열 ">>" 을 표시한다.



이 상태에서 display 메소드를 호출된다면 어떻게 될까요?

예를 들어 생성자에 'H' 라는 문자가 전달되었다고 가정하면


<<HHHHH>>


라는 문자열이 표시됩니다. 




public class CharDisplay extends AbstractDisplay  {


        private char ch;

        public CharDisplay (char ch)  {

                 this.ch = ch;

       }


       public void open()  {

            System.out.print("<<");

       }

       public void print()   {

              System.out.print(ch);

        }


        public void close()  {

           System.out.print(">>");


        }


}










  • StringDisplay 클래스

그러면 또 하나의 하위 클래스인 StringDisplay 클래스를 살펴 봅시다.

물론 여기에서도 open, print, close 가 구현되고 있습니다.

이번에는 어떤 처리를 수행하고 있을까요?

StringDisplay의 open, print, close는 다음과 같은 처리를 실행하고 있습니다.

이 상태에서 display 메소드가 호출된다면 어떻게 될까요?

생성자에 "Hello, world" 라는 문자열이 전달되어 있다고 가정하면

----------------
ㅣHello, world.ㅣ

ㅣHello, world.ㅣ
ㅣHello, world.ㅣ
ㅣHello, world.ㅣ
----------------


와 같이 테두리에 둘러싸인 문자열이 표시됩니다.



open          :       문자열 " +--------+"을 표시한다

print           :       생성자에서 주어진 문자열을 "ㅣ" 와 "ㅣ" 사이에 표시한다.

close         :       문자열 " +--------+을 표시한다.






public class StringDisplay extends AbstractDisplay {

        
         private String string;

         private int width;

         pubilc StringDisplay (String string)  {

                this.string  = string;
                this.width   = string.getBytes().length;

         }

         public void open()  {

              printLine();

         }


         public void print()   {

               System.out.println("l" + string + "l");


         }

         public void close()  {

              printLine();


        }

        private void printLine()  {

                 System.out.print("+");

                 for(int i= 0; i< width; i++) {

                      System.out.print("-");

                 }

                System.out.println("+");

            
       }

}







  • Main 클래스

Main 클래스는 동작 테스트를 실행합니다.

지금까지 만들었던 
CharDisplay 클래스와 StringDisplasy 클래스의 인스턴스를 만들어 display 메소드를 호출하고 있습니다.






public class Main  {

          public static void main(String[] args)   {

               AbstractDisplay d1 = new CharDisplay('H');

               AbstractDisplay d2 = new StringDisplay("hello, world.");

               AbstractDisplay d3 = new StringDisplay("안녕하세요.");


              d1.display();

              d2.display();

              d3.display();

          }

}












  • Template Method 패턴의 등장인물


 AbstractClass(추상 클래스)의 역할

 AbstractClass 는 템플릿 메소드를 구현합니다.

또한 그 템플릿 메소드에서 사용하고 있는 추상 메소드를 선언합니다.

이 추상 메소드는 하위 클래스인 ConcreteClass 역할에 의해 구현됩니다.

예제 프로그램에서는 AbstractDisplay 클래스가 이 역할을 합니다.



ConcreteClass(구현 클래스)의 역할

AbstractClass 역할에서 정의되어 있는 추상 메소드를 구체적으로 구현합니다.

여기에서 구현한 메소드는 AbstractClass 역의 템플릿 메소드에서 호출됩니다.

예제 프로그램에서는 CharDisplay 클래스나 StringDisplay 클래스가 이 역할을 합니다.








  • 로직을 공통화할 수 있다.
Template Method 패턴을 사용하면 어떤 이점이 있을까요?

상위 클래스의 템플릿 메소드에서 알고리즘이 기술되어 있으므로, 

하위 클래스측에서는 알고리즘을 일일이 기술할 필요가 없습니다.


예를 들어 Template Method 패턴을 사용하지 않고, 

에디터의 복사 & 붙여넣기 기능을 사용해서 복수의ConcreteClass 역할을 만들었다고 가정합시다.

ConcreteClass1, ConcreteClass2, ConcreteClass3 은 모두 비슷하지만 다른 클래스가 됩니다.

작성한 직후에는 괜찮짐나 나중에 ConcreteClass1 에 버그가 발견되면 대체 어떻게 될까요?

1개의 버그를 수정하기 위해 모든 ConcreteClass를 수정해야 됩니다.

그런점에서 Template Method 패턴으로 프로그래밍을 하면 템플릿 메소드에 오류가 발견되더라고  템플릿 메소드만 수정하면 됩니다.







  • 상위 클래스와 하위 클래스의 연계
Template Method 패턴에서는 상위 클래스와 하위 클래스가 긴밀하게 연락을 취하며 작동하고 있습니다.

따라서 상위 클래스에서 선언된 추상 메소드를 실제로 하위 클래스에서 구현할 때에는 

그 메소드가 어느 타이밍에 호출되는지 이해해야 합니다.

상위 클래스와 소스 프로그램이 없으면 하위 클래스의 구현이 어려울 수도 있습니다.






  • 하위 클래스를 상위 클래스와 동일시한다.
예제 프로그램 안에서는 

CharDisplay의 인스턴스도, StringDisplay 의 인스턴스도 AbstractDisplay형의 변수에 대입하고 있습니다.

그리고 display 메소드를 호출하고 잇습니다.

상위 클래스형의 변수가 있고, 그 변수에 하위 클래스형의 인스턴스가 대입된다고 가정합니다.

이떄 instanceof 등으로 하위 클래스의 종류를 특정하지 않아도 프로그램이 작동하도록 만드는 것이 좋습니다.


"상위 클래스형의 변수에 하위 클래스의 어떤한 인스턴스를 대입해도 제대로 작송할 수 있도록 한다" 

는 원칙은 lsp이라고 불립니다.,

이 LSP는 Template Method 패턴에 국한되지 않는 상속의 일반적인 원칙입니다.

댓글 없음:

댓글 쓰기