본문 바로가기
Concepts/Design Pattern

행동 패턴 - 반복자(Iterator)

by ocwokocw 2021. 6. 8.

https://refactoring.guru/design-patterns/command

- 반복자(Iterator)

내부 표현계층을 외부로 노출하지 않고 collection의 원소들을 탐색할 수 있는 행동패턴이다.


- 문제점

Collection 은 프로그래밍에서 가장 많이 사용되는 데이터형이긴 하지만 단지 객체들의 집합을 위한 컨테이너에 지나지 않는다. 대부분의 collection은 list로 원소들을 저장하고 있다. 그러나 일부는 스택이나 트리, 그래프 또는 다른 복잡한 데이터 구조에 기반하기도 한다.

 

하지만 아무리 collection이 구조적이라 하더라도, 다른 코드에서 collection을 사용하기 위해서는 원소들에 순차적으로 접근할 수 있어야 한다.

 

List에 기반한 Collection이면 모든 원소들을 접근하는 기능을 제공하는게 어렵지는 않다. 하지만 tree 같은 데이터구조에 기반한 collection 원소들에 접근하는 기능을 제공해야 하면 이야기가 달라진다. 예를 들어 트리를 DFS 방식으로 순회하는 기능 구현은 크게 어렵진 않다. 그러나 시간이 지나 갑자기 BFS 순회하는 기능이 요구될 수도 있다. 또 시간이 지나 트리의 랜덤 접근 기능이 필요해질수도 있다.

 

Collection에 순회 알고리즘을 점차 추가할수록 효율적으로 데이터를 저장하는데 집중해야 하는 책임이 점점 밀려난다. 또 몇몇 알고리즘은 특정 어플리케이션에만 알맞기 때문에 일반적인 collection 클래스에 추가하는게 적절하지 않을 수 있다.

 

클라이언트 코드(Collection 을 사용하는 코드)는 collection에 데이터가 어떻게 저장되는지는 몰라도 되지만, collection은 자신의 원소들에 접근하는 다양한 방법을 제공해야 하기 때문에 해당 코드가 특정 collection 클래스와 결합도를 갖게 된다.


- 해결책

반복자 패턴의 핵심은 collection 의 순회하는 행동을 iterator 라고 불리는 분리된 객체로 분리시키는것이다. 또 알고리즘 구현에서 iterator 객체는 순회하는 동작(예를 들어 현재 position과 얼마나 많은 원소들이 여전히 남아있는지 등)을 캡슐화 시켜야 한다.

 

대게 반복자는 collection 의 원소를 가져오기 위한 1개의 주요 메소드를 제공하고, 클라이언트는 반복자가 모든 원소를 순회하여 반환되는 원소가 없을 때 까지 이 메소드만을 사용하면 된다.

 

모든 반복자는 반드시 같은 인터페이스를 구현해야 한다. 그래야만 적절한 반복자가 있을 때, 클라이언트 코드가 다른 collection 타입이나 다른 순회 알고리즘과도 호환이 가능해진다. 만약 collection에 특별한 순회 기능을 추가해야 한다면 client나 collection을 변경시킬 필요 없이 새로운 반복자 class를 만들면 된다.


- 실생활 예제

우리가 로마에 방문해서 주요 관광지를 둘러본다고 가정해보자. 하지만 로마를 관광할 때 잘 모르는 상태로 가면 콜로세움도 찾지 못한채 원을 빙빙 돌면서 시간을 낭비할 수도 있다.

 

반면 스마트폰에 가상 가이드앱을 깔아서 네비게이션 용도로 사용할수도 있다. 스마트하고 저렴하게 여러 장소들을 둘러볼 수 있다.

 

또 돈을 좀 써서 해당 지역을 잘 아는 관광가이드를 예약할수도 있다. 관광 가이드는 돈은 좀 들지만 원하는 곳도 데려가주고 재밌는 얘기도 들려준다.

 

위의 예제는 로마에 있는 관광지들은 collection에 그리고 로마를 둘러보는 3가지 옵션을 반복자로 비유하고 있다.


- 참여자

