본문 바로가기
IT/Java

[Java] Java Iterator Pattern

by kyu-nahc 2024. 9. 15.
반응형

 

Iterator Pattern

반복자(Iterator) 패턴은 일련의 데이터 집합에 대하여 순차적인 접근(순회)을 지원하는 패턴이다.

데이터 집합이란 객체들을 그룹으로 묶어 자료의 구조를 취하는 컬렉션을 말한다.

대표적인 컬렉션으로 리스트나 트리, 그래프, 테이블.. 등이 있다.

 

요소를 다양하게 저장하는 컬렉션

 

보통 배열이나 리스트 같은 경우 순서가 연속적인 데이터 집합이기 때문에

간단한 for문을 통해 순회할 수 있다. 그러나 해시, 트리와 같은 컬렉션은 데이터 저장 순서가

정해지지 않고 적재되기 때문에, 각 요소들을 어떤 기준으로 접근해야 할지 애매해진다.

예를 들어 아래와 같이 트리 구조가 있다면 어떤 상황에선 깊이(세로)를 우선으로

순회해야 할 수도 있고, 너비(가로)를 우선으로 순회할 수도 있기 때문이다.

 

어떤 기준으로 순회할지 컬렉션마다 다를수 있다

 

이처럼 복잡하게 얽혀있는 자료 컬렉션들을 순회하는 알고리즘 전략을 정의하는 것을

이터레이터 패턴이라고 한다. 컬렉션 객체 안에 들어있는 모든 원소들에 대한 접근 방식이

공통화되어 있다면 어떤 종류의 컬렉션에서도 이터레이터만 뽑아내면

여러 전략으로 순회가 가능해 보다 다형적인 코드를 설계할 수 있게 된다.

자바의 컬렉션 프레임워크(JCF)에서 각종 컬렉션을 무리 없이 순회할 수 있는 것도

내부에 미리 이터레이터 패턴이 적용되어 있기 때문이다.

 

이밖에도 이터레이터 패턴은 별도의 이터레이터 객체를 반환받아

이를 이용해 순회하기 때문에, 집합체의 내부 구조를 노출하지 않고 순회할 수 있다는 장점도 있다.

 

Iterator Pattern 구조

 

 

  • Aggregate
    ConcreateIterator 객체를 반환하는 인터페이스를 제공한다.
    iterator() : ConcreateIterator 객체를 만드는 팩토리 메서드
  • ConcreateAggregate
    여러 요소들이 이루어져 있는 데이터 집합체
  • Iterator
    집합체 내의 요소들을 순서대로 검색하기 위한 인터페이스를 제공한다.
    hasNext() : 순회할 다음 요소가 있는지 확인 (true / false)
    next() : 요소를 반환하고 다음 요소를 반환할 준비를 하기 위해 커서를 이동시킴
    remove() : 가장 마지막의 요소를 제거
  • ConcreateIterator
    ConcreateAggregate가 구현한 메서드로부터 생성되며, 
    ConcreateAggregate의 컬렉션을 참조하여 순회한다.
    어떤 전략으로 순회할지에 대한 로직을 구체화한다.
// 반복체 객체
public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}
// 집합체 객체 (컬렉션)
public interface Aggregate<E> {
    Iterator<E> iterator();
}
// 반복자 개체를 구현하는 구현체
// 각 순환에 대한 로직을 재정의 할 수 있다.
public class ConcreteIterator<E> implements Iterator<E> {

    private final List<E> data;
    private int currentIndex = 0;

    public ConcreteIterator(List<E> data){
        this.data = data;
    }

    @Override
    public boolean hasNext() {
        return currentIndex < data.size();
    }

    @Override
    public E next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        return data.get(currentIndex++);
    }

    @Override
    public void remove() {
        if (!data.isEmpty()){
            data.removeLast();
        }else{
            throw new NoSuchElementException();
        }
    }
}

// 집합체 인터페이스를 구현하는 구현체
// 생성자로 순회하고 싶은 데이터를 전달받는다.
public class ConcreteAggregate<E> implements Aggregate<E>{

    private List<E> data;

    public ConcreteAggregate(List<E> data){
        this.data=  data;
    }

    // 내부 컬렉션을 인자로 넣어 이터레이터 구현체를 클라이언트에 반환
    @Override
    public Iterator<E> iterator() {
        return new ConcreteIterator<>(data);
    }
}
public class Client {

    public static void main(String[] args) {

        List<Integer> integerList = List.of(1,2,3,4,5,6,7,8,9,10);
        // 집합체 생성
        ConcreteAggregate<Integer> integerAggregate = new ConcreteAggregate<>(integerList);
        // 반복자를 집합체로부터 전달받는다.
        Iterator<Integer> iterator = integerAggregate.iterator();
        while(iterator.hasNext()){
            System.out.print(iterator.next()+"->");
        }
        System.out.println();
        List<String> stringList = List.of("A","B","C","D");
        // 집합체 생성
        ConcreteAggregate<String> stringAggregate = new ConcreteAggregate<>(stringList);
        // 반복자를 집합체로부터 전달받는다.
        Iterator<String> iterator1 = stringAggregate.iterator();
        while(iterator1.hasNext()){
            System.out.print(iterator1.next()+"->");
        }
    }
}

 

 

