18. 방문자 패턴 (Visitor)
물건 클래스는 그대로 두고, 물건들에게 하는 새 일을 방문자 클래스 하나로 더하는 방문자 패턴을 알아봅니다.
한 줄 요약
여러 종류의 물건에게 하는 일을 물건 클래스 밖의 방문자로 꺼내는 패턴이에요. 새 일이 생기면 물건은 그대로 두고 방문자 하나만 더하면 돼요.
문제: 일이 생길 때마다 모든 물건 클래스를 고치는 코드
온라인 가게의 장바구니를 만든다고 해 볼게요. 장바구니에는 종류가 다른 물건이 섞여 있어요.
- 책은 제목과 가격이 있어요.
- 과일은 무게와 1kg당 가격이 있어서, 무게를 곱해야 값이 나와요.
- 장난감은 가격과 함께 깨지기 쉬운지가 적혀 있어요.
주문이 들어오면 장바구니로 두 가지 일을 해요. 영수증을 뽑고, 물건마다 알맞게 포장해요. 가장 먼저 떠오르는 방법은 물건 클래스마다 <값매기기>와 <포장하기>를 넣는 거예요.
[책]을 설계하자:
'제목'을 ""으로 정하자
'가격'을 0으로 정하자
처음 만들어질 때 ([문자열]인 '제목', [숫자]인 '가격') 다음과 같이 하자:
'나'의 '제목'을 '제목'으로 정하자
'나'의 '가격'을 '가격'으로 정하자
(참고: 영수증 일)
<값매기기>를 만들자 ():
틀"· 책 「{'나'의 '제목'}」: {'나'의 '가격'}원"을 출력하자
'나'의 '가격'을 돌려주자
(참고: 포장 일)
<포장하기>를 만들자 ():
틀"· 「{'나'의 '제목'}」: 종이봉투에 넣어요"를 출력하자
[과일]을 설계하자:
'이름'을 ""으로 정하자
'무게'를 0으로 정하자
'킬로당가격'을 0으로 정하자
처음 만들어질 때 ([문자열]인 '이름', [숫자]인 '무게', [숫자]인 '킬로당가격') 다음과 같이 하자:
'나'의 '이름'을 '이름'으로 정하자
'나'의 '무게'를 '무게'로 정하자
'나'의 '킬로당가격'을 '킬로당가격'으로 정하자
<값매기기>를 만들자 ():
'값'을 ('나'의 '무게' * '나'의 '킬로당가격')으로 정하자
틀"· {'나'의 '이름'} {'나'의 '무게'}kg: {'값'}원"을 출력하자
'값'을 돌려주자
<포장하기>를 만들자 ():
틀"· {'나'의 '이름'}: 얼음팩과 함께 상자에 넣어요"를 출력하자
[장난감]을 설계하자:
'이름'을 ""으로 정하자
'가격'을 0으로 정하자
'깨지나'를 거짓으로 정하자
처음 만들어질 때 ([문자열]인 '이름', [숫자]인 '가격', '깨지나') 다음과 같이 하자:
'나'의 '이름'을 '이름'으로 정하자
'나'의 '가격'을 '가격'으로 정하자
'나'의 '깨지나'를 '깨지나'로 정하자
<값매기기>를 만들자 ():
틀"· {'나'의 '이름'}: {'나'의 '가격'}원"을 출력하자
'나'의 '가격'을 돌려주자
<포장하기>를 만들자 ():
만약 ('나'의 '깨지나') 라면:
틀"· {'나'의 '이름'}: 뽁뽁이로 감싸서 넣어요"를 출력하자
그렇지 않다면:
틀"· {'나'의 '이름'}: 그냥 상자에 넣어요"를 출력하자
'장바구니'를 [새로운 [책]("어린 왕자", 9000), 새로운 [과일]("사과", 2, 4000), 새로운 [장난감]("유리 구슬", 3000, 참)]로 정하자
"[영수증]"을 출력하자
'합계'를 0으로 정하자
'장바구니'의 '물건'마다 반복하자:
'합계'를 ('합계' + '물건'의 <값매기기>())로 정하자
틀"합계: {'합계'}원"을 출력하자
"[포장]"을 출력하자
'장바구니'의 '물건'마다 반복하자:
'물건'의 <포장하기>()를 실행하자
실행 결과
[영수증]
· 책 「어린 왕자」: 9000원
· 사과 2kg: 8000원
· 유리 구슬: 3000원
합계: 20000원
[포장]
· 「어린 왕자」: 종이봉투에 넣어요
· 사과: 얼음팩과 함께 상자에 넣어요
· 유리 구슬: 뽁뽁이로 감싸서 넣어요
잘 돌아가요. 그런데 가게가 커지면 아파져요.
- 새 일이 생길 때마다 모든 물건 클래스를 고쳐야 해요. “배송 무게 재기”가 생기면
[책],[과일],[장난감]을 모두 열어<무게재기>를 하나씩 넣어야 해요. 물건 종류가 열 개라면 열 곳을 고쳐야 하죠. - 물건 클래스에 남의 일이 쌓여요. 책은 책일 뿐인데, 계산대의 일(값 매기기)과 창고의 일(포장하기)이 모두 책 안에 들어 있어요. 일이 늘수록 물건 클래스가 점점 뚱뚱해져요.
- 한 가지 일이 여러 클래스에 흩어져 있어요. 포장 규칙이 어떻게 되는지 보려면 물건 클래스를 하나하나 열어 봐야 해요. 포장 담당자가 고칠 곳도 여러 군데예요.
해결책: 일을 방문자에게 맡기기
방문자 패턴은 물건에게 하는 일을 물건 밖으로 꺼내, 일마다 방문자 클래스 하나에 모아요.
- 방문자의 약속을 정해요. 물건 종류마다 “이 물건을 만나면 할 일”을 하나씩 약속해요.
- 물건은 자기 정보만 들고 있어요. 방문자가 찾아오면
<맞이하기>하나로, “나는 책이에요”라며 방문자의 알맞은 메서드를 불러 줘요. - 일마다 방문자 하나를 만들어요. 영수증 일은
[계산원]에, 포장 일은[포장원]에 모아요. - 쓰는 쪽은 방문자를 데리고 장바구니를 한 바퀴 돌아요.
비유: 집집마다 찾아오는 검침원
아파트에는 집마다 모양이 다른 계량기가 있어요. 전기 검침원, 가스 검침원, 수도 검침원이 차례로 찾아오지만, 집은 계량기 읽는 법을 몰라도 돼요. 문을 열어 “우리 집은 이렇게 생겼어요”라고 맞이하기만 하면, 검침원이 자기 방법대로 읽어 가요. 새로 “인터넷 점검원”이 생겨도 집을 고칠 필요가 없어요. 점검원 한 명이 새로 다니기만 하면 돼요. 검침원이 바로 방문자예요.
누가 무엇을 하나요?
| 역할 | 이 예제에서 | 하는 일 |
|---|---|---|
| 방문자 약속 | [방문자] (규정) |
물건 종류마다 <책만나기>, <과일만나기>, <장난감만나기>를 약속해요 |
| 구체적인 방문자 | [계산원], [포장원] |
한 가지 일을 물건 종류마다 어떻게 할지 모아 둬요 |
| 물건들 | [책], [과일], [장난감] |
자기 정보를 들고, <맞이하기>로 방문자에게 자기 종류를 알려 줘요 |
| 쓰는 쪽 | 마지막 부분 | 방문자를 만들어 물건들을 하나씩 맞이하게 해요 |
하리로 만들기
같은 장바구니를 방문자 패턴으로 다시 만들어 볼게요.
(참고: 1단계 — 방문자의 약속. 물건 종류마다 "이 물건을 만나면 할 일"이 하나씩 있어요)
[방문자]를 규정하자:
<책만나기>가 있어야 한다 ([책]인 '책')
<과일만나기>가 있어야 한다 ([과일]인 '과일')
<장난감만나기>가 있어야 한다 ([장난감]인 '장난감')
(참고: 2단계 — 물건은 자기 정보만 들고, 찾아온 방문자에게 "나는 책이에요"라고 알려 줘요)
[책]을 설계하자:
'제목'을 ""으로 정하자
'가격'을 0으로 정하자
처음 만들어질 때 ([문자열]인 '제목', [숫자]인 '가격') 다음과 같이 하자:
'나'의 '제목'을 '제목'으로 정하자
'나'의 '가격'을 '가격'으로 정하자
<맞이하기>를 만들자 ([방문자]인 '방문자'):
'방문자'의 <책만나기>('나')를 실행하자
[과일]을 설계하자:
'이름'을 ""으로 정하자
'무게'를 0으로 정하자
'킬로당가격'을 0으로 정하자
처음 만들어질 때 ([문자열]인 '이름', [숫자]인 '무게', [숫자]인 '킬로당가격') 다음과 같이 하자:
'나'의 '이름'을 '이름'으로 정하자
'나'의 '무게'를 '무게'로 정하자
'나'의 '킬로당가격'을 '킬로당가격'으로 정하자
<맞이하기>를 만들자 ([방문자]인 '방문자'):
'방문자'의 <과일만나기>('나')를 실행하자
[장난감]을 설계하자:
'이름'을 ""으로 정하자
'가격'을 0으로 정하자
'깨지나'를 거짓으로 정하자
처음 만들어질 때 ([문자열]인 '이름', [숫자]인 '가격', '깨지나') 다음과 같이 하자:
'나'의 '이름'을 '이름'으로 정하자
'나'의 '가격'을 '가격'으로 정하자
'나'의 '깨지나'를 '깨지나'로 정하자
<맞이하기>를 만들자 ([방문자]인 '방문자'):
'방문자'의 <장난감만나기>('나')를 실행하자
(참고: 3단계 — 일마다 방문자 하나. 영수증 일은 모두 계산원에게 모여 있어요)
[방문자]를 따르는 [계산원]을 설계하자:
'합계'를 0으로 정하자
<책만나기>를 만들자 ([책]인 '책'):
틀"· 책 「{'책'의 '제목'}」: {'책'의 '가격'}원"을 출력하자
'나'의 '합계'를 ('나'의 '합계' + '책'의 '가격')으로 정하자
<과일만나기>를 만들자 ([과일]인 '과일'):
'값'을 ('과일'의 '무게' * '과일'의 '킬로당가격')으로 정하자
틀"· {'과일'의 '이름'} {'과일'의 '무게'}kg: {'값'}원"을 출력하자
'나'의 '합계'를 ('나'의 '합계' + '값')으로 정하자
<장난감만나기>를 만들자 ([장난감]인 '장난감'):
틀"· {'장난감'의 '이름'}: {'장난감'의 '가격'}원"을 출력하자
'나'의 '합계'를 ('나'의 '합계' + '장난감'의 '가격')으로 정하자
(참고: 포장 일은 모두 포장원에게 모여 있어요)
[방문자]를 따르는 [포장원]을 설계하자:
<책만나기>를 만들자 ([책]인 '책'):
틀"· 「{'책'의 '제목'}」: 종이봉투에 넣어요"를 출력하자
<과일만나기>를 만들자 ([과일]인 '과일'):
틀"· {'과일'의 '이름'}: 얼음팩과 함께 상자에 넣어요"를 출력하자
<장난감만나기>를 만들자 ([장난감]인 '장난감'):
만약 ('장난감'의 '깨지나') 라면:
틀"· {'장난감'의 '이름'}: 뽁뽁이로 감싸서 넣어요"를 출력하자
그렇지 않다면:
틀"· {'장난감'의 '이름'}: 그냥 상자에 넣어요"를 출력하자
(참고: 4단계 — 방문자를 데리고 장바구니를 한 바퀴 돌아요)
'장바구니'를 [새로운 [책]("어린 왕자", 9000), 새로운 [과일]("사과", 2, 4000), 새로운 [장난감]("유리 구슬", 3000, 참)]로 정하자
"[영수증]"을 출력하자
'계산원'을 새로운 [계산원]()으로 정하자
'장바구니'의 '물건'마다 반복하자:
'물건'의 <맞이하기>('계산원')를 실행하자
틀"합계: {'계산원'의 '합계'}원"을 출력하자
"[포장]"을 출력하자
'포장원'을 새로운 [포장원]()으로 정하자
'장바구니'의 '물건'마다 반복하자:
'물건'의 <맞이하기>('포장원')를 실행하자
실행 결과
[영수증]
· 책 「어린 왕자」: 9000원
· 사과 2kg: 8000원
· 유리 구슬: 3000원
합계: 20000원
[포장]
· 「어린 왕자」: 종이봉투에 넣어요
· 사과: 얼음팩과 함께 상자에 넣어요
· 유리 구슬: 뽁뽁이로 감싸서 넣어요
결과는 리팩터링 전과 한 줄도 다르지 않아요. 하지만 이제 물건 클래스에는 자기 정보와 <맞이하기>만 남았고, 영수증 일은 [계산원]에, 포장 일은 [포장원]에 한데 모여 있어요.
단계별로 다시 보기
- 1단계 — 방문자의 약속.
[방문자]는 물건 종류마다 메서드를 하나씩 약속해요. 그래서 방문자를 만들 때 어떤 물건도 빠뜨릴 수 없어요. - 2단계 — 물건은 맞이하기만 해요.
[책]의<맞이하기>는'방문자'의 <책만나기>('나')를 부를 뿐이에요. 찾아온 방문자가 계산원인지 포장원인지 몰라도 돼요. 대신 “나는 책이다”라는 것은 책이 가장 잘 알기 때문에, 알맞은 메서드는 책이 골라 줘요. - 3단계 — 일마다 방문자 하나.
[계산원]을 열면 영수증 규칙이 물건 종류별로 한눈에 보여요. 합계도 계산원이'합계'에 쌓아 가요. 포장 규칙은[포장원]한 곳에만 있어요. - 4단계 — 한 바퀴 돌기. 쓰는 쪽은 물건이 무슨 종류인지 따지지 않아요. 모든 물건에게 똑같이
<맞이하기>를 부르면, 물건이 알아서 방문자의 알맞은 메서드로 이어 줘요.
왜 두 번 거쳐 가나요?
쓰는 쪽이 바로'계산원'의 <책만나기>('물건')를 부르려면, 그 물건이 책인지 먼저 알아야 해요. 그러면 다시 “책이면… 과일이면…“을 따지게 되죠. 물건이<맞이하기>에서 자기에게 맞는 메서드를 골라 주면, 쓰는 쪽은 종류를 몰라도 돼요. 물건이 한 번, 방문자가 한 번 골라서 “어떤 물건 × 어떤 일”이 딱 맞게 정해져요.
방문자는 약속에 있는 물건을 모두 다뤄야 해서, 하나라도 빠뜨리면 그 자리에서 막혀요.
[방문자]를 규정하자:
<책만나기>가 있어야 한다 ('책')
<과일만나기>가 있어야 한다 ('과일')
<장난감만나기>가 있어야 한다 ('장난감')
(참고: 무게를 재는 방문자를 새로 만들다가 장난감을 깜빡했어요)
[방문자]를 따르는 [무게측정원]을 설계하자:
<책만나기>를 만들자 ('책'):
"· 책: 0.3kg"을 출력하자
<과일만나기>를 만들자 ('과일'):
틀"· 과일: {'과일'의 '무게'}kg"을 출력하자
'측정원'을 새로운 [무게측정원]()으로 정하자
새 일을 더해 보면
“배송 무게 재기”가 생겼다고 해 볼게요.
- 앞의 방식:
[책],[과일],[장난감]을 모두 열어서<무게재기>를 하나씩 넣어야 해요. - 방문자 패턴:
[방문자]를 따르는[무게측정원]클래스를 하나 새로 만들면 돼요. 물건 클래스는 한 줄도 고치지 않아요.
직접 해 보기
- 책은 0.3kg, 과일은 자기 무게, 장난감은 0.5kg으로 재는
[무게측정원]을 만들고, 장바구니 전체의 배송 무게를 출력해 보세요.- 이번에는 새 물건
[화분]을 더해 보세요. 그러려면[방문자]에<화분만나기>를 더하고,[계산원]과[포장원]에도 모두 넣어야 해요. 새 일을 더하기는 쉽지만, 새 물건을 더하기는 어렵다는 것을 느껴 보세요.
언제 쓰면 좋을까요?
- 물건의 종류는 잘 바뀌지 않는데, 그 물건들에게 하는 일은 자꾸 늘어날 때
- 한 가지 일의 규칙을 한곳에 모아 보고 싶을 때 (계산 규칙, 포장 규칙, 출력 규칙…)
- 물건 클래스에 물건과 상관없는 일이 쌓여 가고 있을 때
- 문서의 문단·표·그림, 프로그램 코드의 문장·식처럼 종류가 정해진 구조 전체를 한 바퀴 돌며 일할 때
좋은 점과 아쉬운 점
| 좋은 점 | 아쉬운 점 |
|---|---|
| 새 일을 더해도 물건 클래스는 그대로예요 | 새 물건 종류를 더하면 모든 방문자를 고쳐야 해요 |
| 한 가지 일의 규칙이 방문자 한 곳에 모여요 | 방문자가 물건의 정보를 읽어야 해서, 물건의 속을 감추기 어려워요 |
| 방문자가 돌면서 합계 같은 값을 모아 둘 수 있어요 | <맞이하기>를 거쳐 두 번 부르는 흐름이 처음에는 낯설어요 |
너무 이른 패턴은 독이에요.
일이 한두 가지뿐이고 더 늘어날 것 같지 않다면, 물건 클래스에 메서드를 두는 게 더 단순해요. 물건 종류가 자주 바뀐다면 방문자는 오히려 짐이 돼요. 일이 계속 늘어나고, 고칠 때마다 물건 클래스를 줄줄이 열게 될 때 방문자를 꺼내도 늦지 않아요.