9. 커맨드 패턴 (Command)
요청 자체를 객체로 캡슐화하여 전달하고 실행하는 커맨드 패턴을 알아봅니다.
한 줄 요약
“해야 할 일” 하나하나를 객체로 만드는 패턴이에요. 일이 객체가 되면 실행을 미루거나, 쌓아 두거나, 되돌리기를 붙이기 쉬워져요.
문제: 하는 법과 되돌리는 법이 따로 노는 코드
간단한 메모장을 만든다고 해 볼게요. 할 수 있는 일은 두 가지예요.
- 쓰기: 끝에 글자를 붙여요.
- 지우기: 끝에서 글자를 몇 개 지워요.
그리고 메모장이라면 되돌리기가 꼭 있어야 하죠. 가장 먼저 떠오르는 방법은 편집기가 할 일을 할 때마다 “무엇을 했는지”를 기록해 두고, 되돌릴 때 그 기록을 보고 거꾸로 하는 거예요.
[편집기]를 설계하자:
'내용'을 [문자열]인 ""으로 정하자
'기록'을 []로 정하여 숨기자
<쓰기>를 만들자 ([문자열]인 '글자'):
'나'의 '내용'을 ('나'의 '내용' + '글자')로 정하자
'나'의 '기록'에 {"종류": "쓰기", "글자": '글자'}를 추가하자
<지우기>를 만들자 ([숫자]인 '개수'):
'길이'를 '나'의 '내용'의 '길이'로 정하자
'지운글자'를 '나'의 '내용'의 <자르기>('길이' - '개수' + 1, '길이')로 정하자
'나'의 '내용'을 '나'의 '내용'의 <자르기>(1, '길이' - '개수')로 정하자
'나'의 '기록'에 {"종류": "지우기", "글자": '지운글자'}를 추가하자
(참고: 되돌리기는 모든 동작을 거꾸로 하는 법을 한곳에서 다 알아야 해요)
<되돌리기>를 만들자 ():
'마지막'을 '나'의 '기록' 뒤에서 꺼낸 값으로 정하자
('마지막'의 "종류")에 따라 나누자:
"쓰기" 인 경우:
'길이'를 '나'의 '내용'의 '길이'로 정하자
'나'의 '내용'을 '나'의 '내용'의 <자르기>(1, '길이' - ('마지막'의 "글자")의 '길이')로 정하자
"지우기" 인 경우:
'나'의 '내용'을 ('나'의 '내용' + ('마지막'의 "글자"))로 정하자
'메모'를 새로운 [편집기]()로 정하자
'메모'의 <쓰기>("안녕")을 실행하자
'메모'의 <쓰기>("하세요")를 실행하자
'메모'의 <지우기>(2)를 실행하자
틀"지금: {'메모'의 '내용'}"을 출력하자
'메모'의 <되돌리기>()를 실행하자
틀"되돌린 뒤: {'메모'의 '내용'}"을 출력하자
'메모'의 <되돌리기>()를 실행하자
틀"한 번 더: {'메모'의 '내용'}"을 출력하자
실행 결과
지금: 안녕하
되돌린 뒤: 안녕하세요
한 번 더: 안녕
되돌리기가 잘 돼요. 그런데 할 수 있는 일이 늘어나면 아파져요.
- 하는 법과 되돌리는 법이 떨어져 있어요. “지우기”를 하는 법은
<지우기>에, 지우기를 되돌리는 법은<되돌리기>의따라 나누자안에 있어요. 한 가지 일의 이야기가 두 군데로 나뉘어 있어요. - 새 동작마다 두 곳을 고쳐야 해요. “붙여넣기”나 “모두 대문자로”를 넣으려면 새 메서드를 만들고,
<되돌리기>에도 경우를 하나 더 넣어야 해요. 되돌리기 쪽을 빠뜨리면 맞는 갈래가 없어서 조용히 아무 일도 안 일어나요. - 기록이 글자 이름표로 되어 있어요.
"종류"와"쓰기"같은 이름을 한 글자만 틀려도 에러 없이 어긋나요. - 편집기가 너무 많은 일을 해요. 글자를 다루는 일, 기록하는 일, 되돌리는 일이 모두
[편집기]한 클래스에 들어 있어요. 기록이 비었을 때 되돌리기를 누르면 어떻게 할지 같은 일도 여기에 붙어요.
해결책: 할 일을 객체로 만들기
커맨드 패턴은 “쓰기”와 “지우기”를 메서드가 아니라 객체로 만들어요.
- 편집기는 글자를 붙이고 지우는 기본 기능만 가져요.
- 명령이라면 지킬 약속, 곧 규정을 만들어요. 명령은
<실행>과<취소>를 할 줄 알아야 해요. - 동작마다 명령 클래스를 하나씩 만들어요. 하는 법과 되돌리는 법이 한 클래스 안에 나란히 있고, 되돌릴 때 필요한 것(지운 글자)은 명령이 직접 기억해요.
- 기록관리자는 명령을 실행하고 쌓아 두기만 해요. 되돌릴 때는 맨 위의 명령을 꺼내
<취소>를 부르면 끝이에요. 그 명령이 무엇인지는 몰라도 돼요.
비유: 식당의 주문서
손님이 주문하면 직원은 주문서에 적어서 주방에 건네요. 주문서 한 장 한 장이 “해야 할 일”이에요. 주문서가 있으니 순서대로 쌓아 둘 수도 있고, 손님이 “아까 그거 취소해 주세요”라고 하면 그 주문서를 찾아 취소할 수도 있어요. 직원은 요리하는 법을 몰라도 돼요. 주문서를 받아서 넘기기만 하면 되니까요. 주문서가 바로 “커맨드”예요.
누가 무엇을 하나요?
| 역할 | 이 예제에서 | 하는 일 |
|---|---|---|
| 명령 약속 | [명령] (규정) |
모든 명령이 할 줄 알아야 하는 일(실행, 취소)을 정해요 |
| 구체적인 명령 | [쓰기명령], [지우기명령] |
하는 법과 되돌리는 법을 함께 알고, 필요한 것을 기억해요 |
| 받는 쪽 | [편집기] |
실제로 글자를 다뤄요. 명령이 이 객체에게 일을 시켜요 |
| 부르는 쪽 | [기록관리자] |
명령을 실행하고 쌓아 두었다가, 되돌릴 때 꺼내요 |
하리로 만들기
같은 메모장을 커맨드 패턴으로 다시 만들어 볼게요. 마지막에는 더 되돌릴 것이 없을 때도 눌러 봤어요.
(참고: 1단계 — 편집기는 글자를 붙이고 지우는 기본 기능만 가져요)
[편집기]를 설계하자:
'내용'을 [문자열]인 ""으로 정하자
<붙이기>를 만들자 ([문자열]인 '글자'):
'나'의 '내용'을 ('나'의 '내용' + '글자')로 정하자
[문자열]을 돌려주는 <끝에서지우기>를 만들자 ([숫자]인 '개수'):
'길이'를 '나'의 '내용'의 '길이'로 정하자
'지운글자'를 '나'의 '내용'의 <자르기>('길이' - '개수' + 1, '길이')로 정하자
'나'의 '내용'을 '나'의 '내용'의 <자르기>(1, '길이' - '개수')로 정하자
'지운글자'를 돌려주자
(참고: 2단계 — 명령이라면 하는 법과 되돌리는 법을 함께 알아요)
[명령]을 규정하자:
<실행>이 있어야 한다 ()
<취소>가 있어야 한다 ()
(참고: 3단계 — 동작마다 명령 하나. 되돌릴 때 필요한 것은 명령이 기억해요)
[명령]을 따르는 [쓰기명령]을 설계하자:
'편집기'를 [편집기]인 비어있음으로 정하여 숨기자
'글자'를 [문자열]인 ""으로 정하여 숨기자
처음 만들어질 때 ([편집기]인 '편집기', [문자열]인 '글자') 다음과 같이 하자:
'나'의 '편집기'를 '편집기'로 정하자
'나'의 '글자'를 '글자'로 정하자
<실행>을 만들자 ():
'나'의 '편집기'의 <붙이기>('나'의 '글자')를 실행하자
<취소>를 만들자 ():
'나'의 '편집기'의 <끝에서지우기>('나'의 '글자'의 '길이')를 실행하자
[명령]을 따르는 [지우기명령]을 설계하자:
'편집기'를 [편집기]인 비어있음으로 정하여 숨기자
'개수'를 [숫자]인 0으로 정하여 숨기자
'지운글자'를 [문자열]인 ""으로 정하여 숨기자
처음 만들어질 때 ([편집기]인 '편집기', [숫자]인 '개수') 다음과 같이 하자:
'나'의 '편집기'를 '편집기'로 정하자
'나'의 '개수'를 '개수'로 정하자
<실행>을 만들자 ():
'나'의 '지운글자'를 '나'의 '편집기'의 <끝에서지우기>('나'의 '개수')로 정하자
<취소>를 만들자 ():
'나'의 '편집기'의 <붙이기>('나'의 '지운글자')를 실행하자
(참고: 4단계 — 기록관리자는 명령을 실행하고 쌓아 두기만 해요. 무슨 명령인지는 몰라요)
[기록관리자]를 설계하자:
'지난명령들'을 []로 정하여 숨기자
<하기>를 만들자 ([명령]인 '명령'):
'명령'의 <실행>()을 실행하자
'나'의 '지난명령들'에 '명령'을 추가하자
<되돌리기>를 만들자 ():
만약 ('나'의 '지난명령들'의 '길이'가 0과 같다) 라면:
"되돌릴 것이 없어요."를 출력하자
돌려주자
'마지막'을 '나'의 '지난명령들' 뒤에서 꺼낸 값으로 정하자
'마지막'의 <취소>()를 실행하자
'메모'를 새로운 [편집기]()로 정하자
'기록'을 새로운 [기록관리자]()로 정하자
'기록'의 <하기>(새로운 [쓰기명령]('메모', "안녕"))을 실행하자
'기록'의 <하기>(새로운 [쓰기명령]('메모', "하세요"))를 실행하자
'기록'의 <하기>(새로운 [지우기명령]('메모', 2))를 실행하자
틀"지금: {'메모'의 '내용'}"을 출력하자
'기록'의 <되돌리기>()를 실행하자
틀"되돌린 뒤: {'메모'의 '내용'}"을 출력하자
'기록'의 <되돌리기>()를 실행하자
틀"한 번 더: {'메모'의 '내용'}"을 출력하자
'기록'의 <되돌리기>()를 실행하자
'기록'의 <되돌리기>()를 실행하자
실행 결과
지금: 안녕하
되돌린 뒤: 안녕하세요
한 번 더: 안녕
되돌릴 것이 없어요.
앞의 세 줄은 리팩터링 전과 같아요. 세 번째 되돌리기에서 “안녕”까지 지워져 메모가 비고, 네 번째에는 되돌릴 것이 없다고 알려 줘요.
단계별로 다시 보기
- 1단계 — 편집기는 기본 기능만.
[편집기]는 기록도, 되돌리기도 몰라요. 글자를 붙이고, 끝에서 지운 글자를 돌려줄 뿐이에요. - 2단계 — 약속 만들기.
[명령]은 “할 줄 알고, 되돌릴 줄도 알면 명령”이라는 약속이에요. 새 명령을 만들면서<취소>를 빼먹으면 하리가 바로 알려 줘요. 앞의 방식처럼 되돌리기가 조용히 안 되는 일이 없어요. - 3단계 — 명령마다 자기 이야기를 한곳에.
[지우기명령]은<실행>할 때 지운 글자를'지운글자'에 기억해 두고,<취소>할 때 그 글자를 다시 붙여요. 하는 법과 되돌리는 법이 한 클래스 안에 나란히 있어요. - 4단계 — 부르는 쪽은 무엇인지 몰라도 돼요.
[기록관리자]는 명령을 실행하고 목록에 쌓을 뿐이에요. 되돌릴 때는 맨 뒤의 명령을 꺼내<취소>를 불러요. 쓰기인지 지우기인지는 신경 쓰지 않아요.
왜 맨 뒤부터 되돌리나요?
마지막에 한 일이 가장 먼저 되돌려져야 하기 때문이에요. “안녕” → “하세요” → 2글자 지우기 순서로 했다면, 되돌릴 때는 지우기 → “하세요” → “안녕” 순서여야 메모가 올바르게 돌아와요. 쌓아 둔 접시를 위에서부터 꺼내는 것과 같아요.
새 명령을 더해 보면
“모두 지우기”를 넣는다고 해 볼게요.
- 앞의 방식:
[편집기]에<모두지우기>를 만들고,<되돌리기>의따라 나누자에도"모두지우기" 인 경우:를 넣어야 해요. - 커맨드 패턴:
[명령]을 따르는 [모두지우기명령]하나만 만들면 돼요.<실행>에서 지금 내용을 기억해 두고 비우고,<취소>에서 기억한 내용을 되붙이면 끝이에요.[편집기]도[기록관리자]도 그대로예요.
직접 해 보기
- 위 코드에
[모두지우기명령]을 더해 보세요.[편집기]의<끝에서지우기>에 지금 글자 수를 넘기면 모두 지울 수 있어요.- 더 해 보고 싶다면
[기록관리자]에 다시하기를 붙여 보세요.<되돌리기>에서 꺼낸 명령을'되돌린명령들'목록에 쌓아 두고,<다시하기>에서 거기서 꺼내<실행>하면 돼요. 명령이 객체라서 가능한 일이에요.
언제 쓰면 좋을까요?
- 되돌리기(그리고 다시하기)가 필요할 때
- 할 일을 바로 하지 않고 쌓아 두었다가 나중에, 또는 순서대로 실행하고 싶을 때 (작업 대기열, 예약 실행)
- 버튼, 단축키, 메뉴처럼 여러 곳에서 같은 일을 시킬 때. 같은 명령 객체를 나눠 쓰면 돼요
- 했던 일을 기록하거나 다시 재생하고 싶을 때 (매크로)
좋은 점과 아쉬운 점
| 좋은 점 | 아쉬운 점 |
|---|---|
| 하는 법과 되돌리는 법이 한 클래스에 모여 있어요 | 동작 하나마다 클래스가 하나씩 생겨요 |
| 새 동작을 더해도 부르는 쪽(기록관리자)을 고치지 않아요 | 명령이 받는 쪽(편집기)을 알아야 해서 연결이 하나 더 생겨요 |
| 되돌리기, 다시하기, 쌓아 두기, 나중에 실행하기가 쉬워요 | 되돌리기를 정확하게 만들려면 “무엇을 기억해 둘지” 잘 정해야 해요 |
규정 덕분에 <취소>를 빼먹으면 바로 알려 줘요 |
너무 이른 패턴은 독이에요.
되돌리기도, 쌓아 두기도 필요 없고 버튼 하나가 한 가지 일만 한다면, 그 일을 바로 부르는 게 가장 읽기 쉬워요. “되돌리기를 붙여 주세요”라는 말이 나올 때가 커맨드 패턴으로 옮길 때예요.