하리Hari

15. 반복자 패턴 (Iterator)

모음의 속이 어떻게 생겼든 같은 방법으로 하나씩 꺼낼 수 있게 해 주는 반복자 패턴을 알아봅니다.

한 줄 요약
모음(목록, 목록 안의 목록, 나무 등)의 속이 어떻게 생겼는지 보여 주지 않고, “남았니?“와 “다음 거 줘” 두 가지만으로 하나씩 꺼낼 수 있게 해 주는 패턴이에요.

문제: 모음의 속을 다 알아야 쓸 수 있는 코드

음악 앱을 만든다고 해 볼게요. 곡을 모아 두는 방법이 두 가지 있어요.

  • 재생목록은 곡을 목록 하나에 차례로 담아요.
  • 앨범장은 앨범 여러 개를 담고, 앨범마다 곡 목록이 있어요. 목록 안에 목록이 있는 셈이에요.

두 모음 모두 “처음부터 끝까지 틀기”와 “앞의 몇 곡만 미리듣기”를 하고 싶어요. 가장 먼저 떠오르는 방법은 쓰는 쪽에서 모음의 속을 직접 도는 거예요.

[재생목록]을 설계하자:
    '곡들'을 []로 정하자

    <넣기>를 만들자 ([문자열]인 '곡'):
        '나'의 '곡들'에 '곡'을 추가하자

[앨범]을 설계하자:
    '이름'을 [문자열]인 ""으로 정하자
    '곡들'을 []로 정하자

    처음 만들어질 때 ([문자열]인 '이름', '곡들') 다음과 같이 하자:
        '나'의 '이름'을 '이름'으로 정하자
        '나'의 '곡들'을 '곡들'로 정하자

[앨범장]을 설계하자:
    '앨범들'을 []로 정하자

    <넣기>를 만들자 ([앨범]인 '앨범'):
        '나'의 '앨범들'에 '앨범'을 추가하자

(참고: 쓰는 쪽이 모음의 속을 알아야 해요. 재생목록은 목록 하나, 앨범장은 목록 안의 목록이에요)
<재생목록틀기>를 만들자 ('목록'):
    '번호'를 1로 정하자
    '목록'의 '곡들'의 '곡'마다 반복하자:
        틀"{'번호'}. {'곡'}"을 출력하자
        '번호'를 ('번호' + 1)로 정하자

<앨범장틀기>를 만들자 ('장'):
    '번호'를 1로 정하자
    '장'의 '앨범들'의 '앨범'마다 반복하자:
        '앨범'의 '곡들'의 '곡'마다 반복하자:
            틀"{'번호'}. {'곡'}"을 출력하자
            '번호'를 ('번호' + 1)로 정하자

(참고: "앞의 몇 곡만 미리듣기"도 모음마다 따로 만들어요. 반복이 두 겹이면 '반복을 끝내자'로는 안쪽만 끝나서, 돌려주자로 빠져나와요)
<재생목록미리듣기>를 만들자 ('목록', [숫자]인 '몇곡'):
    '들은수'를 0으로 정하자
    '목록'의 '곡들'의 '곡'마다 반복하자:
        만약 ('들은수'가 '몇곡'과 같다) 라면:
            반복을 끝내자
        틀"미리듣기: {'곡'}"을 출력하자
        '들은수'를 ('들은수' + 1)로 정하자

<앨범장미리듣기>를 만들자 ('장', [숫자]인 '몇곡'):
    '들은수'를 0으로 정하자
    '장'의 '앨범들'의 '앨범'마다 반복하자:
        '앨범'의 '곡들'의 '곡'마다 반복하자:
            만약 ('들은수'가 '몇곡'과 같다) 라면:
                돌려주자
            틀"미리듣기: {'곡'}"을 출력하자
            '들은수'를 ('들은수' + 1)로 정하자

'출근길'을 새로운 [재생목록]()으로 정하자
'출근길'의 <넣기>("아침 햇살")을 실행하자
'출근길'의 <넣기>("달리는 버스")를 실행하자
'출근길'의 <넣기>("커피 한 잔")을 실행하자

'내앨범'을 새로운 [앨범장]()으로 정하자
'내앨범'의 <넣기>(새로운 [앨범]("봄", ["벚꽃", "새싹"]))을 실행하자
'내앨범'의 <넣기>(새로운 [앨범]("여름", ["바다", "소나기", "수박"]))을 실행하자

