하리Hari

16. 메멘토 패턴 (Memento)

객체의 속을 드러내지 않고 그 순간의 상태를 저장해 두었다가 되돌리는 메멘토 패턴을 알아봅니다.

한 줄 요약
객체가 자기 상태를 스스로 사진 찍듯 담아 두고, 나중에 그 사진으로 스스로 되돌아가게 하는 패턴이에요. 사진을 보관하는 쪽은 사진 속이 어떻게 생겼는지 몰라도 돼요.

문제: 저장하려면 남의 속을 다 꺼내야 하는 코드

모험 게임을 만든다고 해 볼게요. 용사에게는 체력, 위치, 가방이 있어요. 게임에는 저장이 있어서, 용사가 쓰러지면 마지막으로 저장한 곳에서 다시 시작해요.

가장 먼저 떠오르는 방법은 저장하는 함수가 용사의 속을 하나하나 꺼내 사전에 담아 두고, 불러올 때 다시 하나하나 넣어 주는 거예요.

[용사]를 설계하자:
    '체력'을 [숫자]인 100으로 정하자
    '위치'를 [문자열]인 "마을"로 정하자
    '가방'을 ["나무칼"]로 정하자

    <가기>를 만들자 ([문자열]인 '곳'):
        '나'의 '위치'를 '곳'으로 정하자

    <줍기>를 만들자 ([문자열]인 '물건'):
        '나'의 '가방'에 '물건'을 추가하자

    <다치기>를 만들자 ([숫자]인 '피해'):
        '나'의 '체력'을 ('나'의 '체력' - '피해')로 정하자

    <상태>를 만들자 ():
        틀"{'나'의 '위치'} · 체력 {'나'의 '체력'} · 가방 {'나'의 '가방'}"을 출력하자

(참고: 저장하는 쪽이 용사의 속을 하나하나 꺼내 사전에 담아요. 가방은 목록이라 그대로 담으면 같은 목록을 함께 가리키므로, 하나씩 옮겨 담아 복사해요)
<저장하기>를 만들자 ('용사'):
    '가방복사'를 []로 정하자
    '용사'의 '가방'의 '물건'마다 반복하자:
        '가방복사'에 '물건'을 추가하자
    {"체력": '용사'의 '체력', "위치": '용사'의 '위치', "가방": '가방복사'}를 돌려주자

(참고: 불러올 때도 속을 하나하나 되돌려 넣어요. 여기서도 가방을 복사해야 저장이 망가지지 않아요)
<불러오기>를 만들자 ('용사', '저장'):
    '용사'의 '체력'을 ('저장'의 "체력")으로 정하자
    '용사'의 '위치'를 ('저장'의 "위치")로 정하자
    '가방복사'를 []로 정하자
    ('저장'의 "가방")의 '물건'마다 반복하자:
        '가방복사'에 '물건'을 추가하자
    '용사'의 '가방'을 '가방복사'로 정하자

'용사'를 새로운 [용사]()로 정하자
'용사'의 <가기>("숲")를 실행하자
'용사'의 <줍기>("약초")를 실행하자
'저장'을 <저장하기>('용사')로 정하자
"[저장했어요]"를 출력하자
'용사'의 <상태>()를 실행하자

'용사'의 <가기>("동굴")을 실행하자
'용사'의 <줍기>("횃불")을 실행하자
'용사'의 <다치기>(120)을 실행하자
'용사'의 <상태>()를 실행하자

"[쓰러졌어요. 저장한 곳에서 다시 시작해요]"를 출력하자
<불러오기>('용사', '저장')를 실행하자
'용사'의 <상태>()를 실행하자

실행 결과
[저장했어요]
숲 · 체력 100 · 가방 [나무칼, 약초]
동굴 · 체력 -20 · 가방 [나무칼, 약초, 횃불]
[쓰러졌어요. 저장한 곳에서 다시 시작해요]
숲 · 체력 100 · 가방 [나무칼, 약초]