Iterator Pattern 특징

패턴 적용 시기

  • 컬렉션에 상관없이 객체 접근 순회 방식을 통일하고자 할 때
  • 컬렉션을 순회하는 다양한 방법을 지원하고 싶을 때
  • 컬렉션의 복잡한 내부 구조를 클라이언트로부터 숨기고 싶은 경우 (편의 + 보안)
  • 데이터 저장 컬렉션 종류가 변경 가능성이 있을 때
    클라이언트가 집합 객체 내부 표현 방식을 알고 있다면,
    표현 방식이 달라지면 클라이언트 코드도 변경되어야 하는 문제가 생긴다.

패턴 장점

  • 일관된 이터레이터 인터페이스를 사용해
    여러 형태의 컬렉션에 대해 동일한 순회 방법을 제공한다.
  • 컬렉션의 내부 구조 및 순회 방식을 알지 않아도 된다.
  • 집합체의 구현과 접근하는 처리 부분을 반복자 객체로 분리결합도가 감소한다.
    Client에서 iterator로 접근하기 때문에 ConcreteAggregate 내에 수정 사항이 생겨도
    iterator에 문제가 없다면 문제가 발생하지 않는다.
  • 순회 알고리즘을 별도의 반복자 객체에 추출하여 각 클래스의 책임을 분리하여
    단일 책임 원칙(SRP)을 준수한다.
  • 데이터 저장 컬렉션 종류가 변경되어도 클라이언트 구현 코드는 손상되지 않아 수정에는 닫혀 있어
    개방 폐쇄 원칙(OCP)을 준수한다.

패턴 단점

  • 클래스가 늘어나고 복잡도가 증가한다.
    만일 앱이 간단한 컬렉션에서만 작동하는 경우 패턴을 적용하는 것은 복잡도만 증가할 수 있다.
    이터레이터 객체를 만드는 것이 유용한 상황인지 판단할 필요가 있다.
  • 구현 방법에 따라 캡슐화를 위배할 수 있다.

 

Iterator Pattern 적용 예시

위의 구조 예시 코드를 보면 아직 Iterator Pattern이 굳이 왜 사용되는지 이해하지 못할 수 있다.

적용 예시를 통해 Iterator Pattern을 사용하는 이유에 대해 이해할 수 있다.

만약 Project에서 Review라는 데이터가 존재하고 해당 Review를 클라이언트 입장에서 

어떤 기준을 통하여 Review 데이터를 정렬하여 순회하고 싶다고 하자.

public class Review {
    private int rating;
    private LocalDate time;

    public int getRating() {
        return rating;
    }

    public LocalDate getTime() {
        return time;
    }

    public Review(int rating, LocalDate time) {
        this.rating = rating;
        this.time = time;
    }

    public String toString(){
        return "Review Rating : " +rating+"Review Time : "+time;
    }
}

 

Review 객체가 다음과 같이 있을 때,

Review 객체를 별점 높은 순으로 정렬하기 위해서

Client (main)에서 Review 정렬 및 순회 함수 코드를 구현해야 한다.

public class Client {

    public static void main(String[] args) {

       List<Review> reviews = new ArrayList<Review>();
       reviews.add(new Review(5, LocalDate.of(2024,9,15)));
       reviews.add(new Review(4, LocalDate.of(2024,3,20)));
       reviews.add(new Review(1, LocalDate.of(2024,2,2)));

       reviews.sort(Comparator.comparing(Review::getRating).reversed());
        for(Review review : reviews) {
           System.out.println(review);
       }
    }
}

 

일반적인 for Each문을 통해 집합체의 요소들을 순회하였다.

그러나 이러한 구성 방식은 List에 들어간 Review를 순회할 때,

Review가 어떠한 구조로 이루어져 있는지를 클라이언트에 노출된다.

따라서 이를 보다 객체 지향적으로 구성하기 위해 이터레이터 패턴을 사용하는 것이다.

public class RatingReviewIterator implements Iterator<Review> {

    private Iterator<Review> iterator;

    public RatingReviewIterator(List<Review> reviews) {
        reviews.sort(Comparator.comparing(Review::getRating).reversed());
        this.iterator = reviews.iterator();
    }

    @Override
    public boolean hasNext() {
        return this.iterator.hasNext(); // 자바 내부 이터레이터에 위임
    }

    @Override
    public Review next() {
        return this.iterator.next(); // 자바 내부 이터레이터에 위임
    }
}
public class TimeReviewIterator implements Iterator<Review> {

