[Design Pattern] 이터레이터 패턴

olwooz·2023년 2월 24일
0

Design Pattern

목록 보기
15/22
콜렉션의 표현(리스트, 스택, 트리 등)을 드러내지 않고 요소들을 순회할 수 있게 해주는 행동 패턴

문제

콜렉션은 프로그래밍에서 가장 많이 사용되는 데이터 타입 중 하나지만, 단지 객체 그룹의 컨테이너일 뿐

콜렉션들은 단순한 리스트에 요소를 저장하지만, 어떤 콜렉션들은 스택, 트리, 그래프 또는 다른 복잡한 자료 구조를 기반으로 함

콜렉션이 어떻게 구성되어 있든, 다른 코드들이 콜렉션의 요소들을 사용할 수 있도록 요소들에 접근하는 방법을 제공해줘야 함

  • 같은 요소를 반복해서 접근하지 않고 순회하는 방법이 있어야 함

리스트 기반 콜렉션은 순회가 쉽지만 트리와 같은 복잡한 자료 구조는 그렇지 않음

  • 어떨 땐 DFS, 어떨 땐 BFS, 어떨 땐 랜덤 접근 등 다양한 방법으로 순회하고 싶을 수 있음
  • 다양한 순회 알고리즘들이 콜렉션에 더해질수록 효율적인 데이터 저장 공간이라는 주 책임이 모호해짐
  • 특정 애플리케이션을 위해 만들어진 알고리즘을 일반 콜렉션 클래스에 포함시키는 것도 이상함

다양한 콜렉션을 다루는 클라이언트 코드는 요소가 어떻게 저장되는지 신경조차 쓰지 않을 수 있음

하지만 콜렉션들은 요소에 접근하는 각기 다른 방법을 제공하기 때문에 코드를 특정 콜렉션 클래스들에 결합할 수밖에 없음

해결책

이터레이터 패턴 - 콜렉션을 순회하는 행동을 이터레이터라는 별도의 객체로 추출

이터레이터 객체는 알고리즘을 직접 구현할 뿐만 아니라 현재 위치, 남은 요소의 갯수 등 순회의 세부 사항들을 캡슐화함 → 여러 이터레이터들이 서로 독립적으로 동시에 같은 콜렉션을 순회할 수 있음

이터레이터는 주로 콜렉션의 요소를 가져오는 단일 주요 메서드 제공

  • 클라이언트는 이터레이터가 더 이상 아무것도 반환하지 않을 때까지(모든 요소 순회 완료) 이 메서드를 계속 실행할 수 있음

모든 이터레이터는 동일한 인터페이스를 구현 → 적절한 이터레이터가 존재하는 한 클라이언트 코드는 어떤 유형의 콜렉션, 어떤 순회 알고리즘과도 호환 가능

  • 콜렉션을 순회하는 특별한 방법이 필요하면 콜렉션이나 클라이언트 변경 없이 새 이터레이터 클래스를 생성하면 됨

구조

1. 이터레이터 인터페이스 - 콜렉션을 순회하기 위해 필요한 작업들 선언
   - 다음 요소 가져오기, 현재 위치 가져오기, 순회 재시작 등
   
2. concrete 이터레이터 - 콜렉션을 순회하는 특정 알고리즘 구현
   - 이터레이터 객체는 순회 진행도를 스스로 추적해야 함 
   → 여러 이터레이터들이 같은 콜렉션을 독립적으로 순회할 수 있게 해줌
    
3. 콜렉션 인터페이스 - 이터레이터들을 콜렉션과 호환시키는 하나 이상의 메서드 선언
   - concrete 콜렉션들이 다양한 유형의 이터레이터를 반환할 수 있도록 
     메서드의 반환 타입을 이터레이터 인터페이스로 선언해야 함
    
4. concrete 콜렉션 - 클라이언트가 요청할 때마다 특정 concrete 이터레이터 클래스의 새 인스턴스 반환
   - 콜렉션 자체 코드도 같은 클래스에 포함
    