<재생목록틀기>('출근길')을 실행하자
<앨범장틀기>('내앨범')을 실행하자
<재생목록미리듣기>('출근길', 2)를 실행하자
<앨범장미리듣기>('내앨범', 3)을 실행하자

실행 결과\

  1. 아침 햇살\
  2. 달리는 버스\
  3. 커피 한 잔\
  4. 벚꽃\
  5. 새싹\
  6. 바다\
  7. 소나기\
  8. 수박
    미리듣기: 아침 햇살
    미리듣기: 달리는 버스
    미리듣기: 벚꽃
    미리듣기: 새싹
    미리듣기: 바다

잘 돌아가요. 그런데 모음과 할 일이 늘어나면 아파져요.

  • 쓰는 쪽이 모음의 속을 다 알아요. <앨범장틀기>는 앨범장 안에 '앨범들'이 있고, 앨범마다 '곡들'이 있다는 것까지 알아야 해요. 곡을 꺼내고 싶을 뿐인데요.
  • 할 일과 모음의 가짓수를 곱한 만큼 함수가 생겨요. 할 일 두 가지(틀기, 미리듣기)에 모음 두 가지라서 함수가 넷이에요. “곡 수 세기”를 더하면 둘이 또 늘고, “추천 목록” 같은 새 모음이 생기면 할 일마다 하나씩 또 늘어요.
  • 속을 바꾸면 쓰는 쪽이 모두 깨져요. 앨범장이 앨범을 사전에 담도록 바뀌면, '앨범들'을 도는 함수를 전부 고쳐야 해요.
  • 속을 숨길 수 없어요. 쓰는 쪽이 '곡들'을 직접 꺼내야 하니 숨기자를 붙일 수 없어요. 그러면 아무나 '곡들'에 엉뚱한 것을 넣을 수도 있어요.
  • 반복이 두 겹이면 멈추기도 어색해요. 반복을 끝내자는 가장 안쪽 반복만 끝내서, <앨범장미리듣기>는 돌려주자로 함수째 빠져나와야 했어요.

해결책: “하나씩 꺼내기”를 맡는 객체 따로 두기

반복자 패턴은 모음을 도는 일을 반복자라는 객체에게 맡겨요.

  1. “하나씩 꺼내기”의 약속을 정해요. <남았나>(아직 남은 게 있니?)와 <다음>(다음 것을 줘) 두 가지면 돼요.
  2. 모음마다 자기 속에 맞는 반복자를 만들어요. 재생목록의 반복자는 목록에서 몇 번째까지 꺼냈는지 기억하고, 앨범장의 반복자는 몇 번째 앨범의 몇 번째 곡까지 꺼냈는지 기억해요.
  3. 모음은 반복자를 하나 만들어 주는 일만 해요. 속은 숨겨 둬요.
  4. 쓰는 쪽은 반복자에게 “남았니? 다음 거 줘”만 되풀이해요. 상대가 재생목록인지 앨범장인지 몰라요.

비유: 책갈피

책을 읽을 때 우리는 책이 몇 쪽씩 묶여 제본됐는지 몰라도 돼요. 책갈피가 “여기까지 읽었어요”를 기억해 주니까, 책갈피 다음 쪽을 펼치기만 하면 돼요. 같은 책에 책갈피를 두 개 꽂으면 두 사람이 각자 다른 곳까지 읽을 수도 있고요. 반복자는 모음에 꽂는 책갈피예요.

누가 무엇을 하나요?

역할 이 예제에서 하는 일
반복자 약속 [반복자] (규정) <남았나>와 <다음>을 정해요
구체적인 반복자 [목록반복자], [앨범장반복자] 자기 모음의 속을 알고, 어디까지 꺼냈는지 기억해요
모음 약속 [곡모음] (규정) 반복자를 하나 만들어 줄 수 있어야 해요
구체적인 모음 [재생목록], [앨범장] 속을 숨기고, 자기에게 맞는 반복자를 만들어 줘요
쓰는 쪽 <틀기>, <미리듣기> 반복자에게 묻고 받기만 해요

하리로 만들기

같은 음악 앱을 반복자 패턴으로 다시 만들어 볼게요.