잘 돌아가요. 그런데 게임이 커지면 아파져요.

  • 저장하는 쪽이 용사의 속을 다 알아요. <저장하기>와 <불러오기>는 용사에게 '체력', '위치', '가방'이 있다는 것과, 그 이름까지 정확히 알아야 해요.
  • 용사에게 무엇이 하나 늘면 빠뜨리기 쉬워요. '돈'이 생기면 <저장하기>와 <불러오기>를 둘 다 고쳐야 해요. 한쪽만 고치면 에러도 없이 돈만 조용히 되돌아오지 않아요.
  • 복사해야 한다는 것까지 쓰는 쪽이 챙겨요. 가방은 목록이라서 그대로 담으면 저장과 용사가 같은 목록을 함께 가리켜요. 그러면 동굴에서 주운 횃불이 저장에도 들어가 버려요. 그래서 두 함수 모두 하나씩 옮겨 담는 코드를 넣었어요.
  • 용사의 속을 숨길 수 없어요. 밖에서 꺼내 가야 하니 '체력'에 숨기자를 붙일 수 없어요. 아무 곳에서나 체력을 고칠 수 있어요.
  • 저장도 그냥 사전이에요. 누구든 '저장'의 "체력"을 고쳐 둘 수 있어요.

해결책: 용사가 자기 사진을 직접 찍게 하기

메멘토 패턴은 저장을 세 역할로 나눠요.

  1. 원본(용사)이 자기 상태를 담은 메멘토(저장점)를 직접 만들어요. 자기 속은 자기가 제일 잘 아니까요.
  2. 메멘토는 그 순간의 상태를 담아 두기만 해요. 담긴 값을 바꾸는 방법은 주지 않아요.
  3. 관리인(저장소)은 메멘토를 쌓아 두었다가 필요할 때 건네주기만 해요. 메멘토 안을 들여다보거나 고치지 않아요.
  4. 되돌릴 때는 원본이 메멘토를 받아서 스스로 자기 상태를 되돌려요.

비유: 즉석 사진

여행지에서 즉석 사진을 찍으면, 사진에는 그 순간의 모습이 그대로 남아요. 사진첩은 사진을 꽂아 두기만 하고, 사진 속 장면을 고치지 않아요. 나중에 “그때 그 자리”로 돌아가고 싶으면 사진을 꺼내 보고 그대로 서면 돼요. 사진은 메멘토, 사진첩은 관리인, 사진을 찍고 그 자리로 돌아가는 사람은 원본이에요.

누가 무엇을 하나요?

역할 이 예제에서 하는 일
원본 [용사] 자기 상태로 저장점을 만들고, 저장점으로 스스로 되돌아가요
메멘토 [저장점] 그 순간의 상태를 담아 두고, 꺼내 보는 방법만 줘요
관리인 [저장소] 저장점을 쌓아 두었다가 건네줘요. 속은 몰라요

하리로 만들기

같은 게임을 메멘토 패턴으로 다시 만들어 볼게요.

(참고: 1단계 — 저장점(메멘토). 그 순간의 용사를 그대로 담아 두고, 바꾸는 방법은 주지 않아요)
[저장점]을 설계하자:
    '체력'을 [숫자]인 0으로 정하여 숨기자
    '위치'를 [문자열]인 ""으로 정하여 숨기자
    '가방'을 []로 정하여 숨기자

    처음 만들어질 때 ([숫자]인 '체력', [문자열]인 '위치', '가방') 다음과 같이 하자:
        '나'의 '체력'을 '체력'으로 정하자
        '나'의 '위치'를 '위치'로 정하자
        (참고: 가방은 하나씩 옮겨 담아 복사해 둬요. 그래야 용사의 가방이 바뀌어도 저장점은 그대로예요)
        '복사본'을 []로 정하자
        '가방'의 '물건'마다 반복하자:
            '복사본'에 '물건'을 추가하자
        '나'의 '가방'을 '복사본'으로 정하자

    [숫자]를 돌려주는 <체력>을 만들자 ():
        '나'의 '체력'을 돌려주자

    [문자열]을 돌려주는 <위치>를 만들자 ():
        '나'의 '위치'를 돌려주자

    (참고: 가방도 복사해서 내줘요. 받은 쪽이 바꿔도 저장점은 그대로예요)
    <가방>을 만들자 ():
        '꺼낸가방'을 []로 정하자
        '나'의 '가방'의 '물건'마다 반복하자:
            '꺼낸가방'에 '물건'을 추가하자
        '꺼낸가방'을 돌려주자