반복자 패턴의 구조와 참여자들을 알아보자.

  • Iterator: 원소를 순회하는데 필요한 연산들을 정의하는 인터페이스이다. 다음 원소 참조, 현재 Position 등의 연산을 정의한다.
  • ConcreteIterators: Collection 을 순회하기 위한 특정 알고리즘을 구현한다. 서로 독립적인 반복자는 같은 collection에 대해서 순회할 수 있다.
  • IterableCollection: Collection과 호환되는 반복자를 얻기위한 1개 이상의 메소드를 정의한다. 메소드의 반환 타입은 ConcreteCollection 이 다양한 종류의 반복자들을 반환할 수 있도록 Iterator 인터페이스를 구현한 타입으로 선언해야 한다.
  • ConcreteCollection: ConcreteCollection은 client가 요청할 때 마다 특정 Iterator 클래스의 새로운 인스턴스를 반환한다. 다이어그램에서는 나머지 collection 코드들은 생략되었다.
  • Client: Client 는 collection과 반복자를 인터페이스로 참조한다. 이렇게 하면 다양한 collection과 반복자를 사용할 수 있게 된다. 일반적으로 client 는 반복자를 만들지는 않고 collection 으로 부터 얻는다. 하지만 client 가 특별한 반복자를 정의하는것과 같은 특수한 경우에는 직접 생성할수도 있다.

- Java 예제

이번에는 Java 예제를 직접 구현하지 않고 Java에 내장된 소스코드를 통해 Iterator 를 알아보도록 하자. 우선 IterableCollection 인터페이스인 List 형과 그의 구현체(ConcreteCollection)인 ArrayList 를 선언하였다. 그리고 이를 2개의 서로 다른 Iterator(Iterator, ListIterator)로 순회하여 Printout을 찍는 코드이다.

 

public static void main(String[] args) {

		List<Integer> collection = new ArrayList<>();
		collection.add(0);
		collection.add(1);
		collection.add(2);
		collection.add(3);
		collection.add(4);

		System.out.println("====Iterator====");
		Iterator<Integer> iterator = collection.iterator();
		iterator.forEachRemaining(element -> System.out.print(element + ","));
		
		System.out.println();
		System.out.println("====ListIterator====");
		ListIterator<Integer> listIterator = collection.listIterator();
		if(listIterator.hasNext()) {
			System.out.print(listIterator.next() + ",");
		}
		if(listIterator.hasNext()) {
			System.out.print(listIterator.next() + ",");
		}
		
		if(listIterator.hasPrevious()) {
			System.out.print(listIterator.previous() + ",");
		}
		if(listIterator.hasPrevious()) {
			System.out.print(listIterator.previous() + ",");
		}
	}


====Iterator====
0,1,2,3,4,
====ListIterator====
0,1,1,0,

 

Java의 기본 Iterator 인터페이스는 아래와 같이 생겼다. 주석은 모두 생략하였고, default remove 메소드는 Exception을 던지고 있고, default forEachRemaining 은 forEach 문과 같이 Consumer 함수형 인터페이스를 인자로 받아서 반복자의 다음 원소를 action의 accept 메소드에 넘긴다. 

 

public interface Iterator<E> {
   
    boolean hasNext();

    E next();

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

 

그리고 우리가 ArrayList의 인스턴스인 collection에서 iterator() 메소드로 반복자를 얻는 코드는 아래와 같다. Iterator를 구현한 Itr 이라는 private class를 반환한다.

 

public Iterator<E> iterator() {
        return new Itr();
    }

private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

 

collection에서 또 다른 반복자를 얻는 메소드인 listIterator 내부는 아래와 같이 생겼다. 우리가 앞에서 보았떤 Itr 을 상속받는다. 또 ListIterator의 인터페이스를 구현하고 있는데, ListIterator는 Iterator를 구현하며 이전원소를 탐색하거나 이전 원소의 인덱스를 알 수 있는 추가적인 메소드가 더 있다.

 

public ListIterator<E> listIterator() {
        return new ListItr(0);
    }

private class ListItr extends Itr implements ListIterator<E> {
        ListItr(int index) {
            super();
            cursor = index;
        }

        public boolean hasPrevious() {
            return cursor != 0;
        }

        public int nextIndex() {
            return cursor;
        }

        public int previousIndex() {
            return cursor - 1;
        }

        @SuppressWarnings("unchecked")
        public E previous() {
            checkForComodification();
            int i = cursor - 1;
            if (i < 0)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i;
            return (E) elementData[lastRet = i];
        }

        public void set(E e) {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.set(lastRet, e);
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        public void add(E e) {
            checkForComodification();

            try {
                int i = cursor;
                ArrayList.this.add(i, e);
                cursor = i + 1;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
    }

댓글