Self-Dev/Design Patterns R&D

[27] 디자인 패턴 목록 - 행동 디자인 패턴 - 반복자 패턴(Iterator Patten)

Khadra 2024. 7. 13. 18:20

출처 : 디자인 패턴에 뛰어들기 - 알렉산더 슈베츠 도서


반복자 패턴(Iterator Patten)

반복자는 컬렉션의 요소들의 기본 표현(리스트, 스택, 트리 등)을 노출하지 않고 그들을 하나씩 순회할 수 있도록 하는 행동 디자인 패턴이다.



문제

컬렉션은 프로그래밍에서 가장 많이 사용되는 데이터 유형 중 하나이다.
하지만, 객체 그룹의 단순한 컨테이너에 불과하다.



다양한 유형들의 컬렉션들 예시이다.


대부분의 컬렉션들은 그들의 요소들을 간단한 리스트들에 저장한다.

  • 그 중 일부는 스택, 트리, 그래프 및 기타 복잡한 데이터 구조들을 기반으로 한다.
  • 그러나 컬렉션이 어떻게 구성되어 있는지를 떠나서, 컬렉션은 그 요소들에 접근할 수 있는 어떤 방법을 다른 코드에 제공해야 한다.

그래야 다른 코드가 이 요소들을 사용할 수 있다.
같은 요소에 반복해서 접근하지 않고 컬렉션의 각 요소를 순회하는 방법이 있을 것이다.


리스트로 된 컬렉션이 있다면 아주 쉽게 해결할 수 있을 것이다.

  • 모든 요소를 루프 처리하면 되는 것을 알 수 있다.
  • 하지만 트리처럼 복잡한 데이터 구조의 요소들은 어떻게 순차적으로 순회해야 하는지
    예를 들어
    • 어떤 날은 트리의 깊이를 우선으로 순회하는 것이 적절할지도 모른다.
    • 하지만 그다음 날에는 너비를 우선으로 순회해야 할 수도 있다.
    • 그리고 그다음 주에는 트리 요소들에 대한 임의 접근 등 다른 방식의 순회가 필요할지도 모른다.



같은 컬렉션을 여러 가지 방법들로 순회할 수 있다.


현재 컬렉션의 주요 책임은 효율적인 데이터 저장이나, 컬렉션에 더 많은 순회 알고리즘들을 추가할수록 컬렉션의 주요 책임이 무엇인지 점점 명확해지지 않게 된다.

  • 일부 알고리즘들은 특정 앱에 맞게 조정되었을 수 있으므로 일반적인 컬렉션 클래스에 이들을 포함하는 것은 이상할 수 있다.
  • 반면에 다양한 컬렉션들과 작동해야 하는 클라이언트 코드는 자신들의 요소가 어떻게 저장되는지 관심을 두지 않는다.
  • 하지만 컬렉션마다 그 요소들에 접근할 수 있도록 허용하는 방법이 다르다.
  • 즉, 코드를 특정한 컬렉션 클래스에 결합할 수밖에 없다.


해결책

반복자 패턴의 주 아이디어는 컬렉션의 순회 동작을 반복자라는 별도의 객체로 추출하는 것이다.


반복자들은 다양한 순회 알고리즘들을 구현한다. 여러 반복자 객체들이 동시에 같은 컬렉션을 순회할 수 있는 예시이다.

반복자 객체는 알고리즘 자체를 구현하는 것 외에도 모든 순회 세부 정보들(예: 현재 위치 및 남은 요소들의 수)을 캡슐화한다.

  • 이로써, 여러 반복자들이 서로 독립적으로 동시에 같은 컬렉션을 통과할 수 있다.

일반적으로 반복자들은 컬렉션의 요소들을 가져오기 위한 하나의 주 메서드를 제공한다.

  • 클라이언트는 이 메서드를 더 이상 아무것도 반환하지 않을 때까지 계속 실행할 수 있다.
    • 이는 반복자가 모든 요소를 순회했음을 의미한다.

모든 반복자들은 같은 인터페이스를 구현해야 한다.

  • 이렇게 하면 적절한 반복자가 있는 한 클라이언트 코드는 모든 컬렉션 유형들 및 순회 알고리즘들과 호환된다.
  • 컬렉션을 순회하는 특별한 방법이 필요하면 컬렉션이나 클라이언트를 변경할 필요 없이 새 반복자 클래스를 만들기만 하면 된다.


실제상황 적용


도보로 로마를 탐험하는 다양한 방법들 예시이다.