(참고: 2단계 — 용사(원본). 속은 숨기고, 저장점을 만들고 저장점으로 돌아가는 일은 스스로 해요)
[용사]를 설계하자:
    '체력'을 [숫자]인 100으로 정하여 숨기자
    '위치'를 [문자열]인 "마을"로 정하여 숨기자
    '가방'을 ["나무칼"]로 정하여 숨기자

    <가기>를 만들자 ([문자열]인 '곳'):
        '나'의 '위치'를 '곳'으로 정하자

    <줍기>를 만들자 ([문자열]인 '물건'):
        '나'의 '가방'에 '물건'을 추가하자

    <다치기>를 만들자 ([숫자]인 '피해'):
        '나'의 '체력'을 ('나'의 '체력' - '피해')로 정하자

    <상태>를 만들자 ():
        틀"{'나'의 '위치'} · 체력 {'나'의 '체력'} · 가방 {'나'의 '가방'}"을 출력하자

    [저장점]을 돌려주는 <저장하기>를 만들자 ():
        새로운 [저장점]('나'의 '체력', '나'의 '위치', '나'의 '가방')을 돌려주자

    <불러오기>를 만들자 ([저장점]인 '점'):
        '나'의 '체력'을 '점'의 <체력>()으로 정하자
        '나'의 '위치'를 '점'의 <위치>()로 정하자
        '나'의 '가방'을 '점'의 <가방>()으로 정하자

(참고: 3단계 — 저장소(관리인). 저장점을 쌓아 두기만 하고, 속은 들여다보지 않아요)
[저장소]를 설계하자:
    '저장점들'을 []로 정하여 숨기자

    <남기기>를 만들자 ([저장점]인 '점'):
        '나'의 '저장점들'에 '점'을 추가하자

    [저장점]을 돌려주는 <마지막>을 만들자 ():
        '개수'를 '나'의 '저장점들'의 '길이'로 정하자
        '나'의 '저장점들'의 '개수'번째를 돌려주자

'용사'를 새로운 [용사]()로 정하자
'저장소'를 새로운 [저장소]()로 정하자

'용사'의 <가기>("숲")를 실행하자
'용사'의 <줍기>("약초")를 실행하자
'저장소'의 <남기기>('용사'의 <저장하기>())를 실행하자
"[저장했어요]"를 출력하자
'용사'의 <상태>()를 실행하자

'용사'의 <가기>("동굴")을 실행하자
'용사'의 <줍기>("횃불")을 실행하자
'용사'의 <다치기>(120)을 실행하자
'용사'의 <상태>()를 실행하자

"[쓰러졌어요. 저장한 곳에서 다시 시작해요]"를 출력하자
'용사'의 <불러오기>('저장소'의 <마지막>())를 실행하자
'용사'의 <상태>()를 실행하자

실행 결과
[저장했어요]
숲 · 체력 100 · 가방 [나무칼, 약초]
동굴 · 체력 -20 · 가방 [나무칼, 약초, 횃불]
[쓰러졌어요. 저장한 곳에서 다시 시작해요]
숲 · 체력 100 · 가방 [나무칼, 약초]

결과는 리팩터링 전과 한 줄도 다르지 않아요. 하지만 이제 용사의 속은 모두 숨겨져 있고, 저장소는 저장점 안에 무엇이 있는지 전혀 몰라요.

단계별로 다시 보기

  • 1단계 — 저장점(메멘토). [저장점]은 체력, 위치, 가방을 숨긴 채 담아 둬요. 꺼내 보는 <체력>, <위치>, <가방>은 있지만, 값을 바꾸는 기능은 없어요. 가방은 만들 때 한 번, 꺼내 줄 때 한 번 복사해서, 용사가 나중에 무엇을 주워도 저장점은 그대로예요.
  • 2단계 — 용사(원본). <저장하기>는 자기 상태로 저장점을 만들어 돌려주고, <불러오기>는 저장점을 받아 자기 상태를 되돌려요. “무엇을 저장해야 하는지”를 아는 곳이 용사 자신뿐이에요.
  • 3단계 — 저장소(관리인). [저장소]는 저장점을 <남기기>로 쌓고 <마지막>으로 건네줄 뿐이에요. 저장점 안에 무엇이 있는지 몰라도 되고, 알 필요도 없어요.
  • 쓰는 쪽. '저장소'의 <남기기>('용사'의 <저장하기>())와 '용사'의 <불러오기>('저장소'의 <마지막>()) 두 줄이면 저장하고 불러와요. 체력도, 가방 복사도 신경 쓰지 않아요.

하리에는 “용사만 볼 수 있게”가 없어요
책에 나오는 메멘토는 원본만 메멘토 속을 볼 수 있게 하기도 해요. 하리의 숨기자는 “그 클래스 안에서만”이라서, [용사]가 [저장점]의 숨긴 값을 직접 읽을 수는 없어요. 그래서 여기서는 꺼내 보는 기능만 있고 바꾸는 기능은 없는 저장점을 만들었어요. 저장소가 그 기능을 부를 수는 있어도, 저장점을 바꾸는 기능은 어디에도 없어요.

