본문 바로가기

탬플릿 매서드, STRATEGY 패턴 [행위]

by Recstasy 2020. 12. 8.

STRATEGY는 대표적인 행위패턴 중의 하나다. 

 

알고리즘을 클래스화해서 객체의 행동을 쉽게 변경할 수 있는 패턴

 

STRATEGY패턴 정의에서 눈여겨 볼 점은 '객체의 행동을 쉽게 변경'이란 부분이다.

 

탬플릿 메소드 vs 스트래티지

 

스트래터지 패턴은 탬플릿 메소드랑 비슷하다.

 

두 패턴 모두 상위 레벨의 정책(알고리즘)을 하위 레벨의 구체적인 부분과 분리해주는 역할을 한다. 단, 탬플릿 메소드 패턴은 윗 사진 '특정 핀 아답터'와 같이 인터페이스를 구현한 객체를 사용한다. 가령, (충전기)8핀, 5핀 전용 아답터들은 '충전기'라는 상위 클래스를 모두 이어받았지만 특정 충전핀에 맞춰져 있는 것과 같다. 즉, 번거로움과 비용추가를 감안해야 한다. 

 

반면, 스트래터지 패턴은 선 교체로 사용할 수 있는 아답터(우측 사진)와 같다. 아답터 1개만 준비하면, 여러 기능을 쉽고 빠르게 교체하며 사용할 수 있다. 이를 다이어그램으로 표현하면 아래와 같다.

 

ex. STRATEGY 패턴

 

객체의 행위를 쉽고 빠르게 변경할 수 있는 STRATEGY패턴은 게임에서 무기를 변경하거나 포토샵에서 여러 포맷으로 export하는 식의 설계에 유용하다. 위의 다이어그램에서 "OperationContext"는 usb를 꽂을 수 있는 아답터다.

 

* jpgExportStrategy, gifExportStrategy는 AbstractOperationStrategy추상클래스를 구현했음

* gltf, jpg로 압축하는 기능은 'getOperator()'메서드로 통합(리스코프 치환)

 

jpgExportStrategy, gifExportStrategy는 모두 getOperator()메서드를 통해 각자 기능을 실행한다. OperationContext는 setOperationStrategy(operation)메서드를 통해  필요한 기능을 합성한다. 이는, usb충전 아답터에 5핀, 8핀과 같은 다양한 케이블을 꽂는 것과 같다.

 

일반적으로 Strategy패턴은 '앱 개발'에서 빈번하게 사용되는 1순위다. 어떤 어플이건 다양한 기능이 존재하고, 이때 Strategy로 '사용자가 요청한' 기능만 수행한다. 가끔 신입 개발자가 if-else구문으로 여러 기능을 처리하는 경우가 있는데, 결론적으로 최악이다. 기능이 100개 1000개로 늘어나다면? 그 개발팀은 난리난다. 조건문은 모든 기능을 확인하는 괴물이다. 기능이 추가되는 상황에서 조건문은 금지다.

 

가령, 자동차의 색상을 변경하는 앱을 생각해보자. 그리고 신입이 다음과 같이 작성했다.

 


let userColor = '사용자 요청 색상1';

const colors = ['빨강', '노랑', '파랑', '녹색', '노랑', '은색', '검정', '진회색', '은회색'];

 

for( color of colors) {

   if( userColor == color ) {

       //자동차 색상 변경

   }

}


 

for구문 내의 조건문, switch구문 모두 최악이다. 어쨌든 colors 배열의 모든 구문을 돌아버리니깐. 만일, colors가 객체이며, 각 색상에 해당하는 값이 1,000줄이 넘어가는 코드로 이뤄져 있다면? for구문은 대략 5만 줄을 읽어야 한다.

 

위의 코드에 충격받은 개발자는 아래와 같이 Strategy 추상클래스(인터페이스)부터 생성한다. 

 

  export class AbstractStrategy{
       constructor() {

       }

       operate(){
           throw new Error( 'must content this area');
       }
   }

[AbstractStrategy.js]

 

 

