스트래티지 패턴 (strategy pattern) (소스코드 첨부)스트래티지 패턴 (strategy pattern) (소스코드 첨부)

Posted at 2013.11.11 14:49 | Posted in == JAVA ==/Design Patterns



facebook에 글올리기



스트래티지 패턴 (strategy pattern)


스트래티지 패턴(Strategy pattern)에서는 알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 스트래티지를 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.


상속을 이용한 간단한 동물의 행동을 호출하는 시스템에서 스트래티지 패턴(Strategy pattern)을 적용하여 보겠습니다.


1. 상속을 이용한 간단한 시스템




왼쪽의 UML 다이어그램을 살펴보자. abstract를 사용한 추상 클래스로 Animal을 만든 후, 동물 공통의 행동 함수 ; 울기(Cry), 움직이기(move), 동물의 모습(display) 메소드를 정의했다.

여기서 구현할 동물은 독수리(Eagle)과 호랑이(Tiger)로 Animal 클래스를 상속 받았다. 독수리(Eagle)와 호랑이(Tiger)는 각각의 울기(cry)와 모습(display) 메소드를 구현했다


이것이 일반적인 상속을 사용한 시스템이다. 우리는 Eagle(독수리)의 객체를 생성하여 독수리의 울음소리와 모습 그리고 공통된 움직임을 호출할 수 있다.

(호랑이도 마찬가지다)


추가되는 동물이 있으면, 새로운 동물의 객체를 만들고 Animal 추상 클래스를 상속받아 그 동물의 필요한 기능 (움직이기(move), 움직이기(cry), 모습(display))을 구현해주면 된다.


소스코드 보기


독수리와 호랑이 각각의 객체를 잘 분리하였고, 공통되는 기능을 추상클래스에서 상속받아 객체지향 프로그래밍에 맞게 잘 만든것 같다. 추가되는 동물도 생성해서 Animal 클래스를 상속받으면 된다.


2. 기능 추가


여기서 동물의 기능을 추가하려면 어떻게 해야 될까?

fly(날아가기) 기능을 추가하는 방법들에 대해 생각해보자


(1) Animal 클래스에 fly() 메소드 추가


Animal 클래스에 fly() 메소드를 추가하면, Animal 클래스를 상속받는 객체도 fly 메소드를 호출할때, fly() 기능을 호출할 수 있다. 

하지만 날아가기(fly) 기능은 동물의 공통 기능이 아니다. 

Animal 클래스에 추가하면, 상속 받고 있는 호랑이도 날아가기를 할 수 있다. 


이것은 문제가 있다. 우리는 날아가기가 가능한 동물에만 fly 메소드를 실행할 수 있어야 한다.




(2) Fly 기능을 인터페이스로 만들고 구현




두 번째는 fly() 메소드를 분리하여 인터페이스로 만든 후 Eagle에서 구현하는 방법이다. 이제 호랑이는 날 수 없고 독수리만 날 수 있다. 비둘기(Pigeon)이 추가되어도 fly() 함수가 필요하면 Fly 인터페이스를 구현하면 된다. 정상적으로 된거 같아 보인다. 

하지만 이 방법 또한 문제가 있다. fly() 함수에 변경이 생길 경우이다.

기존에 fly() 메소드를 호출하면 "날아간다" 에서 "날개를 펄럭이며 날아간다" 로 변경을 해야 한다고 가정하자. 그러면 fly() 함수의 내용을 변경하면 될 것이다.

하지만 이렇게 변경해야 하는 객체가(앵무새 까마귀, 까치 등) 몇십 몇백개라고 하면, 그 모든 객체들의 fly() 메소드를 모두 변경해야 한다. 중복이 발생한 것이다.

이것 역시 좋은 방법은 아니다.



(3) 같은 그룹을 묶어 상속