5. 클라이언트 - 콜렉션과 이터레이터 모두를 각각의 인터페이스를 통해 다룸 
   → concrete 클래스들에 결합되지 않으면서 다양한 콜렉션과 이터레이터 사용 가능
   - 클라이언트는 보통 이터레이터를 직접 생성하지 않고 콜렉션에서 가져오지만, 
     클라이언트가 특별한 이터레이터를 정의하거나 한 경우 클라이언트가 직접 생성할 수 있음

적용

콜렉션이 복잡한 자료 구조를 가질 때 클라이언트로부터 복잡성을 숨기고 싶은 경우 (편의/보안)

- 이터레이터는 복잡한 자료 구조를 다루는 세부 사항을 캡슐화해 
  클라이언트에게 콜렉션 요소에 접근하는 단순한 메서드 제공
- 클라이언트에게도 편리하고, 콜렉션을 부주의하거나 악의를 가진 행동으로부터 보호 가능

앱 전반에 걸친 순회 코드의 중복을 줄이고 싶은 경우

- 단순하지 않은 순회 알고리즘은 크기가 매우 큰 경향이 있음 
  → 비즈니스 로직에 위치하게 되면 기존 코드의 역할을 모호하게 하고 유지보수를 어렵게 만듦
- 순회 코드를 지정된 이터레이터들로 옮기면 애플리케이션의 코드를 더 간결하고 깔끔하게 만들 수 있음

다양한 자료 구조를 순회할 수 있게 하려 하거나, 어떤 자료 구조인지 사전에 알 수 없는 경우

- 이터레이터 패턴은 콜렉션과 이터레이터 모두에게 일반 인터페이스를 제공
- 코드가 해당 인터페이스를 사용한다면, 해당 인터페이스를 구현한 
  다양한 콜렉션들과 이터레이터들을 전달받아도 잘 작동함

구현방법

1. 이터레이터 인터페이스 선언 
   - 최소한 콜렉션의 다음 요소를 가져오는 메서드는 가지고 있어야 함
   - 편의를 위해 이전 요소 가져오기, 현재 위치 추적하기, 순회의 끝 검사하기 등 다른 메서드 추가 가능
   
2. 콜렉션 인터페이스를 선언하고 이터레이터를 가져오는 메서드 설명
   - 반환 타입은 이터레이터 인터페이스 타입이여야 함
   
3. 이터레이터로 순회할 수 있게 하려는 콜렉션들에게 concrete 반복자 클래스들을 구현
   - 이터레이터 객체는 하나의 콜렉션 인스턴스와 연결되어 있어야 함 → 주로 이터레이터의 생성자에 의해 연결됨
    
4. 콜렉션 클래스 안에 콜렉션 인터페이스 구현
   - 클라이언트에게 특정 콜렉션 클래스들을 위한 이터레이터를 생성하는 지름길 제공
   - 콜렉션은 연결을 맺기 위해 이터레이터의 생성자에게 자기 자신을 전달해야 함
   
5. 클라이언트 코드에서 콜렉션 순회 코드를 이터레이터로 변경
   - 클라이언트는 콜렉션 요소들을 순회해야 할 때마다 새 이터레이터 객체를 가져옴

장단점

장점

- SRP - 거대한 순회 알고리즘들을 개별 클래스들로 분리해 클라이언트 코드와 콜렉션들을 깔끔하게 만들 수 있음
- OCP - 다른 코드를 훼손하지 않으면서 새로운 유형의 콜렉션과 이터레이터들을 구현해 기존 코드에 전달할 수 있음
- 같은 콜렉션을 병렬적으로 순회할 수 있음
- 순회를 지연시키고 필요할 때 재개할 수 있음

단점

- 앱이 단순한 콜렉션들만 다룬다면 패턴을 적용하는 게 과함
- 일부 특별한 콜렉션들의 요소들은 직접 탐색하는 것이 더 효율적일 수 있음