(참고: 1단계 — "하나씩 꺼내기"의 약속. 남았는지 묻고, 다음 것을 받아요)
[반복자]를 규정하자:
    [논리]를 돌려주는 <남았나>가 있어야 한다 ()
    [문자열]을 돌려주는 <다음>이 있어야 한다 ()

(참고: 모음의 약속. 속이 어떻든 반복자를 하나 만들어 줄 수 있으면 돼요)
[곡모음]을 규정하자:
    [반복자]를 돌려주는 <차례로>가 있어야 한다 ()

(참고: 2단계 — 재생목록과 그 반복자. 반복자가 "지금 어디까지 꺼냈나"를 기억해요)
[반복자]를 따르는 [목록반복자]를 설계하자:
    '곡들'을 []로 정하여 숨기자
    '위치'를 [숫자]인 1로 정하여 숨기자

    처음 만들어질 때 ('곡들') 다음과 같이 하자:
        '나'의 '곡들'을 '곡들'로 정하자

    [논리]를 돌려주는 <남았나>를 만들자 ():
        ('나'의 '위치' <= '나'의 '곡들'의 '길이')를 돌려주자

    [문자열]을 돌려주는 <다음>을 만들자 ():
        '자리'를 '나'의 '위치'로 정하자
        '곡'을 '나'의 '곡들'의 '자리'번째로 정하자
        '나'의 '위치'를 ('나'의 '위치' + 1)로 정하자
        '곡'을 돌려주자

[곡모음]을 따르는 [재생목록]을 설계하자:
    '곡들'을 []로 정하여 숨기자

    <넣기>를 만들자 ([문자열]인 '곡'):
        '나'의 '곡들'에 '곡'을 추가하자

    [반복자]를 돌려주는 <차례로>를 만들자 ():
        새로운 [목록반복자]('나'의 '곡들')을 돌려주자

(참고: 3단계 — 앨범장과 그 반복자. 목록 안의 목록을 도는 일을 반복자가 맡아요)
[앨범]을 설계하자:
    '이름'을 [문자열]인 ""으로 정하자
    '곡들'을 []로 정하자

    처음 만들어질 때 ([문자열]인 '이름', '곡들') 다음과 같이 하자:
        '나'의 '이름'을 '이름'으로 정하자
        '나'의 '곡들'을 '곡들'로 정하자

[반복자]를 따르는 [앨범장반복자]를 설계하자:
    '앨범들'을 []로 정하여 숨기자
    '앨범위치'를 [숫자]인 1로 정하여 숨기자
    '곡위치'를 [숫자]인 1로 정하여 숨기자

    처음 만들어질 때 ('앨범들') 다음과 같이 하자:
        '나'의 '앨범들'을 '앨범들'로 정하자

    [논리]를 돌려주는 <남았나>를 만들자 ():
        (참고: 지금 앨범을 다 꺼냈으면 다음 앨범의 첫 곡으로 넘어가요. 빈 앨범은 건너뛰어요)
        ('나'의 '앨범위치' <= '나'의 '앨범들'의 '길이')인 동안 반복하자:
            '몇번째앨범'을 '나'의 '앨범위치'로 정하자
            '앨범'을 '나'의 '앨범들'의 '몇번째앨범'번째로 정하자
            만약 ('나'의 '곡위치' <= '앨범'의 '곡들'의 '길이') 라면:
                참을 돌려주자
            '나'의 '앨범위치'를 ('나'의 '앨범위치' + 1)로 정하자
            '나'의 '곡위치'를 1로 정하자
        거짓을 돌려주자

    [문자열]을 돌려주는 <다음>을 만들자 ():
        '몇번째앨범'을 '나'의 '앨범위치'로 정하자
        '몇번째곡'을 '나'의 '곡위치'로 정하자
        '앨범'을 '나'의 '앨범들'의 '몇번째앨범'번째로 정하자
        '곡'을 '앨범'의 '곡들'의 '몇번째곡'번째로 정하자
        '나'의 '곡위치'를 ('나'의 '곡위치' + 1)로 정하자
        '곡'을 돌려주자

[곡모음]을 따르는 [앨범장]을 설계하자:
    '앨범들'을 []로 정하여 숨기자

    <넣기>를 만들자 ([앨범]인 '앨범'):
        '나'의 '앨범들'에 '앨범'을 추가하자

    [반복자]를 돌려주는 <차례로>를 만들자 ():
        새로운 [앨범장반복자]('나'의 '앨범들')을 돌려주자

