15. 반복자 패턴 (Iterator)
모음의 속이 어떻게 생겼든 같은 방법으로 하나씩 꺼낼 수 있게 해 주는 반복자 패턴을 알아봅니다.
한 줄 요약
모음(목록, 목록 안의 목록, 나무 등)의 속이 어떻게 생겼는지 보여 주지 않고, “남았니?“와 “다음 거 줘” 두 가지만으로 하나씩 꺼낼 수 있게 해 주는 패턴이에요.
문제: 모음의 속을 다 알아야 쓸 수 있는 코드
음악 앱을 만든다고 해 볼게요. 곡을 모아 두는 방법이 두 가지 있어요.
- 재생목록은 곡을 목록 하나에 차례로 담아요.
- 앨범장은 앨범 여러 개를 담고, 앨범마다 곡 목록이 있어요. 목록 안에 목록이 있는 셈이에요.
두 모음 모두 “처음부터 끝까지 틀기”와 “앞의 몇 곡만 미리듣기”를 하고 싶어요. 가장 먼저 떠오르는 방법은 쓰는 쪽에서 모음의 속을 직접 도는 거예요.
[재생목록]을 설계하자:
'곡들'을 []로 정하자
<넣기>를 만들자 ([문자열]인 '곡'):
'나'의 '곡들'에 '곡'을 추가하자
[앨범]을 설계하자:
'이름'을 [문자열]인 ""으로 정하자
'곡들'을 []로 정하자
처음 만들어질 때 ([문자열]인 '이름', '곡들') 다음과 같이 하자:
'나'의 '이름'을 '이름'으로 정하자
'나'의 '곡들'을 '곡들'로 정하자
[앨범장]을 설계하자:
'앨범들'을 []로 정하자
<넣기>를 만들자 ([앨범]인 '앨범'):
'나'의 '앨범들'에 '앨범'을 추가하자
(참고: 쓰는 쪽이 모음의 속을 알아야 해요. 재생목록은 목록 하나, 앨범장은 목록 안의 목록이에요)
<재생목록틀기>를 만들자 ('목록'):
'번호'를 1로 정하자
'목록'의 '곡들'의 '곡'마다 반복하자:
틀"{'번호'}. {'곡'}"을 출력하자
'번호'를 ('번호' + 1)로 정하자
<앨범장틀기>를 만들자 ('장'):
'번호'를 1로 정하자
'장'의 '앨범들'의 '앨범'마다 반복하자:
'앨범'의 '곡들'의 '곡'마다 반복하자:
틀"{'번호'}. {'곡'}"을 출력하자
'번호'를 ('번호' + 1)로 정하자
(참고: "앞의 몇 곡만 미리듣기"도 모음마다 따로 만들어요. 반복이 두 겹이면 '반복을 끝내자'로는 안쪽만 끝나서, 돌려주자로 빠져나와요)
<재생목록미리듣기>를 만들자 ('목록', [숫자]인 '몇곡'):
'들은수'를 0으로 정하자
'목록'의 '곡들'의 '곡'마다 반복하자:
만약 ('들은수'가 '몇곡'과 같다) 라면:
반복을 끝내자
틀"미리듣기: {'곡'}"을 출력하자
'들은수'를 ('들은수' + 1)로 정하자
<앨범장미리듣기>를 만들자 ('장', [숫자]인 '몇곡'):
'들은수'를 0으로 정하자
'장'의 '앨범들'의 '앨범'마다 반복하자:
'앨범'의 '곡들'의 '곡'마다 반복하자:
만약 ('들은수'가 '몇곡'과 같다) 라면:
돌려주자
틀"미리듣기: {'곡'}"을 출력하자
'들은수'를 ('들은수' + 1)로 정하자
'출근길'을 새로운 [재생목록]()으로 정하자
'출근길'의 <넣기>("아침 햇살")을 실행하자
'출근길'의 <넣기>("달리는 버스")를 실행하자
'출근길'의 <넣기>("커피 한 잔")을 실행하자
'내앨범'을 새로운 [앨범장]()으로 정하자
'내앨범'의 <넣기>(새로운 [앨범]("봄", ["벚꽃", "새싹"]))을 실행하자
'내앨범'의 <넣기>(새로운 [앨범]("여름", ["바다", "소나기", "수박"]))을 실행하자
<재생목록틀기>('출근길')을 실행하자
<앨범장틀기>('내앨범')을 실행하자
<재생목록미리듣기>('출근길', 2)를 실행하자
<앨범장미리듣기>('내앨범', 3)을 실행하자
실행 결과\
- 아침 햇살\
- 달리는 버스\
- 커피 한 잔\
- 벚꽃\
- 새싹\
- 바다\
- 소나기\
- 수박
미리듣기: 아침 햇살
미리듣기: 달리는 버스
미리듣기: 벚꽃
미리듣기: 새싹
미리듣기: 바다
잘 돌아가요. 그런데 모음과 할 일이 늘어나면 아파져요.
- 쓰는 쪽이 모음의 속을 다 알아요.
<앨범장틀기>는 앨범장 안에'앨범들'이 있고, 앨범마다'곡들'이 있다는 것까지 알아야 해요. 곡을 꺼내고 싶을 뿐인데요. - 할 일과 모음의 가짓수를 곱한 만큼 함수가 생겨요. 할 일 두 가지(틀기, 미리듣기)에 모음 두 가지라서 함수가 넷이에요. “곡 수 세기”를 더하면 둘이 또 늘고, “추천 목록” 같은 새 모음이 생기면 할 일마다 하나씩 또 늘어요.
- 속을 바꾸면 쓰는 쪽이 모두 깨져요. 앨범장이 앨범을 사전에 담도록 바뀌면,
'앨범들'을 도는 함수를 전부 고쳐야 해요. - 속을 숨길 수 없어요. 쓰는 쪽이
'곡들'을 직접 꺼내야 하니숨기자를 붙일 수 없어요. 그러면 아무나'곡들'에 엉뚱한 것을 넣을 수도 있어요. - 반복이 두 겹이면 멈추기도 어색해요.
반복을 끝내자는 가장 안쪽 반복만 끝내서,<앨범장미리듣기>는돌려주자로 함수째 빠져나와야 했어요.
해결책: “하나씩 꺼내기”를 맡는 객체 따로 두기
반복자 패턴은 모음을 도는 일을 반복자라는 객체에게 맡겨요.
- “하나씩 꺼내기”의 약속을 정해요.
<남았나>(아직 남은 게 있니?)와<다음>(다음 것을 줘) 두 가지면 돼요. - 모음마다 자기 속에 맞는 반복자를 만들어요. 재생목록의 반복자는 목록에서 몇 번째까지 꺼냈는지 기억하고, 앨범장의 반복자는 몇 번째 앨범의 몇 번째 곡까지 꺼냈는지 기억해요.
- 모음은 반복자를 하나 만들어 주는 일만 해요. 속은 숨겨 둬요.
- 쓰는 쪽은 반복자에게 “남았니? 다음 거 줘”만 되풀이해요. 상대가 재생목록인지 앨범장인지 몰라요.
비유: 책갈피
책을 읽을 때 우리는 책이 몇 쪽씩 묶여 제본됐는지 몰라도 돼요. 책갈피가 “여기까지 읽었어요”를 기억해 주니까, 책갈피 다음 쪽을 펼치기만 하면 돼요. 같은 책에 책갈피를 두 개 꽂으면 두 사람이 각자 다른 곳까지 읽을 수도 있고요. 반복자는 모음에 꽂는 책갈피예요.
누가 무엇을 하나요?
| 역할 | 이 예제에서 | 하는 일 |
|---|---|---|
| 반복자 약속 | [반복자] (규정) |
<남았나>와 <다음>을 정해요 |
| 구체적인 반복자 | [목록반복자], [앨범장반복자] |
자기 모음의 속을 알고, 어디까지 꺼냈는지 기억해요 |
| 모음 약속 | [곡모음] (규정) |
반복자를 하나 만들어 줄 수 있어야 해요 |
| 구체적인 모음 | [재생목록], [앨범장] |
속을 숨기고, 자기에게 맞는 반복자를 만들어 줘요 |
| 쓰는 쪽 | <틀기>, <미리듣기> |
반복자에게 묻고 받기만 해요 |
하리로 만들기
같은 음악 앱을 반복자 패턴으로 다시 만들어 볼게요.
(참고: 1단계 — "하나씩 꺼내기"의 약속. 남았는지 묻고, 다음 것을 받아요)
[반복자]를 규정하자:
[논리]를 돌려주는 <남았나>가 있어야 한다 ()
[문자열]을 돌려주는 <다음>이 있어야 한다 ()
(참고: 모음의 약속. 속이 어떻든 반복자를 하나 만들어 줄 수 있으면 돼요)
[곡모음]을 규정하자:
[반복자]를 돌려주는 <차례로>가 있어야 한다 ()
(참고: 2단계 — 재생목록과 그 반복자. 반복자가 "지금 어디까지 꺼냈나"를 기억해요)
[반복자]를 따르는 [목록반복자]를 설계하자:
'곡들'을 []로 정하여 숨기자
'위치'를 [숫자]인 1로 정하여 숨기자
처음 만들어질 때 ('곡들') 다음과 같이 하자:
'나'의 '곡들'을 '곡들'로 정하자
[논리]를 돌려주는 <남았나>를 만들자 ():
('나'의 '위치' <= '나'의 '곡들'의 '길이')를 돌려주자
[문자열]을 돌려주는 <다음>을 만들자 ():
'자리'를 '나'의 '위치'로 정하자
'곡'을 '나'의 '곡들'의 '자리'번째로 정하자
'나'의 '위치'를 ('나'의 '위치' + 1)로 정하자
'곡'을 돌려주자
[곡모음]을 따르는 [재생목록]을 설계하자:
'곡들'을 []로 정하여 숨기자
<넣기>를 만들자 ([문자열]인 '곡'):
'나'의 '곡들'에 '곡'을 추가하자
[반복자]를 돌려주는 <차례로>를 만들자 ():
새로운 [목록반복자]('나'의 '곡들')을 돌려주자
(참고: 3단계 — 앨범장과 그 반복자. 목록 안의 목록을 도는 일을 반복자가 맡아요)
[앨범]을 설계하자:
'이름'을 [문자열]인 ""으로 정하자
'곡들'을 []로 정하자
처음 만들어질 때 ([문자열]인 '이름', '곡들') 다음과 같이 하자:
'나'의 '이름'을 '이름'으로 정하자
'나'의 '곡들'을 '곡들'로 정하자
[반복자]를 따르는 [앨범장반복자]를 설계하자:
'앨범들'을 []로 정하여 숨기자
'앨범위치'를 [숫자]인 1로 정하여 숨기자
'곡위치'를 [숫자]인 1로 정하여 숨기자
처음 만들어질 때 ('앨범들') 다음과 같이 하자:
'나'의 '앨범들'을 '앨범들'로 정하자
[논리]를 돌려주는 <남았나>를 만들자 ():
(참고: 지금 앨범을 다 꺼냈으면 다음 앨범의 첫 곡으로 넘어가요. 빈 앨범은 건너뛰어요)
('나'의 '앨범위치' <= '나'의 '앨범들'의 '길이')인 동안 반복하자:
'몇번째앨범'을 '나'의 '앨범위치'로 정하자
'앨범'을 '나'의 '앨범들'의 '몇번째앨범'번째로 정하자
만약 ('나'의 '곡위치' <= '앨범'의 '곡들'의 '길이') 라면:
참을 돌려주자
'나'의 '앨범위치'를 ('나'의 '앨범위치' + 1)로 정하자
'나'의 '곡위치'를 1로 정하자
거짓을 돌려주자
[문자열]을 돌려주는 <다음>을 만들자 ():
'몇번째앨범'을 '나'의 '앨범위치'로 정하자
'몇번째곡'을 '나'의 '곡위치'로 정하자
'앨범'을 '나'의 '앨범들'의 '몇번째앨범'번째로 정하자
'곡'을 '앨범'의 '곡들'의 '몇번째곡'번째로 정하자
'나'의 '곡위치'를 ('나'의 '곡위치' + 1)로 정하자
'곡'을 돌려주자
[곡모음]을 따르는 [앨범장]을 설계하자:
'앨범들'을 []로 정하여 숨기자
<넣기>를 만들자 ([앨범]인 '앨범'):
'나'의 '앨범들'에 '앨범'을 추가하자
[반복자]를 돌려주는 <차례로>를 만들자 ():
새로운 [앨범장반복자]('나'의 '앨범들')을 돌려주자
(참고: 4단계 — 쓰는 쪽은 모음의 속을 몰라요. 남았는지 묻고 다음을 받을 뿐이에요)
<틀기>를 만들자 ([곡모음]인 '모음'):
'꺼내기'를 '모음'의 <차례로>()로 정하자
'번호'를 1로 정하자
('꺼내기'의 <남았나>()가 참과 같다)인 동안 반복하자:
틀"{'번호'}. {'꺼내기'의 <다음>()}"을 출력하자
'번호'를 ('번호' + 1)로 정하자
<미리듣기>를 만들자 ([곡모음]인 '모음', [숫자]인 '몇곡'):
'꺼내기'를 '모음'의 <차례로>()로 정하자
'들은수'를 0으로 정하자
(('꺼내기'의 <남았나>()가 참과 같다) 그리고 ('들은수' < '몇곡'))인 동안 반복하자:
틀"미리듣기: {'꺼내기'의 <다음>()}"을 출력하자
'들은수'를 ('들은수' + 1)로 정하자
'출근길'을 새로운 [재생목록]()으로 정하자
'출근길'의 <넣기>("아침 햇살")을 실행하자
'출근길'의 <넣기>("달리는 버스")를 실행하자
'출근길'의 <넣기>("커피 한 잔")을 실행하자
'내앨범'을 새로운 [앨범장]()으로 정하자
'내앨범'의 <넣기>(새로운 [앨범]("봄", ["벚꽃", "새싹"]))을 실행하자
'내앨범'의 <넣기>(새로운 [앨범]("여름", ["바다", "소나기", "수박"]))을 실행하자
<틀기>('출근길')을 실행하자
<틀기>('내앨범')을 실행하자
<미리듣기>('출근길', 2)를 실행하자
<미리듣기>('내앨범', 3)을 실행하자
실행 결과\
- 아침 햇살\
- 달리는 버스\
- 커피 한 잔\
- 벚꽃\
- 새싹\
- 바다\
- 소나기\
- 수박
미리듣기: 아침 햇살
미리듣기: 달리는 버스
미리듣기: 벚꽃
미리듣기: 새싹
미리듣기: 바다
결과는 리팩터링 전과 한 줄도 다르지 않아요. 하지만 이제 할 일마다 함수가 하나씩뿐이고, 모음의 속은 모두 숨겨져 있어요.
단계별로 다시 보기
- 1단계 — 하나씩 꺼내기의 약속.
[반복자]는<남았나>와<다음>두 가지만 약속해요.[곡모음]은 “반복자를 만들어 줄 수 있으면 곡모음”이라는 약속이에요. - 2단계 — 목록을 도는 반복자.
[목록반복자]는 곡 목록과'위치'를 가지고 있어요.<다음>을 부를 때마다 지금 위치의 곡을 주고 위치를 한 칸 옮겨요. 이 “어디까지 꺼냈나”를 반복자가 기억한다는 게 중요해요. - 3단계 — 두 겹을 도는 반복자.
[앨범장반복자]는 앨범 위치와 곡 위치를 함께 기억해요. 지금 앨범을 다 꺼냈으면<남았나>가 다음 앨범의 첫 곡으로 넘어가요. 두 겹 반복이 반복자 안에 숨었어요. - 4단계 — 쓰는 쪽은 묻고 받기만.
<틀기>와<미리듣기>는[곡모음]을 받아서<차례로>()로 반복자를 얻고,<남았나>가 참인 동안<다음>을 받아요. 재생목록이든 앨범장이든 같은 함수 하나로 돼요. 미리듣기도 조건 하나로 멈춰서, 두 겹 반복을 빠져나올 걱정이 없어요.
반복자를 두 개 만들면요?
<차례로>()를 부를 때마다 새 반복자가 생기고, 위치는 반복자마다 따로예요. 그래서 한 재생목록을 두 사람이 따로따로 넘겨 가며 들을 수 있어요. 책 한 권에 책갈피 두 개를 꽂는 것과 같아요.
이제 모음의 속은 숨기자로 감출 수 있어요. 쓰는 쪽이 예전처럼 속을 직접 꺼내려 하면 그 자리에서 막혀요.
[재생목록]을 설계하자:
'곡들'을 []로 정하여 숨기자
<넣기>를 만들자 ([문자열]인 '곡'):
'나'의 '곡들'에 '곡'을 추가하자
'출근길'을 새로운 [재생목록]()으로 정하자
'출근길'의 <넣기>("아침 햇살")을 실행하자
(참고: 쓰는 쪽이 속을 직접 꺼내려 하면 막혀요)
'출근길'의 '곡들'의 '곡'마다 반복하자:
'곡'을 출력하자
새 모음을 더해 보면
“추천 목록”이라는 새 모음이 생겼다고 해 볼게요. 속은 또 다르게 생겼어요.
- 앞의 방식:
<추천목록틀기>,<추천목록미리듣기>처럼 할 일마다 함수를 하나씩 또 만들어야 해요. - 반복자 패턴:
[곡모음]을 따르는 [추천목록]과 그 반복자 하나씩만 만들면 돼요.<틀기>와<미리듣기>는 고치지 않아도 그대로 추천 목록을 틀어요.
거꾸로 새 할 일이 생겨도(곡 수 세기 같은) 반복자를 쓰는 함수를 하나만 만들면 모든 모음에서 돼요.
직접 해 보기
[재생목록]에 끝에서부터 꺼내는<거꾸로>를 더해 보세요.'위치'가 마지막 곡에서 시작해 하나씩 줄어드는[거꾸로반복자]를 만들고,<거꾸로>가 그것을 돌려주면 돼요. 그다음[반복자]를 받아서 트는 함수를 만들어'출근길'의 <거꾸로>()를 넘겨 보세요.3. 커피 한 잔부터가 아니라1. 커피 한 잔부터 나오면 성공이에요.
하리의 마다 반복하자와의 관계
'목록'의 '곡'마다 반복하자는 목록에서 하나씩 꺼내 주는 일을 하리가 대신 해 줘요. 반복자 패턴과 같은 생각이에요. 다만 마다 반복하자는 목록, 문자열, 사전처럼 하리가 이미 아는 모음만 돌 수 있어요. 직접 만든 [앨범장]처럼 속이 특별한 모음은, 이 페이지처럼 반복자를 만들어 같은 방법으로 꺼낼 수 있게 해 줘요.
언제 쓰면 좋을까요?
- 모음의 속을 숨기고 싶을 때
- 속이 서로 다른 모음들을 같은 방법으로 돌고 싶을 때
- 한 모음을 여러 순서로 돌고 싶을 때 (차례로, 거꾸로, 건너뛰며)
- 같은 모음을 여러 곳에서 따로 돌아야 할 때
좋은 점과 아쉬운 점
| 좋은 점 | 아쉬운 점 |
|---|---|
| 쓰는 쪽이 모음의 속을 몰라도 돼요 | 목록 하나뿐이라면 마다 반복하자보다 길고 번거로워요 |
| 도는 방법이 반복자 안에 모여서, 할 일마다 복사되지 않아요 | 반복자가 위치를 기억하는 동안 모음이 바뀌면 엉뚱한 곡을 줄 수 있어요 |
| 새 모음이나 새 순서를 더해도 쓰는 쪽은 그대로예요 | 클래스가 모음마다 하나씩 더 늘어요 |
| 반복자마다 위치가 따로라서 여러 곳에서 동시에 돌 수 있어요 |
너무 이른 패턴은 독이에요.
모음이 목록 하나뿐이고 속을 숨길 이유도 없다면마다 반복하자면 충분해요. 속이 다른 모음이 둘 이상 생기거나, 같은 모음을 여러 순서로 돌아야 할 때 반복자를 꺼내도 늦지 않아요.