프로그래밍 Design Pattern 이해하기 - 6 커맨드 패턴


Android/번역본 2014.08.25 13:54



커맨드 패턴 (캡슐화 호출)


이번 포스팅에서는 우리는 캡슐화를 호출하는 방식에 대해 배워본다. 


요구사항을 받았다! 


"새로이 확장된 날씨 시스템에 대해 만들고 싶다. 우리는 Home Automation Remote Control을 구현하고 싶다. 그 중 리모컨을 만들고 싶다. 리모컨에는 일곱가지 기능이 들어있고 각각 온오프가 가능해야 한다. 그리고 각각의 기능은 다른 기기에 연결이 되어야 한다. 조명, 팬, 욕조, 오디오 등 각종 홈오토메이션 장비를 제어하기 위해 자바 API 클래스를 제공해주겠다. 그리고 리모컨 프로그래밍을 위한 API도 같이 만들어 달라"


 라는 요구사항을 받게 되었다. 





제공해준 vendor classes들을 보았다!




vendor class들을 둘러보니, "on()", "off()" 메서드들이 모두 공통적으로 존재한다. dim(), setTemperature() 등도 공통적으로 사용될 수 있는 메서드들이다. 뿐만 아니라, 나중에 새로운 장비들이 추가 될 것으로 고려해야 한다. 


 '문제를 분리'하는 것에 초점을 맞춰야 한다. 리모컨의 버튼이 눌렸을 때 어떻게 요청해야 할지 알아야 한다. 그러나 홈오토메이션이 어떤 기기를 켜야 하는지 정확히 아는 것은 옳지 않다. (확장하기 위해서 홈오토메이션 자체가 각각의 기기를 다 알고 있을 필요는 없다!)


 리모컨이 generic 하게 요청을 하게 만들려면 어떻게 해야 할까? 


 아직 확실하지 않다. 하지만 리모트가 vendor 클래스들의 자세한 상황을 가지고 있는 것은 옳지 않다. 

예를 들어, "if (slot1 == Light) light.on(), else if slot1 = Hottub, hottub.jetsOn()" 이렇게 말이다. 좋지않은 디자인이다. 

새로운 기기가 추가 될 때마다 우리는 코드를 수정해야 하고 폭발적으로 버그가 생기고 우리의 일은 더 많아질 것이다!


> 문제점의 정리

- on(), off() 뿐 아니라 dim(), setTemperature()등의 공통된 메서드 존재

- 향후 새로운 기기가 추가 되면 다른 이름을 가진 메서드가 추가 될 가능성 존재

- 리모컨 API입장에서 light.on(), hottub.jetsOn()을 호출하지 말고 어느 정도 추상화 시킬 필요가 있음 



이런 경우 커맨드 패턴을 사용하게 되면, 실제로 작업을 수행하는 객체에서 작업의 요청을 분리할 수 있다. 그래서 요청을 하는 객체가 리모트 컨트롤이자 vendor class들 중 하나의 인스턴스의 액션을 실행하는 객체가 된다!


 어떻게 그것이 가능할까? 어떻게 분리를 시킬 것인가? 


"command objects"를 사용하면 가능하다. 커맨드 객체는 할 일(요청사항)을 캡슐화 할 수 있다. 그래서 만약 우리가 각각의 버튼에 대한 입력이 들어왔을 때 우리는 커맨드 오브젝트에게 "어떤 일" 해야 하는 지 물어보게 된다. 리모트객체는 어떤 일을 해야 하는지 몰라도 되며, 커맨드 객체만이 어떤 일을 해야 할지 알고 있게 된다. 그렇게 되면 리모트에서 기기 클래스들을 분리할 수 있게 된다. 


 자 그러면 어떻게 구현해야 하는지 생각해보자. 저녁 식사를 하는 과정은 커맨드 패턴을 이해하는데 도움이 된다. 


1. 당신은 식당에 들어온 고객이다. 웨이터에게 주문을 하게 된다. 

2. 웨이터는 주문을 받고, 카운터에 가서 "주문이요~!" 라고 소리친다. 

3. 요리사는 주무넹 맞게 음식을 만들고 음식이 나온다.


자 이것을 객체와 메서드로 분리해보자. 