(참고: 4단계 — 쓰는 쪽은 모음의 속을 몰라요. 남았는지 묻고 다음을 받을 뿐이에요)
<틀기>를 만들자 ([곡모음]인 '모음'):
    '꺼내기'를 '모음'의 <차례로>()로 정하자
    '번호'를 1로 정하자
    ('꺼내기'의 <남았나>()가 참과 같다)인 동안 반복하자:
        틀"{'번호'}. {'꺼내기'의 <다음>()}"을 출력하자
        '번호'를 ('번호' + 1)로 정하자

<미리듣기>를 만들자 ([곡모음]인 '모음', [숫자]인 '몇곡'):
    '꺼내기'를 '모음'의 <차례로>()로 정하자
    '들은수'를 0으로 정하자
    (('꺼내기'의 <남았나>()가 참과 같다) 그리고 ('들은수' < '몇곡'))인 동안 반복하자:
        틀"미리듣기: {'꺼내기'의 <다음>()}"을 출력하자
        '들은수'를 ('들은수' + 1)로 정하자

'출근길'을 새로운 [재생목록]()으로 정하자
'출근길'의 <넣기>("아침 햇살")을 실행하자
'출근길'의 <넣기>("달리는 버스")를 실행하자
'출근길'의 <넣기>("커피 한 잔")을 실행하자

'내앨범'을 새로운 [앨범장]()으로 정하자
'내앨범'의 <넣기>(새로운 [앨범]("봄", ["벚꽃", "새싹"]))을 실행하자
'내앨범'의 <넣기>(새로운 [앨범]("여름", ["바다", "소나기", "수박"]))을 실행하자

<틀기>('출근길')을 실행하자
<틀기>('내앨범')을 실행하자
<미리듣기>('출근길', 2)를 실행하자
<미리듣기>('내앨범', 3)을 실행하자

실행 결과\

  1. 아침 햇살\
  2. 달리는 버스\
  3. 커피 한 잔\
  4. 벚꽃\
  5. 새싹\
  6. 바다\
  7. 소나기\
  8. 수박
    미리듣기: 아침 햇살
    미리듣기: 달리는 버스
    미리듣기: 벚꽃
    미리듣기: 새싹
    미리듣기: 바다

결과는 리팩터링 전과 한 줄도 다르지 않아요. 하지만 이제 할 일마다 함수가 하나씩뿐이고, 모음의 속은 모두 숨겨져 있어요.

단계별로 다시 보기

  • 1단계 — 하나씩 꺼내기의 약속. [반복자]는 <남았나>와 <다음> 두 가지만 약속해요. [곡모음]은 “반복자를 만들어 줄 수 있으면 곡모음”이라는 약속이에요.
  • 2단계 — 목록을 도는 반복자. [목록반복자]는 곡 목록과 '위치'를 가지고 있어요. <다음>을 부를 때마다 지금 위치의 곡을 주고 위치를 한 칸 옮겨요. 이 “어디까지 꺼냈나”를 반복자가 기억한다는 게 중요해요.
  • 3단계 — 두 겹을 도는 반복자. [앨범장반복자]는 앨범 위치와 곡 위치를 함께 기억해요. 지금 앨범을 다 꺼냈으면 <남았나>가 다음 앨범의 첫 곡으로 넘어가요. 두 겹 반복이 반복자 안에 숨었어요.
  • 4단계 — 쓰는 쪽은 묻고 받기만. <틀기>와 <미리듣기>는 [곡모음]을 받아서 <차례로>()로 반복자를 얻고, <남았나>가 참인 동안 <다음>을 받아요. 재생목록이든 앨범장이든 같은 함수 하나로 돼요. 미리듣기도 조건 하나로 멈춰서, 두 겹 반복을 빠져나올 걱정이 없어요.

반복자를 두 개 만들면요?
<차례로>()를 부를 때마다 새 반복자가 생기고, 위치는 반복자마다 따로예요. 그래서 한 재생목록을 두 사람이 따로따로 넘겨 가며 들을 수 있어요. 책 한 권에 책갈피 두 개를 꽂는 것과 같아요.

이제 모음의 속은 숨기자로 감출 수 있어요. 쓰는 쪽이 예전처럼 속을 직접 꺼내려 하면 그 자리에서 막혀요.

