스트래티지 패턴 (strategy pattern)
스트래티지 패턴 (strategy pattern)
스트래티지 패턴(Strategy pattern)에서는 알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 스트래티지를 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.
상속을 이용한 간단한 동물의 행동을 호출하는 시스템에서 스트래티지 패턴(Strategy pattern)을 적용하여 보겠습니다.
1. 상속을 이용한 간단한 시스템
여기서 구현할 동물은 독수리(Eagle)과 호랑이(Tiger)로 Animal 클래스를 상속 받았다. 독수리(Eagle)와 호랑이(Tiger)는 각각의 울기(cry)와 모습(display) 메소드를 구현했다왼쪽의 UML 다이어그램을 살펴보자. abstract를 사용한 추상 클래스로 Animal을 만든 후, 동물 공통의 행동 함수 ; 울기(Cry), 움직이기(move), 동물의 모습(display) 메소드를 정의했다.
이것이 일반적인 상속을 사용한 시스템이다. 우리는 Eagle(독수리)의 객체를 생성하여 독수리의 울음소리와 모습 그리고 공통된 움직임을 호출할 수 있다.
(호랑이도 마찬가지다)
추가되는 동물이 있으면, 새로운 동물의 객체를 만들고 Animal 추상 클래스를 상속받아 그 동물의 필요한 기능 (움직이기(move), 움직이기(cry), 모습(display))을 구현해주면 된다.
소스코드 보기
독수리와 호랑이 각각의 객체를 잘 분리하였고, 공통되는 기능을 추상클래스에서 상속받아 객체지향 프로그래밍에 맞게 잘 만든것 같다. 추가되는 동물도 생성해서 Animal 클래스를 상속받으면 된다.
2. 기능 추가
여기서 동물의 기능을 추가하려면 어떻게 해야 될까?
fly(날아가기) 기능을 추가하는 방법들에 대해 생각해보자
(1) Animal 클래스에 fly() 메소드 추가
하지만 날아가기(fly) 기능은 동물의 공통 기능이 아니다. Animal 클래스에 fly() 메소드를 추가하면, Animal 클래스를 상속받는 객체도 fly 메소드를 호출할때, fly() 기능을 호출할 수 있다.
Animal 클래스에 추가하면, 상속 받고 있는 호랑이도 날아가기를 할 수 있다.
이것은 문제가 있다. 우리는 날아가기가 가능한 동물에만 fly 메소드를 실행할 수 있어야 한다.
(2) Fly 기능을 인터페이스로 만들고 구현
하지만 이 방법 또한 문제가 있다. fly() 함수에 변경이 생길 경우이다.두 번째는 fly() 메소드를 분리하여 인터페이스로 만든 후 Eagle에서 구현하는 방법이다. 이제 호랑이는 날 수 없고 독수리만 날 수 있다. 비둘기(Pigeon)이 추가되어도 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())를 캡슐화 해보겠습니다.
날 수 없다(FlyNoWay)와 날개로 날아간다(FlyWithWings)를 구현했습니다.먼저 Fly 인터페이스를 생성한 후 날아가기 종류별로 인터페이스를 구현했습니다.
새 울음(BirdCry), 울지 않는다(CryNoWay), 호랑이 울음(TigerCry)로 구현했습니다.Cry 역시 인터페이스를 생성한 뒤 3가지 우는 방식으로 구현했습니다.
이것도 역시 새로운 울음 방식이 필요하면 클래스를 생성한 뒤 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. 소스 코드 확인하기
소스코드는 첨부파일로 다운받을 수도 있습니다.
Animal.java
public abstract class Animal { //Animall 상속 protected Fly fly; // Fly 인터페이스 선언 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() 함수 추상화 public Fly getFly() { return fly; } public void setFly(Fly fly) { this.fly = fly; } public Cry getCry() { return cry; } public void setCry(Cry cry) { this.cry = cry; } } |
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("호랑이"); // 호랑이 출력 } } |
Turtle.java
public class Turtle extends Animal{ // Animal 상속 public Turtle(){ // Turtle 객체 생성자 cry = new CryNoWay(); // Animal의 cry 변수에 Cry 인터페이스의 CryNoWay를 연결한다. fly = new FlyNoway(); // Animal의 fly 변수에 Fly 인터페이스의 FlyNoWay를 연결한다. } public void display(){ // display 함수 구현 System.out.println("거북이"); // 거북이 출력 } } |
Fly.java (인터페이스)
public interface Fly { public void fly(); } |
FlyNoway.java
public class FlyNoway implements Fly{ // Fly 인터페이스 구현 public void fly(){ // fly 구현 System.out.println("날지 못한다."); // 날지 못한다 출력 } } |
FlyWithWings.java
public class FlyWithWings implements Fly{ // Fly 인터페이스 구현 public void fly(){ // fly 구현 System.out.println("날개로 날다"); // 날개로 날다 출력 } } |
Cry.java (인터페이스)
public interface Cry { public void cry(); } |
public class BirdCry implements Cry{ // Cry 인터페이스 구현 public void cry(){ // cry 함수 System.out.println("까악"); // 까악 출력 } } |
CryNoWay.java
public class CryNoWay implements Cry{ // Cry 인터페이스 구현 public void cry(){ // cry 함수 System.out.println("울지 못한다."); // 울지 못한다 출력 } } |
TigerCry.java
public class TigerCry implements Cry{ // Cry 인터페이스 구현 public void cry(){ // cry 함수 System.out.println("어흥"); // 어흥 출력 } } |
PlayAnimal.java (main 실행 클래스)
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() 함수 실행 } } |
이제 디자인 패턴의 첫번째 전략 스트래티지 전략(strategy pattern)을 알아보았습니다.
궁금하신 사항이나, 잘못된 사항은 댓글로 남겨주세요
다음은 이어서 옵저버 패턴(observer pattern)을 포스팅하겠습니다.
출처: https://hyeonstorage.tistory.com/146?category=549763 [개발이 하고 싶어요]
'JAVA > Design Patterns' 카테고리의 다른 글
[DesignPattern] 싱글턴 패턴 (0) | 2020.07.06 |
---|---|
[DesignPattern] 디자인 패턴의 분류 (0) | 2020.07.06 |
[DesignPattern] 객체지향 모델링 (0) | 2020.07.06 |
옵저버 패턴(Observer Pattern) (3/3) - Observer 패턴 변경 확장 (0) | 2019.07.30 |
옵저버 패턴(Observer Pattern) (2/3) - Observer 패턴 직접 구현 (0) | 2019.07.30 |
옵저버 패턴(Observer Pattern)(1/3) - java.util.Observable (0) | 2019.07.30 |
디자인 패턴의 분류 - Design Patterns (0) | 2019.07.30 |
디자인 패턴의 종류에 대해 알아보자 (0) | 2019.07.30 |