예시 설명

  • 며칠 동안 로마를 방문하고 주요 명소들을 모두 방문할 계획을 세웠다.

    • 그러나 막상 도착한 후 당신은 콜로세움조차 찾지 못하고 제자리를 맴도는 데 많은 시간을 허비할 수 있다.
  • 대안으로 스마트폰용 가상 가이드 앱을 구매하여 내비게이션에 사용할 수 있다.

    • 이 앱은 똑똑하고 저렴하며 실제 가이드와 달리 원하는 만큼 흥미로운 장소에 머물 수 있도록 한다.
  • 세 번째 대안은 조금 더 비싸더라도 도시를 잘 아는 현지 가이드를 고용하는 것이다.

    • 현지 가이드는 당신의 취향에 맞게 여행을 조정하고 모든 명소를 보여주며 흥미진진한 이야기들을 많이 알려줄 수 있다.
    • 이 대안을 선택하면 재미는 있겠지만, 더 많은 비용이 들게 될 것입니다.
  • 이 모든 대안들은 (예: 당신이 무작위로 생각해 낸 방향들, 스마트폰 내비게이터, 현지 가이드) 로마의 많은 관광명소에 대해 반복자들로 작동하는 것을 알 수 있다.



구조



  • 1. 반복자 인터페이스는 컬렉션의 순회에 필요한 작업들(예: 다음 요소 가져오기, 현재 위치 가져오기, 반복자 다시 시작 등)을 선언한다.


  • 2. 구상 반복자들은 컬렉션 순회를 위한 특정 알고리즘들을 구현한다.

    • 반복자 객체는 순회의 진행 상황을 자체적으로 추적해야 한다.
    • 이는 여러 반복자들이 같은 컬렉션을 서로 독립적으로 순회할 수 있도록 한다.
  • 3. 컬렉션 인터페이스는 컬렉션과 호환되는 반복자들을 가져오기 위한 하나 이상의 메서드들을 선언한다.

    • 참고로 메서드들의 반환 유형은 반복자 인터페이스의 유형으로 선언되어야 한다.
    • 그래야 구상 컬렉션들이 다양한 유형의 반복자들을 반환할 수 있기 때문이다.
  • 4. 구상 컬렉션들은 클라이언트가 요청할 때마다 특정 구상 반복자 클래스의 새 인스턴스들을 반환한다.

    • 당신은 컬렉션의 나머지 코드가 어디에 있는지 궁금하실 수도 있다.
    • 걱정하지 않은 점이 같은 클래스에 있을 것이다.
    • 나머지 코드가 어디에 있는지와 같은 세부 사항들은 실제 패턴에 중요하지 않으므로 생략하기로 했다.
  • 5. 클라이언트는 반복자들과 컬렉션들의 인터페이스를 통해 그들과 함께 작동한다.

    • 이렇게 하면 클라이언트가 구상 클래스들에 결합하지 않으므로 같은 클라이언트 코드로 다양한 컬렉션들과 반복자들을 사용할 수 있도록 한다.

일반적으로 클라이언트들은 자체적으로 반복자들을 생성하지 않고 대신 컬렉션들에서 가져온다.

  • 그러나 어떤 경우에는 (예를 들어 클라이언트가 자체 특수 반복자를 정의할 때) 클라이언트가 반복자를 직접 만들 수 있다.


의사코드

이 예시에서 반복자 패턴은 페이스북의 소셜 그래프에 대한 접근을 캡슐화하는 특별한 종류의 컬렉션을 순회하는 데 사용한다.

  • 이 컬렉션은 다양한 방식으로 프로필들을 순회할 수 있는 여러 반복자들을 제공한다.


소셜 프로필들의 순회 예시이다.

예시 설명

  • Friends(친구들) 반복자는 주어진 프로필의 친구들을 탐색하는 데 사용될 수 있다.
  • Colleagues(동료들) 반복자는 프로필 주인과 같은 회사에서 일하지 않는 친구들을 제외한 후 같은 작업을 수행한다.
  • 두 반복자 모두 인증 및 REST 요청 전송과 같은 구현 세부 사항들을 자세히 살펴보지 않고도 클라이언트들이 프로필들을 가져올 수 있도록 하는 공통 인터페이스를 구현한다.
  • 클라이언트 코드는 인터페이스들을 통해서만 컬렉션들 및 반복자들과 작업하기 때문에 구상 클래스들과 결합하지 않다.
  • 앱을 새로운 소셜 네트워크에 연결하기로 했다면 기존 코드를 변경하지 않고 새로운 컬렉션 및 반복자 클래스들을 제공하기만 하면 된다.

