예제 코드로 이해해보는 디자인 패턴 - 커맨드 패턴(Command Pattern)

kindof

·

2023. 11. 29. 14:41

디자인 패턴에 대한 정리글이 꽤 쌓여가고 있는데요.
 
글을 쓸 때마다 각 디자인 패턴을 잘 설명할 수 있는 예제 코드를 고민해보긴 합니다.
 
하지만 디자인 패턴이라는 것 자체가 모든 코드에 100% 적용할 수도 없을 뿐더러, 실제로는 꽤나 디테일하고 복잡한 상황을 대하는 고민의 결과이기에 쉬운 예제로 디자인 패턴이 얼마나 유용한지 전달하기가 쉽지만은 않은 것 같습니다.
 
이번 글에서 소개할 커맨드 패턴(Command Pattern)도 마찬가지입니다.
 
실무에서도 커맨드 패턴을 사용하는 코드 예제가 있지만 꽤나 복잡한데요. 다른 레퍼런스와 영상 등을 보면서 제 나름대로 이해한 바를 소개하겠습니다.
 
 

1. 커맨드 패턴(Command Pattern)이란?

1-1. 커맨드 패턴 디자인

커맨드 패턴(Command pattern)이란 특정한 작업을 수행하는 객체와 그 작업을 요청하는 객체 간의 관계를 느슨하게 결합하는 패턴입니다.
 
조금 구체적으로는, 명령을 객체로 캡슐화해서 만들고 객체를 서로 다른 요청 내역에 따라 매개변수화하는 방식의 패턴인데요.
 
먼저 아래와 같은 그림으로 커맨드 패턴의 구성을 이해해보겠습니다.

https://www.dofactory.com/net/command-design-pattern

 
Command Interface : 모든 커맨드 객체에서 구현해야 하는 메서드(execute, … , etc)를 정의한 인터페이스입니다.

Concrete Command : 특정 액션과 Receiver를 연결해주는 객체입니다. 아래 Invoker 메서드가 커맨드 인터페이스의 메서드를 실행하면 그 구체 객체인 Command Class가 해당 메서드를 수행합니다.

Invoker : execute() 메서드를 통해 Command 구체 클래스에게 특정 작업을 시작하라는 명령을 전달합니다.

Receiver : Command 구체 클래스의 호출을 받아 실제 작업을 수행하는 객체입니다.

Client : 커맨드를 생성하고 Invoker에게 전달하는 객체로, 사용자 요청을 캡슐화합니다.
 
 

1-2. 이러한 디자인을 왜 쓰나?

커맨드 패턴을 사용했을 때 얻을 수 있는 이점에 대해 몇 가지 생각해보겠습니다.

[1] 먼저 SCP, OCP 원칙에 부합하는 코드를 짤 수 있습니다.

각 커맨드는 자신이수행해야 하는 작업을 캡슐화합니다. 이를 통해 각 커맨드 클래스는 특정한 작업에 대한 단일 책임을 자연스럽게 가지게 됩니다.

또한, 커맨드 객체를 중심으로 요청을 하는 객체와 요청을 수행하는 객체가 분리되어 개별 클래스의 확장은 열려있고 변경에는 닫혀있기에 OCP 원칙을 지키기 용이해집니다.

클린 아키텍쳐를 읽었을 때 비슷한 설명이 있었던 것 같아 참조합니다.
 

20장 > 요청 및 응답 모델 설명 중에서,

의존성을 제거하는 일은 매우 중요하다. 요청 및 응답 모델이 독립적이지 않다면, 그 모델에 의존하는 유스케이스도 결국 해당 모델이 수반하는 의존성에 간접적으로 결합되어 버린다.

시간이 지나면 두 객체는 완전히 다른 이유로 변경될 것이고, 따라서 두 객체를 어떤 식으로든 함께 묶는 행위는 공통 폐쇄 원칙과 단일 책임 원칙을 위배하게 된다.

 

[2] 추상화된 것에 의존하고 구체 클래스에 의존하지 않게 됩니다.
 
IoC(제어의 역전) 개념을 커맨드 패턴에서도 볼 수 있는데요. 클라이언트는 직접적으로 구체 클래스의 행동을 통제하는 대신 커맨드를 통해 작업을 캡슐화합니다.

마찬가지로 클린 아키텍쳐에서 읽었던 내용이 이 부분과 맞닿아 있다고 생각해 글 내용을 첨부합니다.
 

클린 아키텍쳐 5장 > 객체 지향 프로그래밍 설명 중에서,

전형적인 호출 트리의 경우 main 함수가 고수준 함수를 호출하고, 고수준 함수는 다시 중간 수준 함수를 호출하며 중간 수준 함수는 다시 저수준 함수를 호출한다. 이러한 호출 트리에서 소스 코드 의존성의 방향은 반드시 제어흐름을 따르게 된다.

 

Clean Architecture

 

하지만 다형성이 끼어들면 무언가 특별한 일이 일어난다.

 

Clean Architecture

 