이번엔 fly() 메소드를 구현해줄 객체를 새(Bird) 객체로 묶어서 계층적으로 상속했다. 이제는 fly() 메소드를 호출할 때 "날아간다"에서 "날개를 펄럭이며 날아간다" 로 변경하려면 Bird 클래스만 수정해주면 된다.

하지만 이 역시 좋지 않은 방법이다.

새중에 날지 못하는 새가 있다. 닭, 펭귄 등이 있다.

이들은 Bird 클래스를 상속하면 안된다. 그러면 어떻게 할 것인가? 날지못하는 새 그룹(BirdNoWay) 그룹을 생성할 것인가? 이것도 올바른 방법이 아닌것 같다. 시스템이 확장될 수록 그룹이 추가될 것이고 나중에는 제대로 파악할 수 없을 것이다. 또한 새들의 공통 속성을 추가할 때면(부리 여부) 모든 새들의 그룹에 추가를 해야 한다. 

중복이 발생하며, 시스템을 더욱 복잡하게 만들뿐이다.


이것도 올바른 방법이 아니다. 




3. 디자인 원칙


디자인의 원칙을 생각해 봅시다.

"애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리 시킨다."

쉽게 풀면,

"바뀌는 부분은 따로 뽑아서 캡슐화시킨다. 그렇게 하면 나중에 바뀌지 않는 부분에는 영향을 미치지 않은 채로 그 부분만 고치거나 확장할 수 있다"


그러면 바뀌는 부분을 찾아보자, 날아가기(fly()) 와 울기(cry())가 동물에 따라 바뀝니다. (움직이기(move)는 동물 전체의 공통 속성으로 "움직인다" 로 통일한다고 가정합니다.)

날아가기(fly)와 울기(cry())를 Animal 에서 분리하여 인터페이스로 구현하겠습니다.




4. 스트래티지 패턴(Strategy Pattern)


교환 가능한 행동을 캡슐화하고 위임을 통해서 어떤 행동을 사용할지 결정한다.


(1) 변하는 부분 캡슐화


날아가기(fly()) 와 울기(cry())를 캡슐화 해보겠습니다.



먼저 Fly 인터페이스를 생성한 후 날아가기 종류별로 인터페이스를 구현했습니다.

날 수 없다(FlyNoWay)와 날개로 날아간다(FlyWithWings)를 구현했습니다.

물론 다른 방식으로 날아가는 방법이 생긴다면 클래스를 생성 후 Fly 인터페이스를 implements 해주면 됩니다.








Cry 역시 인터페이스를 생성한 뒤 3가지 우는 방식으로 구현했습니다.

새 울음(BirdCry), 울지 않는다(CryNoWay), 호랑이 울음(TigerCry)로 구현했습니다.

이것도 역시 새로운 울음 방식이 필요하면 클래스를 생성한 뒤 Cry 인터페이스를 implements 해주면 됩니다.




(2) 인터페이스에 위임


동물의 날아가기(Fly)와 울기(Cry)를 캡슐화하여 분리하였으므로, Animal 클래스에서는 fly()와 cry() 메소드를 제거해줍니다. 그리고 날아가기 와 울기 기능은 각각 캡슐화한 인터페이스에 위임합니다.

Animal 클래스에서 Fly 와 Cry 인터페이스 변수를 추가한 후 위임한 클래스에 날아가기(fly())와 울기(cry())를 요청할 수 있도록 performFly()와 performCry() 함수를 추가합니다.

performFly() 함수와 performCry() 함수는 각각 인터페이스 변수의 fly()와 cry() 함수를 호출합니다.


Animal.java


public abstract class Animal {        //Animall 상속
    
    /**
     * @uml.property  name="fly"
     * @uml.associationEnd  
     */
    protected Fly fly;                // Fly 인터페이스 선언
    /**
     * @uml.property  name="cry"
     * @uml.associationEnd  
     */
    protected Cry cry;                // Cry 인터페이스 선언
    
    public Animal(){                // Animal 생성자
        
    }
 