그리고 색상을 변경하는 기능을 갖춘 클래스를 모아놓은 폴더를 생성한다. 즉, AbstractStrategy인터페이스를 구현한 클래스(구상 클래스)다. 

 

  import {AbstractStrategy} from '../AbstractStrategy.js';

  class redColor extends AbstractStrategy {   
      constructor() {
          super();
      }

      getColor() {  
          return '빨강';
      }
  }

  class blueColor extends AbstractStrategy {   
      constructor() {
          super();
      }

      getColor() {
          return '블루';
      }
  }

  class greenColor extends AbstractStrategy {  
      constructor() {
          super();
      }

      getColor() {
          return '녹색';
      }
  }

  class yellowColor extends AbstractStrategy {   
      constructor() {
          super();
      }

      getColor() {
          return '노랑';
      }
  }

  export { redColor, blueColor, yellowColor, greenColor }

[colorChangeStrategy.js]

 

위의 구상클래스는 모두 AbstractStrategy인터페이스를 따른다. 자동차 객체는 코드의 실행 때마다 모든 색상을 받을 필요가 없다. 오직 setOperationStategy( )메서드에 인자값이 호출되었을 때만 동작한다.(색상 기능)  

 

 
  export class Car{

      constructor() {
          this.name = '그렌져';
          this.operationStrategy = null;
      }

      setOperationStrategy( operation ) {
          this.operationStrategy = operation;   
      }

      changeColor() {
          let carsColor = this.operationStrategy.getColor();
          let carsname = this.name;

          let result = `${carsname}의 색상은 ${carsColor}입니다.`;
          this.print( result );
      }

      print(result) {
          console.log(result);
      }

  }

[car.js]

 

Strategy패턴의 핵심은 주체의 '선택'이다. 따라서 'setOperationStrategy( operation )'메서드가 핵심이다. 해당 메서드의 파라미터값에 어떠한 색상클래스를 집어넣느냐에 따라 결과값이 달라진다.

 

   <script type="module">

        import { Car } from './main_car.js';
        import { redColor, blueColor, yellowColor, greenColor } from './Ability/colors/ColorChange.js';

        const colorSelection = {
            'red': new redColor(),
            'yellow': new yellowColor(),
            'green': new greenColor(),
            'blue': new blueColor()
        }
       
        const car = new Car();
       
        //사용자 Evt. 선택 값

        car.setOperationStrategy( colorSelection.red );
        car.changeColor();

        car.setOperationStrategy( colorSelection.yellow );
        car.changeColor();

        car.setOperationStrategy( colorSelection.green );
        car.changeColor();

    </script>

[client(index.html)]

 

위와 같이, 3D 자동차가 띄워진 상태에서 고객이 색상변경 버튼을 클릭하는 상황을 가정해보자. 조건문이나 반복문 따위는 필요없다. 사용자의 선택과 실행만이 있을 뿐이다.

 

[ 실행결과 ]

 

이벤트 비동기로 실행되는 프로그램에서 Strategy는 권장이 아닌 필수다.

 

 

 

 

 

 

 


템플릿 메서드

 

템플릿 메서드는 SOLID 2원칙 '개방-폐쇄원칙', '단일책임원칙'을 합친 것과 같다. 메서드를 클래스로 생성한 뒤, 추상클래스를 상속받는 방식으로 추가한다. 템플릿 메서드는 기능별로 생성한 클래스를 이용한다는 점에서 스트래터지와 비슷하다.   

 

 

하지만 템플릿 메서드는 스트래터지와 달리 '재사용'을 할 수 없다. 가령, 위의 다이어그램에는 없지만 AbstractOperation을 상속받은 gifExportOperation클래스가 있다고 가정해보자. gifExportOperation은 jpgExportOperation을 사용할 수 있을까? AbstractOperation상속을 포기하지 않는 한 불가능하다. 즉, 상속은 강한 결합을 발생시킨다. 상속으로 인한 의존성을 제거하려면 스트래터지 패턴이 답이다.

 

 

 

│스트래티지

 

 

스트래터지 패턴은 OperationContext(하위레벨)와 같은 진입점을 통해 jpgExortOperation에 대한 「직접적 의존」을 제거하며, gifExportOperation을 추가한다면 getOperator()메서드를 재사용(의존)할 수 있다. 위의 다이어그램에서는 비교적 간단한 jpg, gif 생성 알고리즘을 다뤘지만, 상당히 복잡한 알고리즘이 담긴 상위레벨의 클래스라면 스트래터지 패턴은 더욱 빛이 난다. 가령, 비슷하지만(메서드가 비슷한) 대척점이 있는 기능을 포함한 3D라이브러리 여러개를 사용하는 설계라면, jpgExportOperation위치에 3D라이브러리를 넣을 수 있다. 

 

 

댓글

최신글 전체

이미지
제목
글쓴이
등록일