설계패턴(Design Pattern)

[디자인 패턴] 9. (1) 반복자 패턴 (Iterator Pattern)

긍.응.성 2024. 6. 16. 14:39
반응형

이 글은 헤드퍼스트 디자인패턴과 GoF 디자인패턴을 읽고 정리한 글입니다.

1. 반복자 패턴(Iterator Pattern)

컬렉션의 구현 방법을 노출하지 않으면서 집합체(aggregate object) 내의 모든 항목에 접근하는 방법을 제공하는 패턴

1.1. 구성 요소

Iterator

  • 항목에 접근하며 탐색할 수 있는 인터페이스를 정의합니다.

ConcreteIterator

  • Iterator 인터페이스를 구현합니다.
  • 집합체 항목에서 현재 탐색중인 위치를 관리합니다.

Aggregate

  • 집합체 클래스로 Iterator 객체를 생성하는 인터페이스를 정의합니다.

ConcreteAggregate

  • Aggregate 클래스를 상속하며 Iterator 를 생성하는 메서드를 구현합니다. 자신의 집합체에 대하여 알맞게 탐색할 수 있는 적절한 Iterator 를 생성하도록 합니다.

1.2. 적용 방법

  1. Iterator 패턴에서 사용하는 반복자는 java.util 패키지에 Iterator 인터페이스로 정의되어 있습니다. 이 반복자 대신 새로운 반복자를 정의하고 싶다면 직접 컬렉션에서 각 항목에 접근, 현재 위치를 조회, 추가 항목의 존재 여부 확인을 할 수 있는 인터페이스를 정의합니다.
  2. java.util.Iterator 클래스를 사용하여 집합체 항목에 대하여 반복자 패턴을 적용하고 싶다면 Iterable 인터페이스를 구현하도록 합니다. Iterable 인터페이스를 구현하면 제네릭한 타입에 대하여 반복자를 생성하는 iterator 메서드를 반드시 구현하여야 합니다. 직접 정의한 반복자를 사용하고 싶다면 해당 반복자를 생성하는 인터페이스를 정의하고, 컬렉션에서 이를 구현하도록 합니다.

1.3. 적용 시기

  • 집합체 내부 데이터를 노출시키지 않으며 항목에 접근을 허용하고 싶다면 반복자 패턴을 사용할 수 있습니다.
  • 하나의 집합체에 대하여 하나 이상의 탐색을 지원하고 싶다면 반복자 패턴을 사용할 수 있습니다.
  • 다양한 집합체 구조에 대하여 통일된 탐색 인터페이스를 지원하고 싶다면 반복자 패턴을 사용할 수 있습니다.

1.4. 정리

반복자 패턴은 다양한 집합체의 항목에 대하여 접근, 탐색에 대하여 하나의 책임 단위로 보고 이를 분리한 패턴입니다. 반복자 패턴을 통해 집합체는 항목을 갖고 있는 역할만 수행하여, 탐색을 위해서는 반복자를 사용하여 클라이언트는 통일된 인터페이스로 항목을 접근할 수 있습니다.

반복자 패턴을 적용할 시 다음과 같은 장점을 얻을 수 있습니다.

첫 번째, 반복자 패턴은 다양한 방법의 탐색을 지원할 수 있습니다. Iterator 의 하위클래스를 통해 기존과 다른 방법의 탐색 알고리즘을 제공할 수 있습니다. 두 번째, Iterator 인터페이스를 통해 집합체(Aggregate)에 대한 인터페이스를 더 가볍게 가져갈 수 있습니다. 이는 반복자 패턴이 단일 원칙 패턴에 따라 반복이라는 역할에 대한 컴포넌트를 분리하였기 때문입니다. 마지막 세 번째, 하나 이상의 탐색 상태에 대한 관리가 가능합니다. Aggregate 이 생성하는 Iterator 는 내부적으로 탐색 중인 위치와 같은 상태 정보를 각각 저장하고 있습니다. 그렇기에 Iterator 를 추가로 생성 하더라도 앞서 생성한 Iterator 와는 상태를 공유하지 않기에 하나 이상의 탐색 상태 관리가 가능합니다.

2. 반복자 패턴 구현 시 고려사항

2.1. Who control the iteration? - 반복 행위를 호출하는 자

Iterator 를 통해 실제로 반복을 수행하는 주체에 따라 external iterator와 internal iterator로 구분됩니다. external iterator 는 클라이언트가 반복을 제어하고, internal iterator 는 반복자가 직접 제어합니다.

external iterator가 internal iterator 보다 더 유연합니다. external iterator는 클라이언트가 직접 반복자를 통해 항목에 접근할 수 있기 때문입니다. internal iterator 의 경우 반복을 반복자가 제어한다는 말이 잘 이해가 가지 않을 수 있는데, 클라이언트에서 항목 당 수행해야할 작업을 전달하면 반복자가 스스로 모든 항목에 대하여 작업을 수행해버리는 구조를 갖습니다. 그렇기에 external iterator 에 비하여 유연하지 못하지만 반복에 대한 구현은 직접 하지 않아도 되기에 그 만큼 사용하기 쉽다는 장점이 있습니다.

