전체 페이지뷰

2017년 1월 18일 수요일

Design Patterns 목차와 서문, Strategy Pattern

목차

1. 디자인 패턴 소개
2. 옵저버 패턴
3. 데코레이터 패턴
4. 팩토리 패턴
5. 싱글턴 패턴
6. 호출 캡슐화
7. 어댑터 패턴과 퍼사드 패턴
8. 알고리즘 캡슐화
9. 이터레이터와 컴포지트 패턴
10. 스테이트 패턴
11. 프록시 패턴
12. 패턴들로 이루어진 패턴
13. 실전에서의 디자인 패턴
14. 기타 패턴

서문

솔직히 말해서 Head First의 책들을 좋아하지는 않습니다. 언제나 그렇듯이 적어도 저에게는 복잡하고 산만하고, 단편적이라 머리에 들어오지 않거든요. 아마도 익숙하지 않은 탓이 크겠지요. '초인지(metacognition)' 학습법을 이용했다고 하는데 그 학습법을 충분히 설명해 줍니다. 어떤 분들에게는 그 방법이 도움이 될 것입니다만, 저에게는 오히려 혼란스러워서 제 식으로, 그리고 자바가 아닌 C#으로 이해해 보려고 합니다.

1. 디자인 패턴 소개

긴 프로그래밍의 역사 속에서 이미 선배들은 우리의 문제 대부분을 경험했습니다. 그리고, 그 해결책을 만들어 놓았습니다. 그리고 그것을 해결해 놓은 것이 디자인 패턴입니다. 물론 객체 지향의 관점에서 말이죠. 이제 왜 그 패턴이 필요한 것인가에 대해 알아보도록 합니다.

SimUDuck 애플리케이션

오리의 생태를 시뮬레이션한 SimuDuck이라는 오리 애플리케이션이 있습니다. 
처음으로 이 앱을 디자인한 사람은 표준 객체지향적인 방법으로 Duck 수퍼클래스를 만들고, 그 클래스를 확장해서 다른 오리들을 구현하고 있었습니다.


그런데, 이 오리들이 날아가는 기능을 구현하려고 합니다. 그래서 프로그래머는 단순히 Duck 수퍼 클래스에 Fly()메소드를 집어넣었습니다. 그러면 Duck을 상속받은 모든 오리들이 날아다닐 수 있게 될 터였습니다. 그런데 문제가 생겼습니다.

Duck을 상속받은 서브클래스 중 날아다녀서는 안 되는 오리 인형인 RubberDuck과 같은 클래스들도 날아다니게 된 것입니다. 그렇다면 RubberDuck의 Fly() 메소드를 오버라이드 해서 아무것도 하지 않게 하면 될까요? 만약 다른 서브 클래스인 DecoyDuck(가짜 나무 오리)가 추가되면 어떻게 될까요? Fly(), Quack()을 모두 아무 것도 하지 않도록 오버라이드 하면 되는 것일까요?

인터페이스를 사용한다면?

그렇다면 IFlyable, IQuackable... 등의 인터페이스를 만들어 필요한 클래스가 상속받게 한다면 어떨까요? 서브클래스가 늘어나거나 인터페이스가 늘어날때마다 각각의 메소드들을 다 살펴보고 하나하나 작성하는 번거로운 일이 생길 겁니다. 코드의 관리나 재사용 측면에서 전혀 바람직한 상황이 아닙니다.

디자인 원칙

이럴 때 필요한 디자인 원칙이
"어플리케이션에서 달라지는 부분을 찾아내고, 불변인 부분으로부터 분리시킨다"
입니다.

바뀌는 부분을 따로 뽑아 캡슐화 시키면 후에 변화가 있을 때에도 바뀌지 않는 부분에 영향을 주지 않은 채로 고치거나 확장 가능하다는 것입니다.

지금까지 본 바로, Fly()와 Quack()과 같은 오리의 행동은 변하는 부분입니다. 따라서 Duck class로부터 따로 분리해 관리합니다. 

디자인 원칙: 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다.
행동들은 Duck 클래스가 아닌 Interface로 표현하고 행동을 구현할 때 인터페이스를 구현하도록 합니다.


이제 행동들은  Duck 클래스 내부에 숨겨져있지 않고 행동을 나타내는 인터페이스와 그것을 상속받은 개별 행동 클래스로 묶여져 있습니다.

Duck 행동 통합

이제 Duck 클래스를 구현합니다.