다른 패턴과의 관계

- 이터레이터 패턴을 사용해 컴포지트 트리 순회 가능

- 팩토리 메서드를 반복자와 함께 사용해 콜렉션 자식 클래스들이 해당 콜렉션들과 호환되는 
  다양한 유형의 이터레이터들을 반환하도록 할 수 있음

- 메멘토를 이터레이터와 함께 사용해 현재 순회 상태를 저장하고 필요한 경우 롤백 가능

- 비지터를 이터레이터와 함께 사용해 복잡한 자료 구조를 순회하며 
  요소들이 모두 다른 클래스를 가지더라도 요소들에게 특정 작업을 실행할 수 있음

TypeScript 예제

/**
 * Iterator Design Pattern
 *
 * Intent: Lets you traverse elements of a collection without exposing its
 * underlying representation (list, stack, tree, etc.).
 */

interface Iterator<T> {
    // Return the current element.
    current(): T;

    // Return the current element and move forward to next element.
    next(): T;

    // Return the key of the current element.
    key(): number;

    // Checks if current position is valid.
    valid(): boolean;

    // Rewind the Iterator to the first element.
    rewind(): void;
}

interface Aggregator {
    // Retrieve an external iterator.
    getIterator(): Iterator<string>;
}

/**
 * Concrete Iterators implement various traversal algorithms. These classes
 * store the current traversal position at all times.
 */

class AlphabeticalOrderIterator implements Iterator<string> {
    private collection: WordsCollection;

    /**
     * Stores the current traversal position. An iterator may have a lot of
     * other fields for storing iteration state, especially when it is supposed
     * to work with a particular kind of collection.
     */
    private position: number = 0;

    /**
     * This variable indicates the traversal direction.
     */
    private reverse: boolean = false;

    constructor(collection: WordsCollection, reverse: boolean = false) {
        this.collection = collection;
        this.reverse = reverse;

        if (reverse) {
            this.position = collection.getCount() - 1;
        }
    }

    public rewind() {
        this.position = this.reverse ?
            this.collection.getCount() - 1 :
            0;
    }

    public current(): string {
        return this.collection.getItems()[this.position];
    }

    public key(): number {
        return this.position;
    }

    public next(): string {
        const item = this.collection.getItems()[this.position];
        this.position += this.reverse ? -1 : 1;
        return item;
    }

    public valid(): boolean {
        if (this.reverse) {
            return this.position >= 0;
        }

        return this.position < this.collection.getCount();
    }
}

/**
 * Concrete Collections provide one or several methods for retrieving fresh
 * iterator instances, compatible with the collection class.
 */
class WordsCollection implements Aggregator {
    private items: string[] = [];

    public getItems(): string[] {
        return this.items;
    }

    public getCount(): number {
        return this.items.length;
    }

    public addItem(item: string): void {
        this.items.push(item);
    }

    public getIterator(): Iterator<string> {
        return new AlphabeticalOrderIterator(this);
    }

    public getReverseIterator(): Iterator<string> {
        return new AlphabeticalOrderIterator(this, true);
    }
}

/**
 * The client code may or may not know about the Concrete Iterator or Collection
 * classes, depending on the level of indirection you want to keep in your
 * program.
 */
const collection = new WordsCollection();
collection.addItem('First');
collection.addItem('Second');
collection.addItem('Third');

const iterator = collection.getIterator();

console.log('Straight traversal:');
while (iterator.valid()) {
    console.log(iterator.next());
}

console.log('');
console.log('Reverse traversal:');
const reverseIterator = collection.getReverseIterator();
while (reverseIterator.valid()) {
    console.log(reverseIterator.next());
}
// Output.txt

Straight traversal:
First
Second
Third

Reverse traversal:
Third
Second
First

참고 자료: Refactoring.guru

0개의 댓글