// 컬렉션 인터페이스는 반복자들을 생성하기 위한 팩토리 메서드를 선언해야 합니다.
// 프로그램에서 사용할 수 있는 다양한 종류의 순회가 있는 경우 
// 여러 메서드를 선언할 수 있습니다.
interface SocialNetwork is
  method createFriendsIterator(profileId):ProfileIterator
  method createCoworkersIterator(profileId):ProfileIterator


// 각 구상 컬렉션은 자신이 반환하는 구상 반복자 클래스들의 집합에 연결됩니다.
// 하지만 이러한 메서드들의 시그니처는 반복자 인터페이스를 반환하기 때문에
// 클라이언트는 연결되지 않습니다.
class Facebook implements SocialNetwork is
  // …컬렉션 코드의 대부분은 여기에 포함되어야 합니다.

  // 반복자 생성 코드.
  method createFriendsIterator(profileId) is
    return new FacebookIterator(this, profileId, "friends")
  method createCoworkersIterator(profileId) is
    return new FacebookIterator(this, profileId, "coworkers")


// 모든 반복자에 대한 공통 인터페이스.
interface ProfileIterator is
  method getNext():Profile
  method hasMore():bool


// 구상 반복자 클래스.
class FacebookIterator implements ProfileIterator is
  // 반복자는 순회하는 컬렉션에 대한 참조가 필요합니다.
  private field facebook: Facebook
  private field profileId, type: string

  // 반복자 객체는 다른 반복자들과 별도로 컬렉션을 순회합니다. 
  // 따라서 반복자 상태를 저장해야 합니다.
  private field currentPosition
  private field cache: array of Profile

  constructor FacebookIterator(facebook, profileId, type) is
    this.facebook = facebook
    this.profileId = profileId
    this.type = type

  private method lazyInit() is
    if (cache == null)
      cache = facebook.socialGraphRequest(profileId, type)

  // 각 구상 반복자 클래스는 공통 반복자 인터페이스를 자체적으로 구현합니다.
  method getNext() is
    if (hasMore())
      result = cache[currentPosition]
      currentPosition++
      return result

  method hasMore() is
    lazyInit()
    return currentPosition < cache.length


// 유용한 요령이 하나 더 있습니다. 전체 컬렉션에 대한 접근 권한을 
// 클라이언트 클래스에 부여하는 대신 반복자를 클라이언트 클래스에 전달하는 것입니다. 
// 그러면 컬렉션이 클라이언트에 노출되지 않습니다.
//
// 장점도 하나 더 있습니다. 클라이언트에 다른 반복자를 전달하여 런타임 때
// 클라이언트가 컬렉션과 작동하는 방식을 변경할 수 있습니다. 
// 이것은 클라이언트 코드가 구상 반복자 클래스들에 결합되어 있지 않기 때문에 가능합니다.
class SocialSpammer is
  method send(iterator: ProfileIterator, message: string) is
    while (iterator.hasMore())
      profile = iterator.getNext()
      System.sendEmail(profile.getEmail(), message)


// 앱 클래스는 컬렉션들과 반복자들을 설정한 다음 클라이언트 코드에 전달합니다.
class Application is
  field network: SocialNetwork
  field spammer: SocialSpammer

  method config() is
    if working with Facebook
      this.network = new Facebook()
    if working with LinkedIn
      this.network = new LinkedIn()
    this.spammer = new SocialSpammer()

  method sendSpamToFriends(profile) is
    iterator = network.createFriendsIterator(profile.getId())
    spammer.send(iterator, "Very important message")

  method sendSpamToCoworkers(profile) is
    iterator = network.createCoworkersIterator(profile.getId())
    spammer.send(iterator, "Very important message")


적용

반복자 패턴은 당신의 컬렉션이 내부에 복잡한 데이터 구조가 있지만 이 구조의 복잡성을 보안이나 편의상의 이유로 클라이언트들로부터 숨기고 싶을 때 사용하세요.

  • 반복자는 복잡한 데이터 구조와 작업 시의 세부 사항을 캡슐화하여 클라이언트에 컬렉션 요소들에 접근할 수 있는 몇 가지 간단한 메서드들을 제공한다.
  • 이 접근 방식은 클라이언트에게 매우 편리하다.
  • 또 클라이언트가 컬렉션과 직접 작동할 때 클라이언트가 수행할 수 있는 부주의하거나 악의적인 행동들로부터 컬렉션을 보호한다.