1. Customer는 createOrder()를 호출한다. 

2. 웨이트리스는 takeOrde()r를 호출하게 되고, orderUp()을 호출한다.

3. 요리사는 makeBurger(), makeShake()를 하게 되고 Output을 내보낸다. 


웨이트리스는 일단 각각의 음식이 어떻게 만들어지는 지 알필요가 없다. 웨이트리스는 고객에게 받은 주문을 카운터에게 그로 전달하기만 하면 된다. 실제로 음식을 만드는 객체는 요리사고 orderUp()메서드를 이용해 어떤 음식을 만들지 알게 된다. 여기서 중요한 점은 웨이트리스와 요리사는 완전히 분리된다는 사실이다. 


 이 것과 같이 Remote Control API 는 버튼을 눌렀을 때 해당 객체를 가져오는 것과, vendor class에게 요청으 전달하는 것을 분리시킬 수 있다. 아무리 많은  메뉴들이 생겨나도 우리는 오로지 "OrderUp()"만 호출하면 된다. 실제로 리모트는 조명이 켜지는지 등의 상세한 사항을 알 필요가 없다. 



실제로 만들어보자!


Command Interface를 Impelement한다. 





커맨드를 받아 조명을 켠다. 


vendor 클래스를 객체로 들고 있으며, on() off() 를 제어한다. 



커맨드 객체를 사용한다. 



자 이제 Remote Client를 만들어보자!





커맨드 패턴은 앞서 말했듯이, 

"요구사항(Request)"를 캡슐화 할 수 있으며 매개변수를 써서 다른 요구사항을 넣을 수도 있다. 그리고 중간에서 Request를 전달해주기 때문에 요청 내역을 저장하거나 로그로 기록할 수 있으며, 작업취소 기능도 만들 수 있다. 


정리하면,

- 일련의 행동을 특정 리시버하고 연결을 통해 캡슐화 한다. 

- execute()라는 메소드 하나만 외부에 공개된다. 




여기서 Receiver는 Command가 되며, Concreate Command(Concreate는 이전 챕터에서 이야기를 했다. 상속받아 만들어진 세부 클래스)는 Light이다. 


자 그런데 Light가 거실에 있는 불빛인지, 혹은 주방에 있는 불빛인지 나뉘어지는 경우에는 어떻게 해야할까? 


머릿속에 떠올랐듯이 Light 클래스를 서브클래싱해서 livingRoomLight 와 kitchenLight 로 나누면 된다.






위와 같이 각각은 Concreate class의 execute를 호출하게 된다. 

그리고 UnDo기능을 위해 Invoker라는 것을 사용해보자. 








RemoteControl은 on과 off에 대한 Command들을 배열로 관리하며, on off 버튼 클릭 시 해당 slot의 콘크리트 커맨드 클래스에서 execute를 호출하게 된다. 하나를 예를 들면, StereoOnWithCdCommand 라는 클래스의 execute에서는 위와 같이 호출을 하면 된다. 







이렇게 하게 되면, 외부 기기업체에 Command라는 객체만 던져주고 이를 상속받아 각각에 맞는 코드를 execute()에서 만들어주면 된다!


우리가 아직 추가하지 않은 기능이 있다. "Undo()" (작업취소) 기능


이 역시 전체 Command Conreate class에서 사용되므로 




전등을 켜는 인터페이스 





* 근데 굳이 이렇게 만들어야 하나? 어차피 on / off라면 토글을 이용하면 쉽지 않나?





리모컨에서 Unde를 사용하기 위해서는 

아래 메서드만 추가한다. 





이렇게 하면 파티모드같은 것이 요청이 들어왔을 때 한결 편하다!(매크로 기능)





파티모드 On을 누르게 되면, 전등이 어두워지면서 오디오, TV가 켜지고, DVD 모드로 변경되며 욕조에 물이찬다!


이런 커맨드 패턴의 장점으로는 Job Queue가 여러개 일 때 execute()를 통해 실행되는 것은 Thread가 각각 생겨서 처리가 되기 때문에 구조상 유지 보수가 쉬우며, 확장성이 뛰어나다. 






저작자 표시 비영리 변경 금지
신고

WRITTEN BY
ShakeJ

0 ,