위 그림에서 HL1 모듈은 ML1 모듈의 F() 함수를 호출한다. 소스 코드에서는 HL1 모듈은 인터페이스를 통해 F() 함수를 호출한다. 이 인터페이스는 런타임에서는 존재하지 않는다. HL1은 단순히 ML1 모듈의 함수 F()를 호출할 뿐이다. 하지만 ML1과 I 인터페이스 사이의 소스 코드 의존성이 제어 흐름과는 반대인 점을 주목하자.

이러한 소스 코드 의존성은 소스 코드 사이에 인터페이스를 추가함으로써 방향을 역전시킬 수 있다.

 

여기까지 설명을 커맨드 패턴에 대입해보면 커맨드 인터페이스와 구체 커맨드, 그리고 클라이언트의 호출부터 실제 액션이 일어나는 흐름에서 제어가 역전됐다는 것을 확인할 수 있겠습니다.
 

커맨드 디자인 패턴에서의 의존성 역전

 
 
 

2. 코드로 이해해보기

예전에 Spring MVC와 FrontController에 김영한님 강의를 보며 정리했던 글이 있는데 커맨드 패턴을 설명하기 좋은 예시 같아서 해당 글을 다시 참조해보려고 합니다.
 
사실 대부분의 설명이 이전에 작성했던 글에 모두 포함되어 있으니, 궁금하신 분들은 위 글을 참고해보셔도 좋을 것 같습니다.
 
위 글에서 커맨드 패턴이 적용된 부분은 아래와 같았습니다.

김영한님의 Spring MVC 강의

1. 클라이언트의 HTTP 요청은 FrontController에서 받습니다.

2. FrontController는 requestServlet에서 파라미터를 파싱하고, 사용자 요청에 맞는 컨트롤러를 호출하면서 paramMap과 model 객체를 넘겨줍니다.

3. 사용자 요청에 해당하는 컨트롤러는 paramMap에서 데이터를 받아서 비즈니스 로직을 수행하고 model 객체에 데이터를 다시 담아서 넘겨줍니다. 이 때, 클라이언트에게 돌려줄 ViewName을 같이 보내줍니다. 이 때, ViewName은 Prefix나 Suffix를 제외한 부분만을 리턴합니다.

4. viewResolver는 컨트롤러가 반환한 ViewName을 정확한 원본 ViewName(MyView)로 만들어줍니다.

5. FrontController는 MyView 객체를 통해 HttpServletRequest에 model에 대한 데이터를 담고 dispatcher를 통해 view를 Forwarding합니다.
 
이 설명은 DispatcherServlet 클래스에서 아래와 같은 코드를 통해 구현됩니다.

DispatcherServlet.java > doService()

 

DispatcherServlet.java > doDispatch()

 
간략히 코드를 보면 DispatcherServlet은 mappedHandler에서 어떤 컨트롤러에서 처리되어야 하는지 결정할 HandlerAdapter를 가져오고, handle() 메서드를 통해 핸들러를 invoke() 합니다. invoker 역할을 하고 있죠.
 
이후에 HandlerAdapter(ha)는 컨트롤러의 메서드를 호출하고 파라미터로 processedRequest를 전달합니다. 이 객체가 위에서 설명한 커맨드 객체라고 볼 수 있습니다.
 
컨트롤러에서 처리되는 요청은 이후에 ModelAndView를 통해 클라이언트에게 응답을 리턴합니다.
 


 
이러한 구조에서 DispatcherServlet은 추상화된 커맨드 객체를 통해 여러 클라이언트의 요청을 처리할 때 다양한 컨트롤러가 동작할 수 있도록 하는 것입니다.
 
 

3. 정리

이번 글에서는 커맨드 패턴 디자인에 대해 살펴봤습니다.
 
여러 디자인 패턴을 소개하다보니 모든 디자인 패턴이 지향하는 바는 결국 OOP라는 것을 느낍니다.
 
커맨드 패턴 역시 추상화 된 커맨드 인터페이스를 두고, 변경에는 닫혀있고 확장에는 열린 구체 커맨드 객체를 통해 클라이언트의 요청을 처리합니다.
 
하지만 모든 코드를 커맨드 패턴으로 구현할 필요는 전혀 없습니다. 의존관계가 선명한 클래스들은 그 자체로 어느정도 결합이 있는 것이 자연스럽다 생각합니다.
 
다만, 어떤 명령들이 일종의 Map, Enum 형태에서 꺼내올 수 있는 구조로 느껴질 때 / 혹은 그 명령들 간의 결합이 느슨할 때 한 번쯤 커맨드 패턴을 고려해보면 좋을 것 같습니다.
 
그래서 커맨드 패턴에 대한 많은 설명이 리모컨 예제, Runnable 예제를 다루는 것 같습니다.
 
 

4. Reference

- 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술, 김영한
- 헤드 퍼스트 디자인 패턴 /14가지 GoF 필살 패턴! 유지 관리가 편리한 객체지향 소프트웨어를 만드는 법, 에릭 프리먼, 엘리자베스 롭슨
- 클린 아키텍처 : 소프트웨어 구조와 설계의 원칙,  로버트 C. 마틴