![디자인 패턴 - <전략 패턴> (Strategy Pattern)](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPxsXq%2FbtrQ0XFSQ4o%2FS4C88p7XQB2KITWF8D8m1K%2Fimg.png)
프로그래밍을 시작한지 일년도 넘지 않았지만 처음과 조금 달라진점이 있다면 처음에는 기능 구현조차 버거우니 하드 코딩을 하고 구현을 중심으로 코딩을 했다.
요즘은 머리로 구조나 시스템을 나름 '생각'은 해본다는것이 달라진 것 같다.
하지만 생각만 한다고 해결책이 나오는 것은 아니다.
조금이라도 업무에 도움이 되기 위해서 오늘은 디자인 패턴중에 전략 패턴을 공부해봤다.
단순히 이론과 방법만 알면 이해가 잘 안되니 책에서 본대로 정리를 해봤다.
강아지 시뮬레이션 게임 회사의 개발자 JW - 슈퍼 클래스 Dog
- JW가 다니는 회사의 게임인 강아지 시뮬레이션 게임은 여러 종의 강아지를 선택해서 플레이 할 수 있습니다.
- JW는 'Dog' 라는 추상 클래스를 만들고, 여러 유형의 강아지를 표현할 수 있는 클래스를 만들어서 Dog 클래스를 상속 받습니다.
abstract class Dog
{
public abstract void Bark();
public void Idle()
{
Console.WriteLine("가만히 있음");
}
}
슈퍼 클래스인 Dog 클래스는 모든 강아지들에게 가만히 있는 메소드인 Idle() 메소드가 있고,
강아지마다 짖는 방식이 다르기 때문에 추상 함수로 Bark()를 선언 해주었습니다.
class poodle : Dog
{
public override void Bark()
{
Console.WriteLine("왈왈!");
}
}
class Shiba : Dog
{
public override void Bark()
{
Console.WriteLine("월월!");
}
}
class ToyDog : Dog
{
public override void Bark()
{
// 짖을 수 없음.
}
}
푸들,시바견,강아지 장난감 클래스를 만들고 슈퍼 클래스인 Dog 클래스를 상속 받았습니다.
각 클래스 별로 Bark() 메소드를 오버라이드 해서 짖는 기능을 구축 했습니다.
class MainApp
{
static void Main(string[] args)
{
Dog poodle = new poodle();
Dog shiba = new Shiba();
Dog toydog = new ToyDog();
poodle.Bark();
shiba.Bark();
toydog.Bark();
poodle.Idle();
shiba.Idle();
toydog.Idle();
}
}
ToyDog는 짖지 못하기 때문에 오버라이드한 Bark 메소드에 아무런 구현도 하지 않았습니다.
찝찝하긴 하지만.. 나머지는 생각한 대로 정말 잘 나옵니다!
갑작스러운 '날기' 기능 구현
- 경영진은 다른 게임과의 차별화를 위해 강아지들이 날았으면 좋겠다는 의견을 냅니다.
- 객체지향 개발자 JW는 이 갑작스러운 기능 구현 요청을 Dog 클래스에 Fly() 메소드를 넣어서 한번에 해결 해버립니다.
abstract class Dog
{
public abstract void Bark();
public void Idle()
{
Console.WriteLine("가만히 있음");
}
public void Fly()
{
Console.WriteLine("Fly~!");
}
}
상속의 문제
- 멋있게 모든 강아지들의 날기 기능을 한번에 구현했지만, 문제가 생깁니다.
- ToyDog는 짖지 못하지만, Bark() 메소드를 오버라이드해서 아무 기능도 할 수 없게 만들어서 문제가 없었습니다.
- 하지만 그냥 Dog 클래스에서 Fly() 메소를 구현 해버리니 장난감 기능만 해야하는 ToyDog가 날아가버리는 문제가 생겼습니다.
class MainApp
{
static void Main(string[] args)
{
Dog poodle = new poodle();
Dog shiba = new Shiba();
Dog toydog = new ToyDog();
poodle.Bark();
shiba.Bark();
toydog.Bark();
poodle.Fly();
shiba.Fly();
toydog.Fly();
}
}
무조건 오버라이드가 정답일까?
개발을 하다보면 유지/보수/확장을 신경 써서 해야하는 경우가 많은데,
이 강아지 시뮬레이션 게임에서 앞으로 짖지 못하고 날지 못하는 강아지가 나올 확률도 있고, 이것은 예측이 불가능 하다.
그렇다고 나올때마다 새로운 강아지의 클래스에 Dog클래스를 상속 받고, 하나하나 다 오버라이드를 해서 행동들을 구현 한다면 여러가지 문제점들이 생긴다.
1. 서브 클래스에서 코드가 중복된다. (ex.짖는 방식이 같을 경우)
2. 실행 시에 특징을 바꾸기 힘들다.(클래스 마다 오버라이드를 해서 구현을 다 해두었기 때문에)
3. 모든 강아지의 행동을 알기 힘들다.
4. 코드를 변경했을 때 다른 강아지들에게 원치 않은 영향을 끼칠 수 있다.(위의 fly()메소드와 같은 경우)
지금은 3가지의 강아지 종류밖에 없지만, 덩치가 더 커진다면 더 심해진다.
인터페이스 설계하기
- JW는 상속만이 답이 아니라는 생각이 들어 인터페이스를 만들기로 했습니다.
- Dog클래스에 bark,fly 메소드를 지우고 Barkable, Flyable 인터페이스를 만들어서 짖거나 날 수 있는 강아지들에게만 상속해서 기능을 구현하기로 합니다.
public interface Barkable
{
public void Bark();
}
public interface Flyable
{
public void Fly();
}
class poodle : Dog, Barkable,Flyable
{
public void Bark()
{
Console.WriteLine("왈왈!");
}
public void Fly()
{
Console.WriteLine("푸들 난다요~");
}
}
class Shiba : Dog, Barkable, Flyable
{
public void Bark()
{
Console.WriteLine("월월!");
}
public void Fly()
{
Console.WriteLine("시바 난다요~");
}
}
class ToyDog : Dog
{
}
Barkable,Flyable 인터페이스를 만들고 ToyDog를 제외한 나머지 두 강아지의 클래스에 상속받아서 기능을 구현했습니다.
이번엔 정말 완벽합니다.
class MainApp
{
static void Main(string[] args)
{
poodle poodle = new poodle();
Shiba shiba = new Shiba();
ToyDog toydog = new ToyDog();
poodle.Bark();
shiba.Bark();
//toydog.Bark();
poodle.Fly();
shiba.Fly();
//toydog.Fly();
}
}
하지만 직접 사용하려고 해보니 불편한점이 있습니다.
1. 상위 클래스인 Dog에 두가지 인터페이스를 상속 받지 않으면, Dog 클래스를 이용해서 인스턴스를 생성했을시 Bark()와 Fly()메소드를 사용하지 못한다.
-> poodle, shiba 등 하위 클래스를 이용해서 생성 해야함.
2. 그렇다고 상위 클래스인 Dog에 두가지 인터페이스를 상속 받으면, Dog클래스에 구현한 메소드가 호출 된다.
-> 하위 클래스에서의 구현이 의미가 없어짐.
3. 1번의 불편함을 감수한다고 하더라도, 새로운 강아지가 나올때마다 상속받고 기능을 구현해주어야함.
디자인 원칙 1
애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
지금까지 설계한 코드 디자인은 한가지 행동을 바꿀때마다 서브 클래스의 코드를 전부 바꿔주어야 합니다.
이 과정에서 버그가 생길 가능성도 있습니다.
즉, 코드에 새로운 요구 사항이 있을 때마다 바뀌는 부분이 있다면 분리해야 합니다.
그렇다면 우리는 바뀌는 부분만 따로 뽑아서 '캡슐화' 해야합니다.
바뀌는 부분 분리하기 - BarkBehavior, FlyBehavior 인터페이스 만들기
public interface BarkBehavior
{
public void Bark();
}
public interface FlyBehavior
{
public void Fly(string name);
}
우리가 분리해야 하는 기능은 Bark() 와 Fly() 입니다.
새로 만드는 강아지들마다 다를 수도 있고, 같을 수도 있습니다.
Bark 행동을 정의하는 BarkBehavior 인터페이스를 만들고 Bark() 메소드를 선언 해주었습니다.
Fly 행동을 정의하는 FlyBehavior 도 만들었고, Fly 메소드를 선언 해주었습니다.
class Bark1 : BarkBehavior
{
public void Bark()
{
Console.WriteLine("왈왈!");
}
}
class Bark2 : BarkBehavior
{
public void Bark()
{
Console.WriteLine("월월!");
}
}
class NoBark : BarkBehavior
{
public void Bark()
{
Console.WriteLine("짖을 수 없어!");
}
}
Bark1 ,Bark2 ,NoBark 클래스를 선언하고 BarkBehavior 인터페이스를 상속 받았습니다.
클래스에 맞게 기능을 구현 해주었습니다.
class Flyable : FlyBehavior
{
public void Fly(string name)
{
Console.WriteLine(name + " 난다요~!");
}
}
class noFly : FlyBehavior
{
public void Fly(string name)
{
Console.WriteLine(name + "은(는) 날수없어~!");
}
}
Flyable, noFly 클래스를 선언하고 FlyBehavior 인터페이스를 상속 받았습니다.
클래스 별로 기능을 구현해주었습니다.
이제 우리는 오버라이드해서 하나하나 구현을 해 줄 필요 없이, 이 클래스들을 가져다가 쓰기만 하면 됩니다.
abstract class Dog
{
public string name;
protected FlyBehavior flyBehavior;
protected BarkBehavior barkBehavior;
public void PerformFly(string name)
{
flyBehavior.Fly(name);
}
public void PerformBark()
{
barkBehavior.Bark();
}
public void Idle()
{
Console.WriteLine("가만히 있음");
}
}
슈퍼 클래스인 Dog 클래스에 앞에 만들어둔 두가지 인터페이스의 변수를 선언 해주었습니다.
PerformFly 와 PerformBark 메소드를 정의하고 선언해둔 두가지 인터페이스의 메소드를 호출하게 했습니다.
슈퍼 클래스에서 따로 다른 구현을 할 필요 없이, 우리는 나머지 작업은 서브 클래스에 맡기면 됩니다.
class poodle : Dog
{
public poodle(string name, BarkBehavior bark, FlyBehavior fly)
{
this.name = name;
this.barkBehavior = bark;
this.flyBehavior = fly;
}
}
class Shiba : Dog
{
public Shiba(string name,BarkBehavior bark,FlyBehavior fly)
{
this.name = name;
this.barkBehavior = bark;
this.flyBehavior = fly;
}
}
class ToyDog : Dog
{
public ToyDog(string name, BarkBehavior bark, FlyBehavior fly)
{
this.name = name;
this.barkBehavior = bark;
this.flyBehavior = fly;
}
}
서브 클래스의 생성자에서 BarkBehavior와 FlyBehavior를 결정하게 했습니다.
생성을 시키고 Perform - 함수를 실행시키면 우리가 미리 구현해둔 클래스의 기능을 이용할 수 있습니다.
class MainApp
{
static void Main(string[] args)
{
poodle poodle = new poodle("푸들", new Bark1(), new Flyable());
Shiba shiba = new Shiba("시바", new Bark2(), new Flyable());
ToyDog toydog = new ToyDog("장난감 강아지", new NoBark(), new noFly());
poodle.PerformBark();
shiba.PerformBark();
toydog.PerformBark();
Console.WriteLine();
poodle.PerformFly(poodle.name);
shiba.PerformFly(shiba.name);
toydog.PerformFly(toydog.name);
}
}
상위 클래스의 Perform - 함수만 사용하더라도 구현한 메소드를 호출 할 수 있습니다.
디자인 원칙2
구현보다는 인터페이스에 맞춰서 프로그래밍 한다.
Dog의 행동은 특정 행동 인터페이스로 구현한 별도의 클래스 안에 들어 있습니다.
-> Dog 클래스에서는 그 행동들을 구체적으로 구현할 필요가 없습니다.
전에 오버라이드를 통해 구현하는 방법은 특정 구현에 의존했기 때문에 다른 코드를 추가하지 않는 이상 행동을 변경 할 여지가 없었지만, 인터페이스를 상속받은 행동을 구현하는 클래스를 구현 해놓았기 때문에 어떤 클래스에도 가져다가 쓸 수 있게 되었습니다.
인터페이스에 맞춰서 프로그래밍 한다 = 상위 형식에 맞춰서 프로그래밍 한다.
이 말은 Java나 C#에서 제공하는 Interface를 쓰라는 것이 아니라, 실제 실행시에 쓰이는 객체가 코드에 고정되지 않도록 상위 형식에 맞춰 프로그래밍해서 다형성을 활용해라 라는 뜻입니다.
위에만 보더라도 상위 형식인 FlyBehavior & BarkBehavior 인터페이스를 상속받은 클래스들을 만들어 기능을 구현해놓고,
Dog라는 슈퍼 클래스에서는 단지 FlyBehavior ,BarkBehavior형 변수만 추가 하고 메소드만 호출을 해주었습니다.
이것은 Dog클래스에서는 실제 객체의 형식을 몰라도 Bark(), Fly()라는 메소드만 호출해주면 된다는 뜻입니다.
로켓으로 나는 기능 추가해보기
- 경영진은 또 다른 게임과의 차별화를 위해 강아지가 로켓으로 나는 기능을 구현하라고 지시합니다.
- 우리는 로켓으로 나는 기능을 Dog 클래스를 상속받은 서브클래스에서 하나하나 기능을 구현 할 필요 없이, 단순히 로켓으로 나는 기능을 담당 할 클래스를 만들고 FlyBehavior 인터페이스를 상속받으면 됩니다.
class RocketFly : FlyBehavior
{
public void Fly(string name)
{
Console.WriteLine(name + " 로켓 플라이!");
}
}
class MainApp
{
static void Main(string[] args)
{
poodle poodle = new poodle("푸들", new Bark1(), new RocketFly());
poodle.PerformBark();
}
}
간단하게 기능 구현에 성공했습니다.
동적으로 기능 수정하기
런타임 중에도 강아지들의 기능을 자유자재로 수정할 수 있다면 정말 멋진 게임이 될 것 같습니다.
그래서 동적으로 수정하는 기능을 만들어 보기로 합니다.
abstract class Dog
{
public string name;
protected FlyBehavior flyBehavior;
protected BarkBehavior barkBehavior;
public void SetBark(BarkBehavior bark)
{
barkBehavior = bark;
}
public void SetFly(FlyBehavior fly)
{
this.flyBehavior = fly;
}
//밑에 생략
}
슈퍼 클래스인 Dog 클래스에서 SetBark, SetFly 함수를 통해 BarkBehavior와 FlyBehavior를 바꾸는 메소드를 작성했습니다.
class MainApp
{
static void Main(string[] args)
{
poodle poodle = new poodle("푸들", new Bark1(), new Flyable());
poodle.PerformFly(poodle.name);
poodle.SetFly(new noFly());
poodle.PerformFly(poodle.name);
poodle.SetFly(new RocketFly());
poodle.PerformFly(poodle.name);
}
}
이제 이렇게 Set - 함수를 통해 바꿔주기만 하면 끝입니다!
class MainApp
{
static void Main(string[] args)
{
poodle poodle = new poodle("푸들", new Bark1(), new Flyable());
poodle.PerformBark();
poodle.PerformFly(poodle.name);
Console.WriteLine();
poodle.SetFly(new noFly());
poodle.SetBark(new Bark2());
poodle.PerformBark();
poodle.PerformFly(poodle.name);
Console.WriteLine();
poodle.SetFly(new RocketFly());
poodle.SetBark(new NoBark());
poodle.PerformBark();
poodle.PerformFly(poodle.name);
}
}
실험을 위해 Set - 함수를 통해 bark와 fly를 바꿔봅니다.
동적으로 기능이 바뀌었습니다.
디자인 원칙3
상속보다는 구성을 활용한다.
각 Dog 클래스에는 BakrBehavior와 FlyBehavior가 있고, 각각 짖는 행동과 나는 행동을 위임 받습니다.
이런식으로 두가지 클래스를 합치는것을 '구성'을 이용한다 라고 합니다.
구성을 활용해서 시스템을 만들면 유연성을 크게 향상 시킬 수 있습니다.
전략 패턴
알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해줍니다.
전략패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있습니다.
디자인 패턴은 옛날에 디자인 패턴이라는 것이 정의되지 않았을 때 많은 프로그래머들이 공통적으로 사용했던 좋은 패턴들을 모아서 만든것이라고 하는데,
이 처럼 우리는 이미 디자인 패턴을 쓰고 있을지도 모른다.
물론 나는 이 전략패턴이라는 것을 생각조차 못해봤지만, 이미 내가 적용하고 있는 패턴이라고 하더라도 그 기능과 정확한 이름을 알고 있다면 협업을 할 때 참 좋을 것 같다는 생각을 했다.
그리고 실제로 지금 이 예제와 비슷하게 슈퍼 클래스 하나를 두고 서브 클래스의 기능 구현을 하나하나 해야하는 것이 있어서 고민을 많이 했는데, 이 전략패턴이 나에게 큰 도움을 줄 것 같다.
한빛미디어의 <헤드퍼스트 디자인패턴>을 읽고 정리한 글이니 관심이 있으신 분들은 직접 책을 구매하셔서 공부해보셔도 정말 좋을 것 같다.
'프로그래밍 공부 > 디자인 패턴' 카테고리의 다른 글
싱글톤 패턴 vs 정적 클래스 (Singleton Pattern vs Static Class) (0) | 2024.02.29 |
---|---|
디자인 패턴 - <데코레이터 패턴> (Decorator Pattern) (0) | 2022.11.21 |
디자인 패턴 - <옵저버 패턴> (Observer Pattern) (0) | 2022.11.20 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!