16. 메멘토 패턴 (Memento)
객체의 속을 드러내지 않고 그 순간의 상태를 저장해 두었다가 되돌리는 메멘토 패턴을 알아봅니다.
한 줄 요약
객체가 자기 상태를 스스로 사진 찍듯 담아 두고, 나중에 그 사진으로 스스로 되돌아가게 하는 패턴이에요. 사진을 보관하는 쪽은 사진 속이 어떻게 생겼는지 몰라도 돼요.
문제: 저장하려면 남의 속을 다 꺼내야 하는 코드
모험 게임을 만든다고 해 볼게요. 용사에게는 체력, 위치, 가방이 있어요. 게임에는 저장이 있어서, 용사가 쓰러지면 마지막으로 저장한 곳에서 다시 시작해요.
가장 먼저 떠오르는 방법은 저장하는 함수가 용사의 속을 하나하나 꺼내 사전에 담아 두고, 불러올 때 다시 하나하나 넣어 주는 거예요.
[용사]를 설계하자:
'체력'을 [숫자]인 100으로 정하자
'위치'를 [문자열]인 "마을"로 정하자
'가방'을 ["나무칼"]로 정하자
<가기>를 만들자 ([문자열]인 '곳'):
'나'의 '위치'를 '곳'으로 정하자
<줍기>를 만들자 ([문자열]인 '물건'):
'나'의 '가방'에 '물건'을 추가하자
<다치기>를 만들자 ([숫자]인 '피해'):
'나'의 '체력'을 ('나'의 '체력' - '피해')로 정하자
<상태>를 만들자 ():
틀"{'나'의 '위치'} · 체력 {'나'의 '체력'} · 가방 {'나'의 '가방'}"을 출력하자
(참고: 저장하는 쪽이 용사의 속을 하나하나 꺼내 사전에 담아요. 가방은 목록이라 그대로 담으면 같은 목록을 함께 가리키므로, 하나씩 옮겨 담아 복사해요)
<저장하기>를 만들자 ('용사'):
'가방복사'를 []로 정하자
'용사'의 '가방'의 '물건'마다 반복하자:
'가방복사'에 '물건'을 추가하자
{"체력": '용사'의 '체력', "위치": '용사'의 '위치', "가방": '가방복사'}를 돌려주자
(참고: 불러올 때도 속을 하나하나 되돌려 넣어요. 여기서도 가방을 복사해야 저장이 망가지지 않아요)
<불러오기>를 만들자 ('용사', '저장'):
'용사'의 '체력'을 ('저장'의 "체력")으로 정하자
'용사'의 '위치'를 ('저장'의 "위치")로 정하자
'가방복사'를 []로 정하자
('저장'의 "가방")의 '물건'마다 반복하자:
'가방복사'에 '물건'을 추가하자
'용사'의 '가방'을 '가방복사'로 정하자
'용사'를 새로운 [용사]()로 정하자
'용사'의 <가기>("숲")를 실행하자
'용사'의 <줍기>("약초")를 실행하자
'저장'을 <저장하기>('용사')로 정하자
"[저장했어요]"를 출력하자
'용사'의 <상태>()를 실행하자
'용사'의 <가기>("동굴")을 실행하자
'용사'의 <줍기>("횃불")을 실행하자
'용사'의 <다치기>(120)을 실행하자
'용사'의 <상태>()를 실행하자
"[쓰러졌어요. 저장한 곳에서 다시 시작해요]"를 출력하자
<불러오기>('용사', '저장')를 실행하자
'용사'의 <상태>()를 실행하자
실행 결과
[저장했어요]
숲 · 체력 100 · 가방 [나무칼, 약초]
동굴 · 체력 -20 · 가방 [나무칼, 약초, 횃불]
[쓰러졌어요. 저장한 곳에서 다시 시작해요]
숲 · 체력 100 · 가방 [나무칼, 약초]
잘 돌아가요. 그런데 게임이 커지면 아파져요.
- 저장하는 쪽이 용사의 속을 다 알아요.
<저장하기>와<불러오기>는 용사에게'체력','위치','가방'이 있다는 것과, 그 이름까지 정확히 알아야 해요. - 용사에게 무엇이 하나 늘면 빠뜨리기 쉬워요.
'돈'이 생기면<저장하기>와<불러오기>를 둘 다 고쳐야 해요. 한쪽만 고치면 에러도 없이 돈만 조용히 되돌아오지 않아요. - 복사해야 한다는 것까지 쓰는 쪽이 챙겨요. 가방은 목록이라서 그대로 담으면 저장과 용사가 같은 목록을 함께 가리켜요. 그러면 동굴에서 주운 횃불이 저장에도 들어가 버려요. 그래서 두 함수 모두 하나씩 옮겨 담는 코드를 넣었어요.
- 용사의 속을 숨길 수 없어요. 밖에서 꺼내 가야 하니
'체력'에숨기자를 붙일 수 없어요. 아무 곳에서나 체력을 고칠 수 있어요. - 저장도 그냥 사전이에요. 누구든
'저장'의"체력"을 고쳐 둘 수 있어요.
해결책: 용사가 자기 사진을 직접 찍게 하기
메멘토 패턴은 저장을 세 역할로 나눠요.
- 원본(용사)이 자기 상태를 담은 메멘토(저장점)를 직접 만들어요. 자기 속은 자기가 제일 잘 아니까요.
- 메멘토는 그 순간의 상태를 담아 두기만 해요. 담긴 값을 바꾸는 방법은 주지 않아요.
- 관리인(저장소)은 메멘토를 쌓아 두었다가 필요할 때 건네주기만 해요. 메멘토 안을 들여다보거나 고치지 않아요.
- 되돌릴 때는 원본이 메멘토를 받아서 스스로 자기 상태를 되돌려요.
비유: 즉석 사진
여행지에서 즉석 사진을 찍으면, 사진에는 그 순간의 모습이 그대로 남아요. 사진첩은 사진을 꽂아 두기만 하고, 사진 속 장면을 고치지 않아요. 나중에 “그때 그 자리”로 돌아가고 싶으면 사진을 꺼내 보고 그대로 서면 돼요. 사진은 메멘토, 사진첩은 관리인, 사진을 찍고 그 자리로 돌아가는 사람은 원본이에요.
누가 무엇을 하나요?
| 역할 | 이 예제에서 | 하는 일 |
|---|---|---|
| 원본 | [용사] |
자기 상태로 저장점을 만들고, 저장점으로 스스로 되돌아가요 |
| 메멘토 | [저장점] |
그 순간의 상태를 담아 두고, 꺼내 보는 방법만 줘요 |
| 관리인 | [저장소] |
저장점을 쌓아 두었다가 건네줘요. 속은 몰라요 |
하리로 만들기
같은 게임을 메멘토 패턴으로 다시 만들어 볼게요.
(참고: 1단계 — 저장점(메멘토). 그 순간의 용사를 그대로 담아 두고, 바꾸는 방법은 주지 않아요)
[저장점]을 설계하자:
'체력'을 [숫자]인 0으로 정하여 숨기자
'위치'를 [문자열]인 ""으로 정하여 숨기자
'가방'을 []로 정하여 숨기자
처음 만들어질 때 ([숫자]인 '체력', [문자열]인 '위치', '가방') 다음과 같이 하자:
'나'의 '체력'을 '체력'으로 정하자
'나'의 '위치'를 '위치'로 정하자
(참고: 가방은 하나씩 옮겨 담아 복사해 둬요. 그래야 용사의 가방이 바뀌어도 저장점은 그대로예요)
'복사본'을 []로 정하자
'가방'의 '물건'마다 반복하자:
'복사본'에 '물건'을 추가하자
'나'의 '가방'을 '복사본'으로 정하자
[숫자]를 돌려주는 <체력>을 만들자 ():
'나'의 '체력'을 돌려주자
[문자열]을 돌려주는 <위치>를 만들자 ():
'나'의 '위치'를 돌려주자
(참고: 가방도 복사해서 내줘요. 받은 쪽이 바꿔도 저장점은 그대로예요)
<가방>을 만들자 ():
'꺼낸가방'을 []로 정하자
'나'의 '가방'의 '물건'마다 반복하자:
'꺼낸가방'에 '물건'을 추가하자
'꺼낸가방'을 돌려주자
(참고: 2단계 — 용사(원본). 속은 숨기고, 저장점을 만들고 저장점으로 돌아가는 일은 스스로 해요)
[용사]를 설계하자:
'체력'을 [숫자]인 100으로 정하여 숨기자
'위치'를 [문자열]인 "마을"로 정하여 숨기자
'가방'을 ["나무칼"]로 정하여 숨기자
<가기>를 만들자 ([문자열]인 '곳'):
'나'의 '위치'를 '곳'으로 정하자
<줍기>를 만들자 ([문자열]인 '물건'):
'나'의 '가방'에 '물건'을 추가하자
<다치기>를 만들자 ([숫자]인 '피해'):
'나'의 '체력'을 ('나'의 '체력' - '피해')로 정하자
<상태>를 만들자 ():
틀"{'나'의 '위치'} · 체력 {'나'의 '체력'} · 가방 {'나'의 '가방'}"을 출력하자
[저장점]을 돌려주는 <저장하기>를 만들자 ():
새로운 [저장점]('나'의 '체력', '나'의 '위치', '나'의 '가방')을 돌려주자
<불러오기>를 만들자 ([저장점]인 '점'):
'나'의 '체력'을 '점'의 <체력>()으로 정하자
'나'의 '위치'를 '점'의 <위치>()로 정하자
'나'의 '가방'을 '점'의 <가방>()으로 정하자
(참고: 3단계 — 저장소(관리인). 저장점을 쌓아 두기만 하고, 속은 들여다보지 않아요)
[저장소]를 설계하자:
'저장점들'을 []로 정하여 숨기자
<남기기>를 만들자 ([저장점]인 '점'):
'나'의 '저장점들'에 '점'을 추가하자
[저장점]을 돌려주는 <마지막>을 만들자 ():
'개수'를 '나'의 '저장점들'의 '길이'로 정하자
'나'의 '저장점들'의 '개수'번째를 돌려주자
'용사'를 새로운 [용사]()로 정하자
'저장소'를 새로운 [저장소]()로 정하자
'용사'의 <가기>("숲")를 실행하자
'용사'의 <줍기>("약초")를 실행하자
'저장소'의 <남기기>('용사'의 <저장하기>())를 실행하자
"[저장했어요]"를 출력하자
'용사'의 <상태>()를 실행하자
'용사'의 <가기>("동굴")을 실행하자
'용사'의 <줍기>("횃불")을 실행하자
'용사'의 <다치기>(120)을 실행하자
'용사'의 <상태>()를 실행하자
"[쓰러졌어요. 저장한 곳에서 다시 시작해요]"를 출력하자
'용사'의 <불러오기>('저장소'의 <마지막>())를 실행하자
'용사'의 <상태>()를 실행하자
실행 결과
[저장했어요]
숲 · 체력 100 · 가방 [나무칼, 약초]
동굴 · 체력 -20 · 가방 [나무칼, 약초, 횃불]
[쓰러졌어요. 저장한 곳에서 다시 시작해요]
숲 · 체력 100 · 가방 [나무칼, 약초]
결과는 리팩터링 전과 한 줄도 다르지 않아요. 하지만 이제 용사의 속은 모두 숨겨져 있고, 저장소는 저장점 안에 무엇이 있는지 전혀 몰라요.
단계별로 다시 보기
- 1단계 — 저장점(메멘토).
[저장점]은 체력, 위치, 가방을 숨긴 채 담아 둬요. 꺼내 보는<체력>,<위치>,<가방>은 있지만, 값을 바꾸는 기능은 없어요. 가방은 만들 때 한 번, 꺼내 줄 때 한 번 복사해서, 용사가 나중에 무엇을 주워도 저장점은 그대로예요. - 2단계 — 용사(원본).
<저장하기>는 자기 상태로 저장점을 만들어 돌려주고,<불러오기>는 저장점을 받아 자기 상태를 되돌려요. “무엇을 저장해야 하는지”를 아는 곳이 용사 자신뿐이에요. - 3단계 — 저장소(관리인).
[저장소]는 저장점을<남기기>로 쌓고<마지막>으로 건네줄 뿐이에요. 저장점 안에 무엇이 있는지 몰라도 되고, 알 필요도 없어요. - 쓰는 쪽.
'저장소'의 <남기기>('용사'의 <저장하기>())와'용사'의 <불러오기>('저장소'의 <마지막>())두 줄이면 저장하고 불러와요. 체력도, 가방 복사도 신경 쓰지 않아요.
하리에는 “용사만 볼 수 있게”가 없어요
책에 나오는 메멘토는 원본만 메멘토 속을 볼 수 있게 하기도 해요. 하리의숨기자는 “그 클래스 안에서만”이라서,[용사]가[저장점]의 숨긴 값을 직접 읽을 수는 없어요. 그래서 여기서는 꺼내 보는 기능만 있고 바꾸는 기능은 없는 저장점을 만들었어요. 저장소가 그 기능을 부를 수는 있어도, 저장점을 바꾸는 기능은 어디에도 없어요.
저장점의 속은 숨겨져 있어서, 밖에서 몰래 물건을 넣으려 하면 그 자리에서 막혀요.
[저장점]을 설계하자:
'가방'을 []로 정하여 숨기자
처음 만들어질 때 ('가방') 다음과 같이 하자:
'복사본'을 []로 정하자
'가방'의 '물건'마다 반복하자:
'복사본'에 '물건'을 추가하자
'나'의 '가방'을 '복사본'으로 정하자
'점'을 새로운 [저장점](["나무칼"])으로 정하자
(참고: 저장점에 몰래 물건을 넣으려 하면 막혀요)
'점'의 '가방'에 "전설의 검"을 추가하자
용사에게 새 상태를 더해 보면
용사에게 '돈'이 생겼다고 해 볼게요.
- 앞의 방식:
<저장하기>와<불러오기>에 돈을 하나씩 더해야 해요. 하나라도 빠뜨리면 돈만 되돌아오지 않아요. - 메멘토 패턴:
[용사]와[저장점]만 고치면 돼요. 저장소와 쓰는 쪽은 한 글자도 바뀌지 않아요. 저장을 다루는 코드가 용사와 저장점 둘 사이에 모여 있으니까요.
직접 해 보기
위 코드의 용사에게'돈'(처음에는 0)과<벌기>([숫자]인 '얼마')를 더해 보세요. 저장점에도 돈을 담고,<불러오기>가 돈도 되돌리게 하면 돼요. 숲에서 50을 벌고 저장한 뒤 동굴에서 30을 더 벌고 쓰러지면, 불러온 뒤 돈이 50이어야 해요. 저장소와 맨 아래의 쓰는 코드는 그대로 둬도 돼요.
언제 쓰면 좋을까요?
- 되돌리기, 저장과 불러오기, 체크포인트가 필요할 때
- 객체의 상태를 저장해야 하는데, 그 속을 밖에 드러내고 싶지 않을 때
- 상태를 이루는 값이 여럿이라 하나하나 저장하다가 빠뜨리기 쉬울 때
- 시험 삼아 바꿔 보고, 마음에 안 들면 통째로 원래대로 돌리고 싶을 때
좋은 점과 아쉬운 점
| 좋은 점 | 아쉬운 점 |
|---|---|
| 원본의 속을 숨긴 채 저장하고 되돌릴 수 있어요 | 저장할 때마다 상태를 통째로 담아서, 상태가 크면 기억 공간을 많이 써요 |
| 무엇을 저장할지는 원본만 알아서, 빠뜨릴 일이 줄어요 | 목록처럼 함께 가리키는 값은 복사를 잊지 말아야 해요 |
| 관리인은 저장점을 쌓아 두기만 하면 돼서 단순해요 | 저장점을 너무 많이 쌓으면 오래된 것을 언제 버릴지 정해야 해요 |
| 저장점에는 바꾸는 기능이 없어서, 저장한 순간 그대로 남아요 | 하리에서는 원본만 읽게 막을 수 없어서, 읽는 기능은 누구나 부를 수 있어요 |
너무 이른 패턴은 독이에요.
저장할 값이 숫자 하나뿐이라면 그 숫자를 변수에 담아 두면 그만이에요. 상태를 이루는 값이 여럿이고, 그 속을 숨기고 싶어질 때 메멘토를 꺼내도 늦지 않아요.
다른 패턴과의 관계
- 커맨드 패턴과 되돌리기를 다르게 풀어요. 커맨드는 “무엇을 했는지”를 기억하고 거꾸로 해서 되돌리고, 메멘토는 “그때 어땠는지”를 통째로 기억하고 그 상태로 돌아가요. 거꾸로 하기 어려운 일이라면 메멘토가 편하고, 상태가 아주 크다면 커맨드가 가벼워요. 둘을 함께 써서 명령마다 실행 전 저장점을 남겨 두기도 해요.
- 반복자 패턴으로 저장 목록을 보여줄 수 있어요. 저장소가 반복자를 만들어 주면, 저장 목록 화면은 저장소의 속을 몰라도 저장점을 하나씩 꺼내 보여줄 수 있어요.
- 상태 패턴과는 이름만 비슷해요. 상태 패턴은 상태에 따라 행동을 바꾸는 패턴이고, 메멘토는 상태를 저장하고 되돌리는 패턴이에요.