19. 프로토타입 패턴 (Prototype)
잘 맞춰 둔 견본 객체가 스스로를 복제하게 해서, 클래스를 몰라도 똑같은 객체를 만들어 내는 프로토타입 패턴을 알아봅니다.
한 줄 요약
새 객체를 처음부터 만들지 않고, 잘 맞춰 둔 견본 객체를 복제하는 패턴이에요. 베끼는 일은 바깥이 아니라 견본 자신이 해요.
문제: 바깥에서 칸을 하나하나 옮겨 적는 코드
게임에 몬스터를 내보낸다고 해 볼게요. 몬스터마다 이름, 체력, 기술 목록이 있고, 보스 몬스터에게는 부하 수도 있어요. 한 번 잘 맞춰 둔 견본과 똑같은 몬스터를 여러 마리 내보내고 싶어요.
가장 먼저 떠오르는 방법은 견본의 칸을 읽어서 새 몬스터의 생성자에 그대로 넘기는 거예요.
[몬스터]를 설계하자:
'이름'을 ""으로 정하자
'체력'을 0으로 정하자
'기술들'을 []로 정하자
처음 만들어질 때 ([문자열]인 '이름', [숫자]인 '체력', '기술들') 다음과 같이 하자:
'나'의 '이름'을 '이름'으로 정하자
'나'의 '체력'을 '체력'으로 정하자
'나'의 '기술들'을 '기술들'로 정하자
<소개하기>를 만들자 ():
틀"{'나'의 '이름'} (체력 {'나'의 '체력'}) 기술: {'나'의 '기술들'}"을 출력하자
[보스몬스터]를 설계하자:
'이름'을 ""으로 정하자
'체력'을 0으로 정하자
'기술들'을 []로 정하자
'부하수'를 0으로 정하자
처음 만들어질 때 ([문자열]인 '이름', [숫자]인 '체력', '기술들', [숫자]인 '부하수') 다음과 같이 하자:
'나'의 '이름'을 '이름'으로 정하자
'나'의 '체력'을 '체력'으로 정하자
'나'의 '기술들'을 '기술들'로 정하자
'나'의 '부하수'를 '부하수'로 정하자
<소개하기>를 만들자 ():
틀"{'나'의 '이름'} (체력 {'나'의 '체력'}, 부하 {'나'의 '부하수'}) 기술: {'나'의 '기술들'}"을 출력하자
(참고: 잘 맞춰 둔 견본 몬스터)
'슬라임견본'을 새로운 [몬스터]("슬라임", 30, ["몸통박치기"])로 정하자
'왕견본'을 새로운 [보스몬스터]("슬라임 왕", 300, ["몸통박치기", "분열"], 3)으로 정하자
(참고: 견본을 베끼려면 바깥에서 칸을 하나하나 옮겨 적어야 해요. 클래스마다 옮겨 적는 법도 달라요)
'슬라임1'을 새로운 [몬스터]('슬라임견본'의 '이름', '슬라임견본'의 '체력', '슬라임견본'의 '기술들')로 정하자
'왕1'을 새로운 [보스몬스터]('왕견본'의 '이름', '왕견본'의 '체력', '왕견본'의 '기술들', '왕견본'의 '부하수')로 정하자
(참고: 슬라임1만 새 기술을 배웠는데…)
'슬라임1'의 '기술들'에 "점액 뿌리기"를 추가하자
'슬라임견본'의 <소개하기>()를 실행하자
'슬라임1'의 <소개하기>()를 실행하자
'왕1'의 <소개하기>()를 실행하자
실행 결과
슬라임 (체력 30) 기술: [몸통박치기, 점액 뿌리기]
슬라임 (체력 30) 기술: [몸통박치기, 점액 뿌리기]
슬라임 왕 (체력 300, 부하 3) 기술: [몸통박치기, 분열]
이상해요. 새 기술은 '슬라임1'만 배웠는데, 견본까지 “점액 뿌리기”를 알게 됐어요. 이제 이 견본으로 만드는 슬라임은 모두 점액을 뿌려요.
- 목록을 같이 쓰게 돼요.
'슬라임견본'의 '기술들'을 넘기면 목록이 새로 생기지 않고, 같은 목록 하나를 둘이 함께 들고 있게 돼요. 한쪽에 추가하면 다른 쪽도 바뀌어요. - 베끼는 쪽이 견본의 속을 다 알아야 해요. 어떤 칸이 있는지, 생성자에 어떤 순서로 넘기는지 바깥이 알아야 해요. 몬스터에 칸이 하나 늘면 베끼는 곳을 모두 찾아 고쳐야 해요.
- 클래스마다 베끼는 법이 달라요.
[몬스터]와[보스몬스터]는 생성자가 달라서, 베끼는 줄도 따로따로 적어야 해요. 견본이 무슨 클래스인지 모르면 베낄 수조차 없어요. - 숨긴 칸은 베낄 수 없어요. 클래스가
'부하수'를 숨겨 두었다면, 바깥에서는 읽을 수 없으니 옮겨 적을 수도 없어요.
해결책: 견본에게 “너를 하나 더 만들어 줘”라고 부탁하기
프로토타입 패턴은 베끼는 일을 바깥에서 꺼내 견본 자신에게 맡겨요.
- 원형의 약속을 정해요. “나와 똑같은 것을 하나 더 만들어 줘”라는
<복제하기>하나면 돼요. - 클래스마다
<복제하기>를 만들어요. 자기 칸은 자기가 가장 잘 아니까, 숨긴 칸까지 빠짐없이 베끼고 목록은 새 목록에 옮겨 담아요. - 잘 맞춰 둔 견본들을 이름표를 붙여 도감에 모아 둬요.
- 쓰는 쪽은 도감에 이름만 말하고 복제본을 받아 가요. 견본이 무슨 클래스인지 몰라도 돼요.
비유: 열쇠 가게의 열쇠 복사
열쇠를 하나 더 만들고 싶을 때, 우리는 열쇠 홈의 깊이를 하나하나 재서 그리지 않아요. 열쇠 가게에 원래 열쇠를 건네면, 기계가 홈을 그대로 따라 깎아서 똑같은 열쇠를 만들어 줘요. 우리는 열쇠 안이 어떻게 생겼는지 몰라도 돼요. 그리고 복사한 열쇠에 고리를 달아도 원래 열쇠는 그대로예요. 원래 열쇠가 바로 원형이에요.
누가 무엇을 하나요?
| 역할 | 이 예제에서 | 하는 일 |
|---|---|---|
| 원형의 약속 | [원형] (규정) |
<복제하기>를 약속해요 |
| 구체적인 원형 | [몬스터], [보스몬스터] |
자기와 똑같은 새 객체를 만들어 돌려줘요 |
| 원형 모음 | [몬스터도감] |
견본에 이름표를 붙여 모아 두고, 꺼낼 때마다 복제본을 줘요 |
| 쓰는 쪽 | 마지막 부분 | 이름으로 복제본을 받아 써요 |
하리로 만들기
같은 몬스터들을 프로토타입 패턴으로 다시 만들어 볼게요.
(참고: 1단계 — 원형의 약속. "나와 똑같은 것을 하나 더 만들어 줘")
[원형]을 규정하자:
<복제하기>가 있어야 한다 ()
(참고: 2단계 — 자기를 베끼는 법은 자기가 가장 잘 알아요. 기술 목록은 새 목록으로 옮겨 담아요)
[원형]을 따르는 [몬스터]를 설계하자:
'이름'을 ""으로 정하자
'체력'을 0으로 정하자
'기술들'을 []로 정하여 숨기자
처음 만들어질 때 ([문자열]인 '이름', [숫자]인 '체력', '기술들') 다음과 같이 하자:
'나'의 '이름'을 '이름'으로 정하자
'나'의 '체력'을 '체력'으로 정하자
'나'의 '기술들'을 '기술들'로 정하자
<복제하기>를 만들자 ():
'새기술들'을 []로 정하자
'나'의 '기술들'의 '기술'마다 반복하자:
'새기술들'에 '기술'을 추가하자
새로운 [몬스터]('나'의 '이름', '나'의 '체력', '새기술들')를 돌려주자
<배우기>를 만들자 ([문자열]인 '기술'):
'나'의 '기술들'에 '기술'을 추가하자
<소개하기>를 만들자 ():
틀"{'나'의 '이름'} (체력 {'나'의 '체력'}) 기술: {'나'의 '기술들'}"을 출력하자
[원형]을 따르는 [보스몬스터]를 설계하자:
'이름'을 ""으로 정하자
'체력'을 0으로 정하자
'기술들'을 []로 정하여 숨기자
'부하수'를 0으로 정하여 숨기자
처음 만들어질 때 ([문자열]인 '이름', [숫자]인 '체력', '기술들', [숫자]인 '부하수') 다음과 같이 하자:
'나'의 '이름'을 '이름'으로 정하자
'나'의 '체력'을 '체력'으로 정하자
'나'의 '기술들'을 '기술들'로 정하자
'나'의 '부하수'를 '부하수'로 정하자
(참고: 숨긴 '부하수'도 클래스 안에서는 읽을 수 있어서 그대로 베껴요)
<복제하기>를 만들자 ():
'새기술들'을 []로 정하자
'나'의 '기술들'의 '기술'마다 반복하자:
'새기술들'에 '기술'을 추가하자
새로운 [보스몬스터]('나'의 '이름', '나'의 '체력', '새기술들', '나'의 '부하수')를 돌려주자
<배우기>를 만들자 ([문자열]인 '기술'):
'나'의 '기술들'에 '기술'을 추가하자
<소개하기>를 만들자 ():
틀"{'나'의 '이름'} (체력 {'나'의 '체력'}, 부하 {'나'의 '부하수'}) 기술: {'나'의 '기술들'}"을 출력하자
(참고: 3단계 — 견본들을 이름표를 붙여 모아 두는 도감)
[몬스터도감]을 설계하자:
'견본들'을 {}로 정하여 숨기자
<등록하기>를 만들자 ([문자열]인 '이름', [원형]인 '견본'):
'나'의 '견본들'의 '이름'을 '견본'으로 정하자
<꺼내기>를 만들자 ([문자열]인 '이름'):
'견본'을 '나'의 '견본들'의 '이름'으로 정하자
'견본'의 <복제하기>()를 돌려주자
(참고: 4단계 — 견본을 한 번만 잘 맞춰서 도감에 넣어 두고, 필요할 때마다 베껴 와요)
'슬라임견본'을 새로운 [몬스터]("슬라임", 30, ["몸통박치기"])로 정하자
'도감'을 새로운 [몬스터도감]()으로 정하자
'도감'의 <등록하기>("슬라임", '슬라임견본')를 실행하자
'도감'의 <등록하기>("슬라임 왕", 새로운 [보스몬스터]("슬라임 왕", 300, ["몸통박치기", "분열"], 3))를 실행하자
'슬라임1'을 '도감'의 <꺼내기>("슬라임")로 정하자
'왕1'을 '도감'의 <꺼내기>("슬라임 왕")로 정하자
(참고: 이번에는 슬라임1만 새 기술을 배워요)
'슬라임1'의 <배우기>("점액 뿌리기")를 실행하자
'슬라임견본'의 <소개하기>()를 실행하자
'슬라임1'의 <소개하기>()를 실행하자
'왕1'의 <소개하기>()를 실행하자
실행 결과
슬라임 (체력 30) 기술: [몸통박치기]
슬라임 (체력 30) 기술: [몸통박치기, 점액 뿌리기]
슬라임 왕 (체력 300, 부하 3) 기술: [몸통박치기, 분열]
이번에는 견본이 “몸통박치기” 하나만 알고 있어요. '슬라임1'이 배운 기술은 '슬라임1'에게만 들어갔어요.
단계별로 다시 보기
- 1단계 — 원형의 약속.
[원형]은<복제하기>하나만 약속해요. 도감은 견본이 몬스터인지 보스인지 몰라도, 이 약속만 믿고 복제를 부탁해요. - 2단계 — 자기를 베끼는 법은 자기가.
[몬스터]의<복제하기>는 기술을'새기술들'이라는 새 목록에 하나씩 옮겨 담은 뒤 새 몬스터에게 넘겨요. 그래서 복제본과 견본이 목록을 함께 쓰지 않아요.[보스몬스터]는 숨긴'부하수'도 클래스 안에서 읽을 수 있어서 그대로 베껴요. 칸이 숨겨져 있어도 복제는 문제없어요. - 3단계 — 도감.
[몬스터도감]은 이름표와 견본을 사전에 모아 둬요.<꺼내기>는 견본을 찾아<복제하기>를 부르고, 견본 대신 복제본을 돌려줘요. 견본 자체는 도감 밖으로 나가지 않아요. - 4단계 — 이름만 말하기. 쓰는 쪽은
'도감'의 <꺼내기>("슬라임 왕")처럼 이름만 말해요. 생성자에 무엇을 어떤 순서로 넘기는지, 무슨 클래스인지 몰라도 돼요.
목록은 왜 새로 담아야 하나요?
'나'의 '기술들'을 그대로 넘기면 목록이 복사되지 않고, 같은 목록을 가리키는 이름표만 하나 더 생겨요. 겉만 베끼고 속은 함께 쓰는 셈이죠. 복제본이 견본과 따로 움직이려면, 목록이나 사전처럼 속이 바뀌는 것은 새로 만들어 옮겨 담아야 해요. 이렇게 속까지 새로 만드는 복제를 깊은 복제라고 해요.
복제를 바깥에 맡기면 숨긴 칸 앞에서 막혀요. 그래서 복제는 클래스 안에서 해야 해요.
[보스몬스터]를 설계하자:
'이름'을 ""으로 정하자
'부하수'를 0으로 정하여 숨기자
처음 만들어질 때 ([문자열]인 '이름', [숫자]인 '부하수') 다음과 같이 하자:
'나'의 '이름'을 '이름'으로 정하자
'나'의 '부하수'를 '부하수'로 정하자
'왕견본'을 새로운 [보스몬스터]("슬라임 왕", 3)으로 정하자
(참고: 바깥에서 칸을 옮겨 적으려 해도, 숨긴 칸은 읽을 수 없어요)
'왕1'을 새로운 [보스몬스터]('왕견본'의 '이름', '왕견본'의 '부하수')로 정하자
새 몬스터를 더해 보면
“체력 50짜리 화난 슬라임”이 필요해졌다고 해 볼게요.
- 앞의 방식: 새 견본을 만들고, 그 견본을 베끼는 곳마다 생성자에 칸을 하나하나 옮겨 적는 줄을 써야 해요.
- 프로토타입 패턴: 견본 하나를 만들어
'도감'의 <등록하기>("화난 슬라임", …)로 넣어 두면 끝이에요. 꺼내 쓰는 쪽은<꺼내기>("화난 슬라임")한 줄이에요.
직접 해 보기
- 체력 50에 “몸통박치기”, “박치기”를 아는
"화난 슬라임"견본을 도감에 등록하고, 두 마리를 꺼내 소개해 보세요.[몬스터]의<복제하기>에서'새기술들'대신'나'의 '기술들'을 그대로 넘기도록 바꿔 보세요. 그다음'슬라임1'에게 기술을 배우게 하고, 견본이 어떻게 되는지 확인해 보세요.
언제 쓰면 좋을까요?
- 설정할 것이 많은 객체를 한 번 잘 맞춰 두고 똑같이 여러 개 만들고 싶을 때
- 만들어야 할 객체의 클래스를 쓰는 쪽이 모를 때 (도감에서 이름으로 꺼내기)
- 객체에 숨긴 칸이 있어서 바깥에서는 베낄 수 없을 때
- 처음부터 만드는 것이 오래 걸리는 객체를 (파일을 읽어 맞춘 설정처럼) 빠르게 하나 더 만들고 싶을 때
좋은 점과 아쉬운 점
| 좋은 점 | 아쉬운 점 |
|---|---|
| 쓰는 쪽이 클래스나 생성자를 몰라도 똑같은 객체를 얻어요 | 클래스마다 <복제하기>를 직접 만들어야 해요 |
| 숨긴 칸까지 빠짐없이 베낄 수 있어요 | 목록 안에 객체가 있는 것처럼 속이 깊으면, 어디까지 새로 만들지 정하기 어려워요 |
| 견본을 갈아 끼우는 것만으로 새 종류를 만들 수 있어요 | 서로를 가리키는 객체들은 복제하다가 끝없이 돌 수 있어 조심해야 해요 |
너무 이른 패턴은 독이에요.
객체를 만드는 데 필요한 값이 두세 개뿐이라면, 생성자를 부르는 게 더 단순해요. 같은 설정을 여러 곳에서 똑같이 반복하고 있거나, 클래스를 모르는 채로 객체를 만들어야 할 때 프로토타입을 꺼내도 늦지 않아요.
다른 패턴과의 관계
- 팩토리 패턴과 하는 일이 같아요. 둘 다 쓰는 쪽 대신 객체를 만들어 줘요. 팩토리는 “어떤 클래스로 만들지”를 코드로 정하고, 프로토타입은 “이 견본처럼”이라고 객체를 보여 줘서 정해요.
- 메멘토 패턴과 닮은 데가 있어요. 둘 다 객체를 베끼고, 둘 다 숨긴 칸 때문에 베끼는 일을 클래스 안에서 해요. 메멘토는 “나중에 되돌리기” 위해 상태만 베끼고, 프로토타입은 “하나 더 쓰기” 위해 객체를 통째로 베껴요.
- 컴포지트 패턴과 함께 쓸 수 있어요. 상자 안에 상자가 든 구조를 복제하려면, 상자의
<복제하기>가 안에 든 것들의<복제하기>를 차례로 불러요.