반복자 패턴을 사용하여 당신의 앱 전체에서 순회 코드의 중복을 줄이세요.

  • 사소하지 않은 순회 알고리즘들의 코드는 부피가 매우 큰 경향이 있다.
  • 이 코드들이 앱의 비즈니스 로직 내에 배치되면 원래 코드의 책임이 무엇인지 모호해질 수 있으며 코드의 유지관리가 더 어려워질 수 있다.
  • 순회 코드를 지정된 반복자들로 이동하면 당신의 앱의 코드가 더 간결하고 깔끔해질 수 있다.

반복자 패턴은 코드가 다른 데이터 구조들을 순회할 수 있기를 원할 때 또는 이러한 구조들의 유형을 미리 알 수 없을 때 사용하세요.

  • 이 패턴은 컬렉션들과 반복자들 모두에 몇 개의 일반 인터페이스들을 제공한다.
  • 해당 코드가 이러한 인터페이스들을 사용한다는 점을 고려할 때, 이러한 인터페이스들은 그들을 구현하는 다양한 컬렉션들 및 반복자들을 전달받아도 여전히 작동한다.


구현방법

  • 1. 반복자 인터페이스를 선언하세요.

    • 이 인터페이스는 최소한 컬렉션에서 다음 요소를 가져오는 메서드가 있어야 한다.
    • 또 편의를 위해 여기에 몇 가지 다른 메서드들도 (예: 전 요소를 가져오는, 현재 위치를 추적하는, 그리고 반복자의 순회의 끝을 확인하는 메서드들) 추가할 수 있다.
  • 2. 컬렉션 인터페이스를 선언하고 반복자를 가져오는 메서드를 설명하세요.

    • 컬렉션 인터페이스의 반환 유형은 반복자 인터페이스의 유형과 같아야 한다.
    • 뚜렷하게 다른 여러 개의 반복자들의 그룹을 가질 계획이라면 유사한 메서드들을 선언할 수 있다.
  • 3. 반복자들이 순회하게 할 수 있도록 하고 싶은 컬렉션들에 대한 구상 반복자 클래스들을 구현하세요.

    • 반복자 객체는 단일 컬렉션 인스턴스와 반드시 연결되어야 한다.
    • 일반적으로 이러한 연결은 반복자의 생성자를 통해 맺어진다.
  • 4. 당신의 컬렉션 클래스들에서 컬렉션 인터페이스를 구현하세요.

    • 그 주된 목적은 클라이언트에 특정 컬렉션 클래스에 맞게 조정된 반복자들을 생성하기 위한 바로 가기를 제공하는 것이다.
    • 컬렉션 객체는 반복자의 생성자에 자신을 전달해야 하며 이 둘 사이에 연결을 맺어야 한다.
  • 5. 클라이언트 코드를 살펴보면서 반복자들을 사용하여 모든 컬렉션 순회 코드들을 교체하세요.

    • 클라이언트는 컬렉션 요소들을 순회해야 할 때마다 새 반복자 객체를 가져온다.


장단점

장점

  • 단일 책임 원칙

    • 부피가 큰 순회 알고리즘들을 별도의 클래스들로 추출하여 클라이언트 코드와 컬렉션들을 정돈할 수 있다.
  • 개방/폐쇄 원칙

    • 새로운 유형의 컬렉션들과 반복자들을 구현할 수 있다.
    • 이들을 아무것도 훼손하지 않은 체 기존의 코드에 전달할 수 있다.
  • 이제 같은 컬렉션을 병렬로 순회할 수 있다.

    • 왜냐하면 각 반복자 객체에는 자신의 고유한 순회 상태가 포함되어 있기 때문이다.
  • 같은 이유로 해당 순회를 지연하고 필요할 때 계속할 수 있다.


단점

  • 당신의 앱이 단순한 컬렉션들과만 작동하는 경우 반복자 패턴을 적용하는 것은 과도할 수 있다.
  • 반복자를 사용하는 것은 일부 특수 컬렉션들의 요소들을 직접 탐색하는 것보다 덜 효율적일 수 있다.


다른 패턴과의 관계

  • 당신은 반복자들을 사용하여 복합체 패턴 트리들을 순회할 수 있다.
  • 팩토리 메서드를 반복자와 함께 사용하여 컬렉션 자식 클래스들이 해당 컬렉션들과 호환되는 다양한 유형의 반복자들을 반환하도록 할 수 있다.
  • 메멘토 패턴을 반복자 패턴과 함께 사용하여 현재 순회 상태를 포착하고 필요한 경우 롤백할 수 있다.
  • 비지터 패턴과 반복자 패턴을 함께 사용해 복잡한 데이터 구조를 순회하여 해당 구조의 요소들의 클래스들이 모두 다르더라도 이러한 요소들에 대해 어떤 작업을 실행할 수 있다.