    public void performFly(){        // Fly 인터페이스에 연결된 객체의 fly() 함수 실행
        fly.fly();
    }
    
    public void performCry(){        // Cry 인터페이스에 연결된 객체의 Cry() 함수 실행
        cry.cry();
    }
    
    public void move(){                // move() 함수 구현
        System.out.println("움직인다.");    // 움직인다 출력
    }
    
    public abstract void display();        // display() 함수 추상화
}





(3) 각 객체 구현


이제 Animal을 상속한 독수리(Eagle)와 호랑이(Tiger) 객체를 수정해 봅시다.

기존에 Animal에서 상속받아 직접 구현했던 cry() 함수를 제거합니다. 그리고 각 객체의 생성자 함수에서 Animal 클래스의 cry와 fly 변수에 사용할 날아가기 와 울기의 객체를 대입해줍니다.

독수리는 날개로 날아가기(FlyWithWings) 새 울음(BirdCry)를 대입하겠습니다.

호랑이는 날 수 없음(FlyNoway) 호랑이 울음(TigerCry)를 대입하겠습니다.


Eagle.java


public class Eagle extends Animal {          // Animal 상속
    
    public Eagle(){                            // Eagle 객체 생성자
        
        cry = new BirdCry();             // Animal의 cry 변수에 Cry 인터페이스의 BirdCry를 연결한다.
        
        fly = new FlyWithWings();      // Animal 의 fly 변수에 Fly 인터페이스의 FlyWithWings를 연결한다.
 
    }
    
    public void display(){                   // display 함수 구현
        
        System.out.println("독수리");     // 독수리 출력
    }
 
}
 


Tiger.java


public class Tiger extends Animal{      // Animal 상속
    
    public Tiger(){                             // Tiger 객체 생성자
        
        cry = new TigerCry();           // Animal의 cry 변수에 Cry 인터페이스의 TigerCry를 연결한다.
        
        fly = new FlyNoway();         // Animal의 fly 변수에 Fly 인터페이스의 FlyNoWay를 연결한다.
    }
     
    public void display(){                         // display 함수 구현
        System.out.println("호랑이");       // 호랑이 출력
    }
}


다이어그램은 아래와 같이 됩니다.





독수리의 울기 또는 날아가기를 호출하려면 performCry()나 performFly()를 호출하면 됩니다.


스트래티지 패턴(strategy pattern)을 사용하여, 변하는 부분을 캡슐화 하고 해당 기능을 인터페이스에 위임하게 되었습니다.

이제는 객체 또는 기능이 추가/변경 되더라도 쉽고 간단하게 적용할 수 있습니다.

코드의 중복이없이 재사용이 가능해진 것입니다.


5. 객체 추가하기


그러면 이제 거북이(Turtle) 객체를 추가해보겠습니다.

Turtle 클래스를 생성한 뒤, Tiger와 Eagle과 마찬가지로 Animal 클래스를 상속합니다.

Turtle 생성자 함수에 Animal 클래스의 fly와 cry 변수에 각 기능을 연결합니다.

거북이는 날 수 없고, 울지 않으므로, FlyNoway 와 CryNoWay를 연결하면됩니다.






이제 거북이(Turtle)가 추가됐습니다.


새로운 기능이 추가될때도 마찬가지로 변하는 부분인지 변하지 않는 부분인지 판단합니다.

변하지 않는 부분일아면 Animal 클래스에 추가하고, 변하는 부분이면 캡슐화 한 뒤 Animal 클래스에서 인터페이스에 기능을 위임하면 됩니다.


6. 실행하기


이제 호랑이(Tiger), Eagle(독수리), 거북이(Turtle) 객체를 생성하고 각 행동을 호출해 봅시다.


PlayAnimal.java


public class PlayAnimal {
    