직접 Fly(), Quack()을 작성하는 대신 PerformQuack(), PerformFly()가 들어있고, 그에 앞서 
flyBehavior와 quackBehavior라는 인터페이스 형식의 변수가 추가되어 있습니다. 특정 메소드를 직접 작성한 것이 아니라 그 상위인 인터페이스 변수를 연결했다는 점이 중요합니다. 그렇게 함으로써 Duck클래스는 FlyBehavior와 QucakBehavior를 구현하는 클래스들에 대한 참조를 갖게 됩니다. 다형성의 중요한 방법입니다. 여기서는 인터페이스이지만 추상 클래스로도 이런 캐스팅이 가능합니다.(본인 글 참조)

그렇다면 이 PerformQuack()은 구체적으로 어떻게 구현해야 할까요?

 public class Duck
{
    QuackBehavior quackBehavior;
    //기타코드
    public void PerformQuack()
    {
        quackBehavior.Quack();
    }
}
cs

대략 위와 같습니다.
이렇게 만든 코드에서는 객체의 종류에 대해서는 신경쓸 필요가 없습니다. 다만 Quack()이라는 메소드를 실핼시킬 수 있다는 점만이 중요합니다.

이것을 상속받는 클래스는 어떤 식으로 작성되어야 할까요?
public class MallardDuck : Duck
{        
    public MallardDuck()
    {
        quackBehavior = new QuackQuack();
        flyBehavior = new FlyWithWings();
    }
// ...    
}
cs

MallardDuck은 Duck을 상속받고,
생성자에서 quackBehavior와 flyBehavior를 인스턴스의 형태로 받습니다.
이 때, 구체적으로 어떤 메소드를 이용하게 될지가 결정됩니다.

그럼 이제 Duck 클래스를 좀 더 다듬어 보겠습니다.

    // Duck 수퍼클래스
    public abstract class Duck
    {
        protected IFlyable flyBehavior;
        protected IQuackable quackBehavior;
        
        public abstract void Display();
        public void PerformQuack()
        {
            quackBehavior.Quack();
        }
        public void PerformFly()
        {
            flyBehavior.Fly();
        }
        public void Swim()
        {
            Console.WriteLine("현재까지는 가짜 오리도 전부 물에 뜸");
        }
    }
cs

Duck 자체로는 인스턴스를 만들 수 없는 추상클래스로 선언합니다.
내부에 IFlyable, IQuackable의 참조 변수를 선언하고(파생클래스에서 접근가능하도록 protected 선언했습니다), 추상메소드로 Display()를 선언했습니다.
그리고 바뀔 필요가 없는 PerformQuack(), PerformFly(), Swim()은 구체적으로 작성합니다.

이렇게 해서 행동이 바뀌는 경우에 Base 클래스인 Duck까지 손을 대는 불상사를 막을 수 있게 되었습니다.

다음으로는 IFlyable을 상속받는 행동 클래스들을 세부 작성해 봅니다.
    // IFlyable 파생 클래스
    public class FlyWithWings : IFlyable
    {
        public void Fly()
        {
            Console.WriteLine("파닥 파닥");
        }
    }
    public class FlyNoWay : IFlyable
    {
        public void Fly()
        {
            Console.WriteLine("전 못 날아요...");
        }
    }
cs

IQuackable 상속 클래스들도 구현해 봅시다.
    // IQuackable 파생 클래스들
    public class QuackQuack : IQuackable
    {
        public void Quack()
        {
            Console.WriteLine("꽥꽥");
        }
    }
    public class MuteQuack : IQuackable
    {
        public void Quack()
        {
            Console.WriteLine("...");
        }
    }
    public class Squeak : IQuackable
    {
        public void Quack()
        {
            Console.WriteLine("삑");
        }
    }
cs


지금까지의 한 것들을 합쳐서 테스트 해봅니다.
using System;
namespace StrategyPattern
{
    // 두개의 인터페이스 선언
    public interface IQuackable
    {
        void Quack();
    }
    public interface IFlyable
    {
        void Fly();
    }
    // Duck 수퍼클래스
    public abstract class Duck
    {
        protected IFlyable flyBehavior;
        protected IQuackable quackBehavior;
        