[재생목록]을 설계하자:
    '곡들'을 []로 정하여 숨기자

    <넣기>를 만들자 ([문자열]인 '곡'):
        '나'의 '곡들'에 '곡'을 추가하자

'출근길'을 새로운 [재생목록]()으로 정하자
'출근길'의 <넣기>("아침 햇살")을 실행하자

(참고: 쓰는 쪽이 속을 직접 꺼내려 하면 막혀요)
'출근길'의 '곡들'의 '곡'마다 반복하자:
    '곡'을 출력하자

새 모음을 더해 보면

“추천 목록”이라는 새 모음이 생겼다고 해 볼게요. 속은 또 다르게 생겼어요.

  • 앞의 방식: <추천목록틀기>, <추천목록미리듣기>처럼 할 일마다 함수를 하나씩 또 만들어야 해요.
  • 반복자 패턴: [곡모음]을 따르는 [추천목록]과 그 반복자 하나씩만 만들면 돼요. <틀기>와 <미리듣기>는 고치지 않아도 그대로 추천 목록을 틀어요.

거꾸로 새 할 일이 생겨도(곡 수 세기 같은) 반복자를 쓰는 함수를 하나만 만들면 모든 모음에서 돼요.

직접 해 보기
[재생목록]에 끝에서부터 꺼내는 <거꾸로>를 더해 보세요. '위치'가 마지막 곡에서 시작해 하나씩 줄어드는 [거꾸로반복자]를 만들고, <거꾸로>가 그것을 돌려주면 돼요. 그다음 [반복자]를 받아서 트는 함수를 만들어 '출근길'의 <거꾸로>()를 넘겨 보세요. 3. 커피 한 잔부터가 아니라 1. 커피 한 잔부터 나오면 성공이에요.

하리의 마다 반복하자와의 관계

'목록'의 '곡'마다 반복하자는 목록에서 하나씩 꺼내 주는 일을 하리가 대신 해 줘요. 반복자 패턴과 같은 생각이에요. 다만 마다 반복하자는 목록, 문자열, 사전처럼 하리가 이미 아는 모음만 돌 수 있어요. 직접 만든 [앨범장]처럼 속이 특별한 모음은, 이 페이지처럼 반복자를 만들어 같은 방법으로 꺼낼 수 있게 해 줘요.

언제 쓰면 좋을까요?

  • 모음의 속을 숨기고 싶을 때
  • 속이 서로 다른 모음들을 같은 방법으로 돌고 싶을 때
  • 한 모음을 여러 순서로 돌고 싶을 때 (차례로, 거꾸로, 건너뛰며)
  • 같은 모음을 여러 곳에서 따로 돌아야 할 때

좋은 점과 아쉬운 점

좋은 점 아쉬운 점
쓰는 쪽이 모음의 속을 몰라도 돼요 목록 하나뿐이라면 마다 반복하자보다 길고 번거로워요
도는 방법이 반복자 안에 모여서, 할 일마다 복사되지 않아요 반복자가 위치를 기억하는 동안 모음이 바뀌면 엉뚱한 곡을 줄 수 있어요
새 모음이나 새 순서를 더해도 쓰는 쪽은 그대로예요 클래스가 모음마다 하나씩 더 늘어요
반복자마다 위치가 따로라서 여러 곳에서 동시에 돌 수 있어요

너무 이른 패턴은 독이에요.
모음이 목록 하나뿐이고 속을 숨길 이유도 없다면 마다 반복하자면 충분해요. 속이 다른 모음이 둘 이상 생기거나, 같은 모음을 여러 순서로 돌아야 할 때 반복자를 꺼내도 늦지 않아요.

다른 패턴과의 관계

  • 컴포지트 패턴의 나무를 도는 데 잘 어울려요. 폴더 안의 폴더까지 모든 파일을 하나씩 꺼내는 일을 반복자에게 맡기면, 쓰는 쪽은 나무의 깊이를 몰라도 돼요.
  • 팩토리 패턴이 숨어 있어요. <차례로>()는 모음마다 알맞은 반복자를 골라 만들어 주는 팩토리 메서드예요.
  • 전략 패턴과 비슷하게 바꿔 끼울 수 있어요. 차례로, 거꾸로, 건너뛰며처럼 도는 방법이 반복자마다 달라서, 쓰는 쪽은 어떤 반복자를 받든 똑같이 써요.
놀이터
결과
코드를 작성하고 '실행' 버튼을 눌러보세요.