    public static void main(String[] args){            // main 함수 실행
        
        Animal tiger = new Tiger();                // Animal 클래스를 상속받은 Tiger 객체 생성
        
        tiger.display();                        // Tiger의 display() 함수 실행
        tiger.performCry();                        // Tiger의 performCry() 함수 실행
        tiger.performFly();                        // Tiger의 performFly() 함수 실행        
        
        System.out.println("------------------");
        
        Animal eagle = new Eagle();                // Animal 클래스를 상속받은 Eagle 객체 생성        
        
        eagle.display();                        // Eagle의 display() 함수 실행
        eagle.performCry();                        // Eagle의 performCry() 함수 실행
        eagle.performFly();                        // Eagle의 performFly() 함수 실행
        
        System.out.println("------------------");
        
        Animal turtle = new Turtle();            // Animal 클래스를 상속받은 Turtle 객체 생성    
        
        turtle.display();                        // Turtle의 display() 함수 실행
        turtle.performCry();                    // Turtle의 performCry() 함수 실행        
        turtle.performFly();                    // Turtle의 performFly() 함수 실행
    }
 
}
 






7. 소스 코드 확인하기


소스코드는 첨부파일로 다운받을 수도 있습니다.


PatternStrategy.zip



소스코드보기





이제 디자인 패턴의 첫번째 전략 스트래티지 전략(strategy pattern)을 알아보았습니다.


궁금하신 사항이나, 잘못된 사항은 댓글로 남겨주세요


다음은 이어서 옵저버 패턴(observer pattern)을 포스팅하겠습니다.




저작자 표시 비영리 변경 금지
신고
이웃추가
facebook에 글올리기
  1. 앰이
    오 내용이 쏙쏙 들어오네요! 마치... DB정규화를 하는 느낌이랄까..?
  2. 지하철타면서 꾸준히 공부할게요 감사합니다
  3. 지나가는 행인
    안녕 하세요. UML공부하다가 걸쳐걸쳐 검색해 오게 되었는데요..
    마지막 소스코드에서 Tiger class의 생성자 안에서 TigerCry클래스를 참조하게 되면 직접적으로
    TigerCry에 대한 Association 관계가 만들어져 버리는 이유로 상단에 표시한 클래스 다이어그램을 만족하려면

    Void Tiger::Set( Cry cryInstance )
    {
    cry=cryInstance;
    }

    와같은 함수로 다형성을 사용해야 하지않을까 하는데 어떻게 사용 하시나요?
    • 2015.01.27 11:56 신고 [Edit/Del]
      안녕하세요 의견 감사합니다.

      위의 소스 코드에 의하면 말씀하신대로 Tiger 클래스에서 TigerCry와 FlyNoway 를 직접적으로 연관을 짓게 됩니다.

      호랑이의 울음이나 날기에 대한 다른 타입이 가능할 수 있다면, 말씀하신대로 다형성을 고려한 DI를 받도록 수정을 해주는 것이 맞을것 같습니다.

      위의 예제에서는 호랑이는 "호랑이 울음" 과 "날 수 없다"가 확실하기 때문에 다른 행동이 들어올 수 없도록 강제적인 연관을 지었다고 보시면 될 것 같습니다.
      (만약 날 수 있는 호랑이가 가능하도록 확장될 수 있는 경우에는 느슨한 연결로 가능성을 열어둬야 겠죠)

      좋은 지적 감사합니다.
  4. 책 보다 이해하기 쉬웠어요...ㅋㅋㅋㅋ다른것도 업로드 하셨으면 좋겠네요
  5. 내용이 쏙쏙 정리되네요. 감사합니다.
  6. 김성우
    글이좋아서퍼갓어요 출처표기햇습니다!
  7. 감사합니다. ㅎㅎ 쉽게이해잘되었습니다.
  8. 좋은 글 감사합니다.
    그런데 호랑이와 거북이가 performFly() 메소드가 있다는 것 자체가 상당히 이상하게 보입니다.

Name __

Password __

Link (Your Website)

Comment

SECRET | 비밀글로 남기기

티스토리 툴바