[Design Pattern] 4. Behavioral Pattern

Intro

Algorithm과 object 간의 책임 분배에 관한 pattern입니다.

즉, object의 사용 목적에 따라서 method를 정의할 때, 많이 사용되는 구현 pattern을 의미합니다.

Behavioral Pattern

1. Chain of Responsibility(CoR)

cor

request를 여러 handler들을 하나의 chain으로 연결한 object에 전달하여 request를 처리하는 방식입니다. 여기서 각 각의 handler는 스스로 작업을 끝내고 response를 보낼 수도 있고, 이를 다음 handler로 전달할 수도 있으며, 해당 request를 이용해서 side effect를 만들 수도 있다.

이는 대게 request를 처리하는 module을 설계하는 과정에서 많이 사용됩니다. 예를 들어, REST API 를 구현하고자 할 때, 여러 request에서 공통적으로 사용되는 logic을 별도의 middleware라는 것으로 분리하여 구현하고 재활용하는 것을 많이 볼 수 있습니다. (ex. nodeJS express, Go http.Handler, etc...)


여기서 CoR의 특징을 살펴보고 갑시다.

  • Request의 처리 순서도 제어할 수 있습니다.
  • 각 handler가 하나의 역할만 하도록 하여, 유연성과 가독성을 높일 수 있습니다.(Single Responsibility)
  • 새로운 handler의 추가가 기존 code의 영향을 주지 않습니다. (Open/Close)

2. Command

command

모든 request를 하나의 queue에 저장하고, 처리자는 queue의 순서에 따라서, request를 처리하는 pattern입니다. 이러한 방식은 request를 마치 하나의 method paratemer로 받아들이도록 하고, request의 실행을 queue로 관리함으로써, 쉽게 되돌리기 기능도 지원하도록 할 수 있습니다.

쉽게 예를 들면, 식당에서 웨이터는 주문을 받아서, 영수증을 순서에 따라서 order board에 붙이면, 주방장은 이를 보고, 순서에 따라서 요리를 내보내는 형식이라고 보면 되겠습니다.

구현 시에는 Command라는 Interface를 구현하는 각각의 Command class를 작성합니다. 여기서 각 Command는 생성 시에 receiver를 전달받아서, 호출 시에 이를 Receiver에게 전달할 수 있도록 합니다. 그리고, Command Interface를 호출하는 Invoker를 선언해줍니다.


Command의 특징은 다음과 같습니다.

  • Command 단위로 class를 구분할 수 있기 때문에 유연성이 높아집니다. (Single Responsibility)
  • 새로운 Command의 추가가 기존 code에 영향을 주지 않습니다. (Open/Close)
  • 되돌리기와 다시 재생 등의 동작의 구현이 쉽습니다.

3. Iterator

iterator 내부의 구현물을 들어내지 않은 상태에서 구성요소를 순환하기 위해 고안된 pattern입니다.

기본적으로는 iterator는 다음과 같은 요소로 이루어집니다.

  1. 현재 자신의 구성요소를 retuern하는 method
  2. 다음 iterator를 반환하는 method
  3. 다음 iterator가 존재하는지를 체크하는 method

해당 object를 통해서 전체 구조를 순환할 수 있도록 하는 방식입니다. 주요 예시는 file 입출력을 예를 들 수 있습니다. 대게 while 문을 통해서 더 이상 읽을 문자가 없을 때까지 line 단위로 받아오며, next를 호출하는 식의 구현을 많이 보았을 것입니다. 이를 사용하는 이유는 두 가지로 들 수 있습니다.

  1. object 내의 자세한 구현을 감추기 위해서
  2. 필요에 따라 여러 iterator를 생성하기 위해서

대게 object가 하나 이상의 동일 object를 포함하게 된다면, 이 object를 순환할 수 있는 방법은 여러 가지가 존재하게 됩니다. 예를 들어 tree를 구현했다고 했을 때, 기본적으로 depth first search, breadth first search을 생각할 수 있습니다. 하지만, 상황에 따라서 효율적인 방식이 다르기 때문에, 각기 다른 순환 방식을 지원하는 것이 좋습니다.