2.2. Who defines the traversal algorithm? - 탐색 알고리즘의 정의 위치

앞선 내용으로는 Iterator 에서 탐색 알고리즘을 정의하는 것으로 보았지만 집합체에서도 탐색 알고리즘을 구현할 수 있습니다. 집합체가 탐색 알고리즘을 갖는 경우 Iterator 는 현재 상태만을 저장하는 용도로만 사용되며 일반적으로 이러한 역할의 반복자를 Cursor 라고 부릅니다.

Iterator 에서 탐색 알고리즘을 구현한다면 집합체에 대하여 다양한 반복 알고리즘을 제공할 수 있습니다. 또한, 다양한 집합체에 대하여 동일한 탐색 알고리즘을 제공할 수 있다는 장점이 있습니다. 하지만 구현을 위해 탐색 알고리즘은 집합체의 멤버 변수에 대하여 알고 있어야 합니다. 이는 집합체의 캡슐화를 위반하는 행위이기도 합니다.

2.3. How robust it the iterator? - 견고한 반복자

Iterator 를 통해 탐색 중 항목이 추가되거나 삭제될 때 이슈가 있을 수 있습니다. 이에 대하여 견고한 반복자를 robust iterator 라 부릅니다. Iterator 생성 시 집합체의 항목을 모두 복사하여 반복한다면 앞서 언급한 이슈에 대하여 안전하지만, 이는 너무 많은 비용을 초래합니다. 그렇기에 복사를 통한 Iterator 구현은 robust iterator 라고 부르지 않습니다.

robust iterator 를 구현하기 위해서는 집합체에서 반복자에 대한 상태를 직접 조절해주어야 합니다. 항목의 추가나 삭제가 일어나는 경우 반복자의 정상 동작을 위해 상태를 동기화 시켜준다면 해당 반복자는 robust iterator 라 부를 수 있습니다.

2.4. Additional Iterator operations - 반복자 추가 기능

Iterator 인터페이스의 경우 기본적으로 탐색을 위해 first, next, isDone, currentItem 과 같은 메서드가 정의되어있습니다. 하지만 이전 항목에 대하여 탐색할 수 있도록 previous 메서드나, 일정 개수의 항목을 스킵하고 탐색할 수 있도록 skipTo 와 같은 메서드를 제공한다면 더 사용성 좋은 반복자를 제공할 수 있습니다.

2.5. Using polymorphic iterator in C++

다형성을 이용하여 반복자를 반환하는 방법에 대한 내용입니다. 다형성 반복자의 경우 메모리 할당 및 해제에 대하여 주의하여야 하는 내용이 들어있는데, Java의 경우 메모리 관리는 GC를 통해 자동으로 처리되기에 해당 고려사항에 대한 내용은 넘어가겠습니다.

2.6. Iterators may have privileged access

반복자는 관점에 따라 자신을 생성한 집합체의 확장으로 볼 수 있습니다. 반복자는 집합체의 요소를 탐색하기 위해 멤버변수를 알 수도 있으며 실제로 강하게 커플링 되어 있습니다. 그렇기에 반복자를 집합체의 내부 정적 클래스로정의하는 방법도 존재합니다. 실제로 많은 라이브러리에서 반복자가 집합체의 내부 정적 클래스로 선언하고 있습니다.

이 경우 새로운 탐색 알고리즘이 추가된다면 이에 맞게 구현을 변경하는 것이 어려울 수 있는데요, 그런 경우 반복자에 대한 서브클래스를 만들어 제공한다면 확장성까지 갖출 수 있습니다.

2.7. Iterators for composites - 컴포지트 패턴에서의 반복자

Composite 패턴에서 반복자를 구현할 경우, 특히 external iterator 를 구현할 경우 재귀적인 집합체 구조로 어려움이 있습니다. 여기서 어려움이란 여러 level로 구성된 집합체에서 현재의 위치를 파악하는 것을 의미합니다. 그렇기에 Composite 패턴에서의 반복자는 현재 위치를 파악하기 위해 지나온 경로와 현재의 객체를 상태로 관리하여야 합니다.

추가로 Composite 인터페이스에서 관련된 Component 로 이동하기 위한 메서드가 정의되어 있다면 Cursor 기반의 반복자를 사용하는 것이 좋은 대안이 될 수 있습니다(Cursor 반복자이므로 현재 상태만 저장, 탐색은 경우 Aggregate의 역할인 Composite에서 담당하도록 합니다).

2.8. Null iterators - 널 반복자

Null Iterator 는 isDone 메서드의 호출로 항상 true 를 반환하는 반복자입니다. 이는 트리 구조와 같은 특정 집합체에서 사용될 수 있습니다. 대표적인 예로 트리 구조에서 Leaf 노드가 iterator 메서드를 통해 반복자를 생성할 때 NullIterator 를 반환합니다. Leaf 노드의 같은 경우 자식 노드를 가지지 않으므로 반복문을 통해 탐색할 집합체가 없습니다. 이는 클라이언트가 자식 노드를 갖는 Composite 와 자식 노드를 갖지 않는 Leaf 를 하나의 공통된 Iterator 인터페이스를 통해 탐색할 수 있도록 합니다.

반응형