    private Iterator<Review> iterator;

    public TimeReviewIterator(List<Review> reviews) {
        reviews.sort(Comparator.comparing(Review::getTime));
        this.iterator = reviews.iterator();
    }

    @Override
    public boolean hasNext() {
        return this.iterator.hasNext(); // 자바 내부 이터레이터에 위임
    }

    @Override
    public Review next() {
        return this.iterator.next(); // 자바 내부 이터레이터에 위임
    }
}

 

위의 구조 예시에서 Iterator 인터페이스를 직접 만들어 썼지만,

자바에선 이미 이터레이터 인터페이스를 지원한다.

자바의 내부 이터레이터를 재활용해서 메서드 위임을 통해 코드를 간단하게 구현할 수도 있다.

순회 전략으로는 리뷰를 날짜 순서대로 조회 / 별점 순서대로 조회 두 가지가 존재한다.

따라서 이에 대한 이터레이터 클래스 역시 두 가지 생성해 주면 된다.

  • RatingReviewIterator : 별점 순서 이터레이터
  • TimeReviewIterator : 날짜 순서 이터레이터
public interface ReviewAggregate {
    Iterator<Review> ratingTypeIterator();
    Iterator<Review> timeTypeIterator();
}
public class ReviewConcreteAggregate implements ReviewAggregate{

    private List<Review> reviews;

    public ReviewConcreteAggregate(List<Review> reviews) {
        this.reviews = reviews;
    }

    @Override
    public Iterator<Review> ratingTypeIterator() {
        return new RatingReviewIterator(reviews);
    }

    @Override
    public Iterator<Review> timeTypeIterator() {
        return new TimeReviewIterator(reviews);
    }
}

 

그리고 RatingReviewIterator와 TimeReviewIterator객체를 반환하는 

ReviewConcreteAggregate 클래스를 생성하면 된다.

해당 클래스는 ReviewAggregate 인터페이스를 구현하며,

ReviewAggregate에는 각 Iterator를 반환하는 추상 메서드가 존재한다.

따라서 또 다른 기준의 정렬 Iterator가 필요하다면

기존의 코드를 수정하지 않고, Iterator를 반환하는 메서드만 추가해 주면 된다.

public class Client {

    public static void main(String[] args) {

       List<Review> reviews = new ArrayList<Review>();
       reviews.add(new Review(5, LocalDate.of(2024,9,15)));
       reviews.add(new Review(4, LocalDate.of(2024,3,20)));
       reviews.add(new Review(1, LocalDate.of(2024,2,2)));

       ReviewAggregate aggregate = new ReviewConcreteAggregate(reviews);
       Iterator<Review> timeTypeIterator = aggregate.timeTypeIterator();
       Iterator<Review> ratingTypeIterator = aggregate.ratingTypeIterator();

       while(timeTypeIterator.hasNext()) {
           System.out.println(timeTypeIterator.next());
       }
       while(ratingTypeIterator.hasNext()) {
           System.out.println(ratingTypeIterator.next());
       }
    }
}

 

이제 클라이언트는 게시글을 순회할 때 Review 내부가

어떤 집합체로 구현되어 있는지 알 수 없게 감추고 전혀 신경 쓸 필요가 없게 되었다.

그리고 순회 전략을 각 객체로 나눔으로써 때에 따라 적절한 이터레이터 객체만 받으면

똑같은 이터레이터 순회 코드로 다양한 순회 전략을 구사할 수 있게 되었다.


Iterator를 사용하는 또 다른 이유는 이터레이터를 사용함으로써 집합체 구현과 분리할 수 있기 때문이다.
이터레이터 객체를 반환하면 컬렉션을 순회할 때 hasNext()와 next()라는 

Iterator의 메서드만을 이용하기 때문에, 집합체인 Review의 내부 구성을 감출 수 있게 된다.

즉, 위의 while문은 Review의 구현에 의존하지 않는 것이다.
이 말은 만일 추후에 Review의 집합체를 수정하더라도 Review클래스가

올바른 Iterator 만을 반환해 준다면 클라이언트의 코드(위의 while 루프)는 변경하지 않아도 되게 된다.

 

 

참고자료

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EB%B0%98%EB%B3%B5%EC%9E%90Iterator-%ED%8C%A8%ED%84%B4-%EC%99%84%EB%B2%BD-%EB%A7%88%EC%8A%A4%ED%84%B0%ED%95%98%EA%B8%B0

https://www.geeksforgeeks.org/iterator-pattern/

반응형

'IT > Java' 카테고리의 다른 글

[Java] Java Composite Pattern  (11) 2024.09.16
[Java] Java Template Method Pattern  (1) 2024.09.16
[Java] Java Adapter Pattern  (1) 2024.09.15
[Java] Java Singleton Pattern  (2) 2024.09.14
[Java] Java Command Pattern  (0) 2024.09.13

loading