iterator의 특징을 살펴보고 갑시다.

  • Iterator 각 각에 필요로 하는 algorithm을 구현할 수 있기 때문에 유연한 구조를 가질 수 있습니다. (Single Responsibility)
  • 새로운 iterator의 추가가 기존 code에 영향을 미치지 않습니다. (Open/Close)
  • object로부터 생성된 각 iterator는 서로 독립적으로 동작할 수 있습니다.
  • 하지만, 해당 구현은 다루고자 하는 데이터의 양이 적은 경우 지나칠 수도 있고, 직접 접근하는 것보다 속도가 느릴 수 밖에 없습니다.

4. Mediator

mediator

object 간의 혼란스러운 의존성을 줄이기 위해서 고안된 pattern으로, object 간의 직접적인 사용을 제한하고, mediator라는 중계자를 통해서만 동작할 수 있도록 하는 pattern입니다.


여기서 adapter의 특징을 살펴보고 갑시다.

  • 디양한 object 간의 communication을 추출할 수 있기 때문에, object 본연의 작업에 집중하여 편리하고 유지하기 쉽게 만듭니다. (Single Responsibility)
  • 새로운 mediator를 추가할 때, 기존 code의 변경이 필요 없습니다. (Open/Close)
  • 각 Object 간의 의존성을 제거할 수 있습니다.
  • 개발이 진행될수록 mediator가 전체 시스템을 관리하는 a God Object가 되고, mediator를 사용하는 모든 object가 이에 의존성이 생기게 됩니다.

5. Memento

memento

object의 상태 변경에 이전 상태가 큰 영향을 미치거나 history에 대한 구현이 필요한 경우 구현할 수 있습니다. object의 구체적인 구현에 대한 내용을 제외하고, 이전 상태를 저장하고, 필요에 따라 이를 다시 불러와서 사용하는 pattern입니다.

구현을 하기 위해서는, 본래의 object를 그대로 두고, 필요로 하는 private variables를 포함하는 memento를 구현하여 state를 받을 수 있는 method를 포함하게 해서, 이 memento들만 caretaker라는 object에서 list형태의 history로 저장할 수 있도록 합니다.


memento의 특징은 다음과 같습니다.

  • 기존 object의 encapsulation을 유지하면서, 기능을 구현할 수 있습니다.
  • 기존 code를 그대로 유지한 채로 caretaker를 통해서, history logic을 작성할 수 있습니다.
  • 그러나, memento를 유지하기 위한 추가적인 공간이 필요하며, 오래된 데이터 삭제를 위한 원본을 향한 추적이 필요로 됩니다.

6. Observer

observer

가장 많이 쓰이면서, 중요한 pattern 중에 하나라고 생각합니다. subscription 로직을 정의하고, subscription을 수행한 모든 object에게 특정 event의 발생을 전달하는 방식입니다.

즉, object에서 특정 event가 발생하면, 이를 계속해서 broadcasting 하는 방식입니다. 따라서, 이를 구독하고 있는 각 object가 이에 따른 처리를 수행하는 방식입니다.

구현을 하기 위해서는,

Command Pattern과 굉장히 유사하다고할 수 있습니다. Command Pattern은 Queue에 Command를 차곡차곡 쌓아두고, 이를 사용하기를 원하는 Object가 이를 찾아가는 방식이라면, Observer Pattern은 저장하기보다는 이를 필요로 하는 Object에게 전달하는 방식입니다.


여기서 adapter의 특징을 살펴보고 갑시다.

  • 새로운 Subscriber, Publisher가 기존 code에 영향을 미치지 않습니다. (Open/Close)
  • 실행 중에 object간의 관계를 생성하는 것이 가능합니다.
  • 그러나, Subscriber 간의 순서를 정의하거나 각 event의 순서를 엄밀히 구현하는 것은 별도의 구현체를 필요로 합니다.

7. State

state

object의 내부 상태가 변화할 때마다 동작을 바꾸도록 하는 pattern입니다. 마치 object가 이것의 class를 바꾸는 것과 같은 효과를 볼 수 있습니다.

