본문 바로가기
PHP

[PHP] IteratorAggregate - 루프에서 사용될 때 어떤 이터레이터를 사용할지 위임하기.

by teamnova 2025. 11. 3.
728x90

 

[개념 설명]


IteratorAggregate는 PHP의 SPL(Standard PHP Library)에 내장된 인터페이스입니다. 어떤 객체가 이 인터페이스를 구현하면, foreach 루프에서 사용될 때 어떻게 반복(iterate)해야 하는지를 다른 이터레이터 객체에게 위임할 수 있습니다.
IteratorAggregate는 단 하나의 메소드, getIterator()만 구현하면 됩니다. 이 메소드는 반드시 Traversable 인터페이스(주로 Iterator 또는 다른 IteratorAggregate 객체)를 구현한 객체를 반환해야 합니다.


[왜 Iterator 대신 IteratorAggregate를 사용할까요?]


Iterator 인터페이스는 rewind(), current(), key(), next(), valid()라는 5개의 메소드를 모두 직접 구현해야 합니다. 이는 반복 로직이 복잡하거나, 객체 내부에서 여러 방식으로 반복을 제공해야 할 때 코드를 지저분하게 만들 수 있습니다.
IteratorAggregate를 사용하면 다음과 같은 큰 이점을 얻을 수 있습니다.


1. 캡슐화: 객체의 내부 데이터 구조(예: private 배열)를 외부에 노출하지 않고도 안전하게 반복 기능을 제공할 수 있습니다. foreach는 getIterator()가 반환한 이터레이터를 사용할 뿐, 원래 객체의 내부를 직접 건드리지 않습니다.


2. 코드의 단순함: 이미 존재하는 이터레이터를 반환하기만 하면 되므로, 복잡한 5개의 메소드를 직접 구현할 필요가 없습니다. 코드가 훨씬 간결하고 명확해집니다.


3. 유연성: 동일한 객체라도 상황에 따라 다른 이터레이터를 반환하도록 로직을 구성할 수 있습니다. (예: 정순, 역순, 필터링된 이터레이터 등)

 

예제 1: 기본 사용법 - 도서관의 책 목록 순회하기
도서관 객체가 내부에 책 목록을 private 배열로 가지고 있다고 가정해 봅시다.

이 책 목록을 외부에서 직접 접근하지 못하게 하면서 foreach로 순회하고 싶습니다.

 

IteratorAggregate를 사용하지 않은 경우

class Book {
    public function __construct(public string $title) {}
}

class Library {
    private array $books = [];

    public function addBook(Book $book): void {
        $this->books[] = $book;
    }

    // 반복을 위해 내부 배열을 반환하는 public 메소드를 만들어야 함.
    // 이는 캡슐화를 깨뜨리는 좋지 않은 설계!
    public function getBooks(): array {
        return $this->books;
    }
}

$library = new Library();
$library->addBook(new Book('Clean Code . 1'));
$library->addBook(new Book('Design Patterns . 2'));

// 캡슐화를 깨고 내부 배열을 직접 받아서 반복해야 함.
foreach ($library->getBooks() as $book) {
    echo $book->title . PHP_EOL;
}

 

IteratorAggregate를 사용

getBooks() 같은 public 메소드 없이도 객체 자체를 foreach에 사용할 수 있습니다.

class Book {
    public function __construct(public string $title) {}
}

// IteratorAggregate 인터페이스를 구현
class Library implements IteratorAggregate
{
    private array $books = [];

    public function addBook(Book $book): void {
        $this->books[] = $book;
    }

    // getIterator() 메소드를 구현해야 함
    public function getIterator(): Traversable
    {
        // 내부 $books 배열을 감싸는 ArrayIterator 객체를 반환.
        // ArrayIterator가 모든 반복 로직을 알아서 처리해 줌.
        return new ArrayIterator($this->books);
    }
}

$library = new Library();
$library->addBook(new Book('Clean Code . 1'));
$library->addBook(new Book('Design Patterns . 2'));

// 이제 Library 객체 자체를 직접 foreach 루프에 사용할 수 있습니다!
// 외부에서는 $books 배열의 존재를 전혀 알 필요가 없음.
echo "도서관의 책 목록:" . PHP_EOL;
foreach ($library as $book) {
    echo "- " . $book->title . PHP_EOL;
}

 

[제너레이터(Generator)와 함께 사용하기]

Iterator 인터페이스를 자동으로 구현하는 가장 간결한 방법입니다. getIterator()에서 제너레이터 함수를 사용하면 코드를 더욱 단순화할 수 있습니다.

 

class Book {
    public function __construct(public string $title) {}
}

class Library implements IteratorAggregate
{
    private array $books = [];

    public function addBook(Book $book): void {
        $this->books[] = $book;
    }

    // getIterator는 제너레이터를 반환할 수도 있음.
    public function getIterator(): Traversable
    {
        // 제너레이터를 사용하여 역순으로 책을 yield(양보)함.
        for ($i = count($this->books) - 1; $i >= 0; $i--) {
            yield $this->books[$i];
        }
    }
}

$library = new Library();
$library->addBook(new Book('Book A'));
$library->addBook(new Book('Book B'));
$library->addBook(new Book('Book C'));

echo "책 목록 (역순):" . PHP_EOL;
foreach ($library as $book) {
    echo "- " . $book->title . PHP_EOL;
}

 

객체를 명시적으로 생성하는 대신 제너레이터 yield 키워드를 사용하여 반복 로직을 구현.

복잡한 순회 로직을 구현할 때 유연합니다.