        public abstract void Display();
        public void PerformQuack()
        {
            quackBehavior.Quack();
        }
        public void PerformFly()
        {
            flyBehavior.Fly();
        }
        public void Swim()
        {
            Console.WriteLine("현재까지는 가짜 오리도 전부 물에 뜸");
        }
    }
    // IQuackable 파생 클래스들
    public class QuackQuack : IQuackable
    {
        public void Quack()
        {
            Console.WriteLine("꽥꽥");
        }
    }
    public class MuteQuack : IQuackable
    {
        public void Quack()
        {
            Console.WriteLine("...");
        }
    }
    public class Squeak : IQuackable
    {
        public void Quack()
        {
            Console.WriteLine("삑");
        }
    }
    // IFlyable 파생 클래스
    public class FlyWithWings : IFlyable
    {
        public void Fly()
        {
            Console.WriteLine("파닥 파닥");
        }
    }
    public class FlyNoWay : IFlyable
    {
        public void Fly()
        {
            Console.WriteLine("전 못 날아요...");
        }
    }
    // Duck 파생 클래스
    public class MallardDuck : Duck
    {        
        public MallardDuck()
        {
            quackBehavior = new QuackQuack();
            flyBehavior = new FlyWithWings();
        }
        public override void Display()
        {
            Console.WriteLine("저는 물오리입니다.");
        }
    }
    public class RubberDuck :Duck
    {
        public RubberDuck()
        {
            quackBehavior = new Squeak();
            flyBehavior = new FlyNoWay();
        }
        public override void Display()
        {
            Console.WriteLine("저는 고무오리입니다.");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Duck mallard = new MallardDuck();
            mallard.Display();
            mallard.PerformQuack();
            mallard.PerformFly();
            
            Duck rubber = new RubberDuck();
            rubber.Display();
            rubber.PerformQuack();
            rubber.PerformFly();                                   
        }
    }
}
cs

결과)
저는 물오리입니다.
꽥꽥
파닥 파닥
저는 고무오리입니다.

전 못 날아요...

Duck을 상속받은 MallardDuck과 RubberDuck을 작성하고,
그 인스턴스를 생성하여 각각의 메소드들을 사용해 봤습니다.

인스턴스에서 직접 동적으로 행동을 바꾸기 위해서 Duck클래스에 세터메소드 두개를 추가해봅니다.
        public void setFlylBehavior(IFlyable fly)
        {
            flyBehavior = fly;
        }
        public void setQuackBehavior(IQuackable quack)
        {
            quackBehavior = quack;
        }
cs

그리고 Main부분을 이렇게 바꿔주면...
        static void Main(string[] args)
        {
            Duck mallard = new MallardDuck();
            mallard.Display();
            mallard.PerformQuack();
            mallard.PerformFly();
            mallard.setFlylBehavior(new FlyNoWay());
            mallard.PerformFly();
            Console.WriteLine();
            Duck rubber = new RubberDuck();
            rubber.Display();
            rubber.PerformQuack();
            rubber.PerformFly();
            rubber.setQuackBehavior(new QuackQuack());
            rubber.PerformQuack();                                  
        }
cs

결과)
저는 물오리입니다.
꽥꽥
파닥 파닥
전 못 날아요...

저는 고무오리입니다.

전 못 날아요...
꽥꽥

물오리의 Fly와 고무오리의 Quack을 동적으로 바꿀수 있게 됩니다.

이제 모형 오리 서브클래스를 하나더 만듭니다.
    public class ModelDuck :Duck
    {
        public ModelDuck()
        {
            flyBehavior = new FlyNoWay();
            quackBehavior = new QuackQuack();
        }
        public override void Display()
        {
            Console.WriteLine("저는 모형오리입니다.");
        }
    }
cs

그리고,새로운 날기 기능을 넣기 위해 IFlyable 형식의 클래스도 추가합니다.
    public class FlyRocketPowered : IFlyable
    {
        public void Fly()
        {
            Console.WriteLine("로켓 추진!");
        }
    }
cs


Main을 수정해서 새로운 로켓추진 기능을 테스트 해봅니다.
            Duck model = new ModelDuck();
            model.Display();
            model.PerformFly();
            model.setFlylBehavior(new FlyRocketPowered());
            model.PerformFly();
cs

결과)
저는 모형오리입니다.
전 못 날아요...
로켓 추진!


(UML 편집 방법을 잘 모르겠습니다. 처음 써봐서... 틀린 것이 있더라도 보아 넘겨주시길)

나는 행동은 IFlyable 하에 캡슐화 되어 있고,
꽥꽥거리는 행동은 IQuackable 하에 캡슐화 되어 있습니다.
Duck 클래스는 이들을 참조만 하고 있으며,
각각의 서브클래스들이 생성될 때에 행동을 골라 인스턴스를 넘겨주면 됩니다.
이런 식으로 클래스를 합치는 방식을 구성(composition)이라고 합니다.
소위 "A는 B이다"가 아닌 "A에는 B가 있다" 관계입니다.

추후 동작의 구현 내용이 바뀔 때에는 각각의 동작 클래스를 변경하면 되고, 동작을 추가하는 것도 마찬가지입니다. 이렇게 해서 반복되는 코드를 없애고, 수정시의 복잡함을 최소화 하는 방식을 Strategy Pattern이라고 합니다.

댓글 없음:

댓글 쓰기