가장 일반적으로 볼 수 있는 예시가 게시글 작성이다. 엄격한 절차를 따르는 글 작성에는 다음 세 가지의 과정을 거치게 됩니다.

  1. 제출 전
  2. 제출 완료 (검토 중)
  3. 배포 (업로드 완료)

각 단계마다 사용할 수 있는 method와 각 method의 동작이 달라질 수 있습니다. 이를 별도의 class로 나누지 않고, 하나의 class로 만들면서 state를 포함하도록 함으로써, 이를 내부에서 control 할 수 있도록 하는 pattern입니다.

구현 시에는 각 State를 별도의 Class로 분리하고, 그 내부에서 변경되는 method를 직접 구현하도록 합니다. 따라서, 실제 state를 포함한 원본 class는 이 state에 정의된 method를 호출하도록 할 수 있습니다.


state의 특징을 살펴보고 갑시다.

  • 별도의 state를 class로 분리하기 때문에, 유연한 구조를 만들 수 있습니다. (Single Responsibility)
  • 새로운 state의 추가가 기존 code에 영향을 주지 않습니다. (Open/Close)

8. Strategy

strategy

동일한 method에 대해서 여러 algorithm을 정의하고, 각각을 별도의 class로 나누어 상호 호환이 가능하도록 하는 pattern입니다.

즉, object의 method 자체를 별도의 interface로 분리하는 방식이라고 이해할 수 있습니다. 앞서 보았던 state는 context(문맥)에 따라서, 상태가 바뀌지만 Strategy Pattern에서는 행위 자체가 바뀐다고 생각하면 됩니다.

대게 게임에서 쉽게 예시를 생각할 수 있습니다. player의 skill을 interface화 시키고, 해당 동작에 따른 damage와 mp 변화 등을 각 skill마다 직접 계산하여 player object로 전달할 수 있다고 생각하면 쉽습니다.


strategy의 특징을 살펴보고 갑시다.

  • 실행 중의 특정 strategy를 선택하여 실행시키는 것이 가능합니다.
  • 각 strategy에 대한 자세한 구현을 감출 수 있습니다.
  • 대게 상속 형태를 대체하여 사용하는 것이 가능합니다.
  • 새로운 strategy의 추가가 기존 code에 영향을 미치지 않습니다. (Open/Close)

9. Template Method

templateMethod

algorithm의 skeleton을 상위 class에 정의하고, 전체적인 구조는 바꾸지 않으면서 각 단계에 대한 구현을 override 하는 pattern입니다.

따라서, 전공과목 과제를 하다 보면 교수님들이 skeleton 코드를 준다고 했을 때, 대게 구조만 있고, 각 함수의 내부가 비어 있는 것을 볼 수 있었던 거 같습니다. 따라서, 해당 class를 inherit 하여 구체적인 구현을 하는 식으로 class를 만들면 됩니다.


template method의 특징은 다음과 같습니다.

  • 구현의 내용을 줄이고, 중복되는 코드의 사용을 줄일 수 있습니다.
  • 그러나, skeleton에 의한 제한으로 불가피하게 code의 변경이 발생할 수 있습니다.

10. Visitor

visitor

특정 object에 접근하려는 object에 따라 별도의 algorithm을 적용하는 pattern입니다.

즉, 사용하고자 하는 object를 하나의 interface로 추상화하고, 각 object는 이를 사용할 client(visitor)를 허용할 것인지 그리고 어떤 algorithm을 수행할 것인지를 정의해둡니다. 사용할 수 있는 예시는 사용할 수 있는 Element의 종류가 매우 다양하며 계속해서 추가될 가능성이 높을 때 사용할 수 있습니다. 그렇지만, Visitor의 추가는 매우 어렵기 때문에 이에 유의해야 합니다.


여기서 adapter의 특징을 살펴보고 갑시다.

  • 새로운 algorithm의 추가가 기존 code의 변경없이 가능합니다. (Open/Close)
  • 동일한 class 내부에서 동일한 동작을 여러 version으로 정의할 수 있어 유연합니다. (Single Responsibility)
  • 그러나, visitor의 추가는 기존 algorithm의 수정을 불러올 수 있습니다.

Reference

Comments