저장점의 속은 숨겨져 있어서, 밖에서 몰래 물건을 넣으려 하면 그 자리에서 막혀요.

[저장점]을 설계하자:
    '가방'을 []로 정하여 숨기자

    처음 만들어질 때 ('가방') 다음과 같이 하자:
        '복사본'을 []로 정하자
        '가방'의 '물건'마다 반복하자:
            '복사본'에 '물건'을 추가하자
        '나'의 '가방'을 '복사본'으로 정하자

'점'을 새로운 [저장점](["나무칼"])으로 정하자

(참고: 저장점에 몰래 물건을 넣으려 하면 막혀요)
'점'의 '가방'에 "전설의 검"을 추가하자

용사에게 새 상태를 더해 보면

용사에게 '돈'이 생겼다고 해 볼게요.

  • 앞의 방식: <저장하기>와 <불러오기>에 돈을 하나씩 더해야 해요. 하나라도 빠뜨리면 돈만 되돌아오지 않아요.
  • 메멘토 패턴: [용사]와 [저장점]만 고치면 돼요. 저장소와 쓰는 쪽은 한 글자도 바뀌지 않아요. 저장을 다루는 코드가 용사와 저장점 둘 사이에 모여 있으니까요.

직접 해 보기
위 코드의 용사에게 '돈'(처음에는 0)과 <벌기>([숫자]인 '얼마')를 더해 보세요. 저장점에도 돈을 담고, <불러오기>가 돈도 되돌리게 하면 돼요. 숲에서 50을 벌고 저장한 뒤 동굴에서 30을 더 벌고 쓰러지면, 불러온 뒤 돈이 50이어야 해요. 저장소와 맨 아래의 쓰는 코드는 그대로 둬도 돼요.

언제 쓰면 좋을까요?

  • 되돌리기, 저장과 불러오기, 체크포인트가 필요할 때
  • 객체의 상태를 저장해야 하는데, 그 속을 밖에 드러내고 싶지 않을 때
  • 상태를 이루는 값이 여럿이라 하나하나 저장하다가 빠뜨리기 쉬울 때
  • 시험 삼아 바꿔 보고, 마음에 안 들면 통째로 원래대로 돌리고 싶을 때

좋은 점과 아쉬운 점

좋은 점 아쉬운 점
원본의 속을 숨긴 채 저장하고 되돌릴 수 있어요 저장할 때마다 상태를 통째로 담아서, 상태가 크면 기억 공간을 많이 써요
무엇을 저장할지는 원본만 알아서, 빠뜨릴 일이 줄어요 목록처럼 함께 가리키는 값은 복사를 잊지 말아야 해요
관리인은 저장점을 쌓아 두기만 하면 돼서 단순해요 저장점을 너무 많이 쌓으면 오래된 것을 언제 버릴지 정해야 해요
저장점에는 바꾸는 기능이 없어서, 저장한 순간 그대로 남아요 하리에서는 원본만 읽게 막을 수 없어서, 읽는 기능은 누구나 부를 수 있어요

너무 이른 패턴은 독이에요.
저장할 값이 숫자 하나뿐이라면 그 숫자를 변수에 담아 두면 그만이에요. 상태를 이루는 값이 여럿이고, 그 속을 숨기고 싶어질 때 메멘토를 꺼내도 늦지 않아요.

다른 패턴과의 관계

  • 커맨드 패턴과 되돌리기를 다르게 풀어요. 커맨드는 “무엇을 했는지”를 기억하고 거꾸로 해서 되돌리고, 메멘토는 “그때 어땠는지”를 통째로 기억하고 그 상태로 돌아가요. 거꾸로 하기 어려운 일이라면 메멘토가 편하고, 상태가 아주 크다면 커맨드가 가벼워요. 둘을 함께 써서 명령마다 실행 전 저장점을 남겨 두기도 해요.
  • 반복자 패턴으로 저장 목록을 보여줄 수 있어요. 저장소가 반복자를 만들어 주면, 저장 목록 화면은 저장소의 속을 몰라도 저장점을 하나씩 꺼내 보여줄 수 있어요.
  • 상태 패턴과는 이름만 비슷해요. 상태 패턴은 상태에 따라 행동을 바꾸는 패턴이고, 메멘토는 상태를 저장하고 되돌리는 패턴이에요.
놀이터
결과
코드를 작성하고 '실행' 버튼을 눌러보세요.