Java/Design Pattern

[디자인 패턴] 행동 패턴 - 이터레이터 패턴 (반복자 패턴 - Iterator Pattern)

민돌v 2023. 4. 4. 11:30
(인프런) 코딩으로 학습하는 GoF의 디자인 패턴 - 백기선, 강의를 보고 정리한 글입니다.
코드는 GitHub 에 있습니다

 

#1. 객체 생성 관련 패턴

#2. 구조 관련 패턴

 

#3. 행동 관련 패턴

 

 

 


✔️ 이터레이터 패턴이란 (Iterator Patterns)

집합 객체 내부 구조를 노출시키지 않고 순회 하는 방법을 제공하는 패턴

 

  • 반복자 패턴의 본질은 기반이 되는 표현을 노출시키지 않고 연속적으로 객체 요소에 접근하는 방법을 제공하는 것 입니다.
  • 다시말해 aggregate 유형에 무관한 동일 순차 접근 방법을 제공하는 것이며, 여기서 aggregate란 반복자객체를 생성하기위한 인터페이스를 정의하는 것이고 iterator 란 요소에 접근 할 수 있고 순회 할 수 있는 인터페이스를 정의하는 것 입니다.
  • 👏🏻 쉽게말해, 컬렉션 객체 안에 들어있는 모든 항목에 접근하는 방식이 통일되어 있으면 어떤 종류의 집합체에 대해서도 사용할 수 있는 다형적인 코드를 만들수 있고
  • 이러한 이터레이터 패턴을 사용하면 집합 객체를 순회하는 클라이언트 코드를 변경하지 않고 다양한 순회 방법을 제공할 수 있습니다.

 

 

이터레이터 아키텍쳐 구조

 

  • Iterator - 집합체의 요소들을 순서대로 검색하기 위한 인터페이스 정의
  • ConcreateIterator - Iterator 인터페이스를 구현함
  • Aggregate - 여러 요소들로 이루어져 있는 집합체,  Iterator의 역할을 만드는 인터페이스를 정합니다.
  • ConcreateAggregate - Aggreagate 인터페이스를 구현하는 클래스

 

 


✔️ 이터레이터 패턴 사용 예시

반복자 패턴을 사용하지 않았을 때와 사용하였을 때를, 비교 구현해보겠습니다.
자바의 iterator 인터페이스를 사용하였습니다.

 

1) 이터레이터 패턴 적용 전

  • List를 가지고 있는 Board 클래스가 존재하고, 해당 클래스의 멤버변수인 List를 클라이언트에서 호출하고 순회하는 코드를 작성해보가자 합니다.
@Getter
public class Board {

    private final List<Post> posts = new ArrayList<>();

    public void addPost(String title) {
        posts.add(new Post(title, LocalDateTime.now()));
    }
}

@Getter
@AllArgsConstructor
public class Post {

    private String title;
    private LocalDateTime createdDateTime;

}

 

  • 클라이언트는 Board 클래스의 리스트를 호출해 와, List 형식이라는 것을 알고, 순회하고 있습니다.
  • 만약 board 에서 넘겨주는 멤버변수가 List 가 아닌, Tree 나 이중 리스트 등으로 변경되었을 때, 이를 호출하고 순회하고 있는 클라이언트 쪽에서도 모두 변경되어져야 합니다.
  • 이러한 문제점을 Iterator패턴을 사용하여 개선할 수 있습니다.
public class Client {

    public static void main(String[] args) {
        Board board = new Board();
        board.addPost("디자인 패턴 게임");
        board.addPost("선생님, 저랑 디자인 패턴 하나 학습~ㄱ?");
        board.addPost("지금 이자리에 계신 여러분들은 모두 디자인 패턴 학습 ㄱㄱ");

        //TODO 들어간 순서대로 순회하기 → 클라이언트가 보드가 list 의 구조라는 것을 알고있어야 하는 순회
        // → list 타입이 아닌 다른 무엇인가로 변경된다면 클라이언트 코드도 변경되어야함
        List<Post> posts = board.getPosts();
        for (int i = 0; i < posts.size(); i++) {
            Post post = posts.get(i);
            System.out.println(post.getTitle());
        }

        //TODO 가장 최신 글 먼저 순회하기
        Collections.sort(posts, (p1, p2) -> p2.getCreatedDateTime().compareTo(p1.getCreatedDateTime()));
    }
}

 

2) 이터레이터 패턴 적용 후

  • 가장 단순하게 iterator 를 적용하는 방법입니다.
  • Board 가 가지고있는 멤버변수의 리스트를 호출할 때 iterator로 변환해주어 조회합니다.
  • 이렇게 해서 Client 가 iterator 를 사용해 호출한다면, 요구사항이 변경되거나 내부구조가 변경되더라도 iterator 타입을 통해서 조회하고 요소들을 소비할 수 있습니다.
public class Client {

    public static void main(String[] args) {

        //TODO iterator 순회 내부가 List
        // Iterator 를 사용하면 보드 내부에 어떤 타입이 존재하는지 알지 않아도 됨
        Board board = new Board();
	board.addPost("디자인 패턴 게임");
        board.addPost("선생님, 저랑 디자인 패턴 하나 학습~ㄱ?");
        board.addPost("지금 이자리에 계신 여러분들은 모두 디자인 패턴 학습 ㄱㄱ");
        
        
        List<Post> posts = board.getPosts();
        Iterator<Post> iterator = posts.iterator();
        System.out.println(iterator.getClass());
        
         //TODO 가장 최신 글 먼저 순회하기
        // board 자체에서 순회하기 위한 Iterator 를 제공하는 방법
        Iterator<Post> recentPostIterator = board.getDefaultIterator();
        while (recentPostIterator.hasNext()) {
            System.out.println(recentPostIterator.next().getTitle());
        }
    }
}

@Getter
public class Board {

    private final List<Post> posts = new ArrayList<>();

    public void addPost(String title) {
        posts.add(new Post(title, LocalDateTime.now()));
    }

    public Iterator<Post> getDefaultIterator() {
        return posts.iterator();
    }
}

 

만약 Board 가 가지고있는List<Post> 를 생성순으로 정렬하여 조회하고 싶다. 라는 요구사항의 변경이 생성된다면 Board 에서 다른 Iterator 를 반환해주어 변경을 최소화 시킬 수 있습니다.

Board.Class

@Getter
public class Board {

    private final List<Post> posts = new ArrayList<>();

    public void addPost(String title) {
        posts.add(new Post(title, LocalDateTime.now()));
    }

    public Iterator<Post> getDefaultIterator() {
        return posts.iterator();
    }

    public Iterator<Post> getRecentPostIterator() {
        return new RecentPostIterator(posts);
    }
}

리스트를 정렬한 iterator를 반환

public class RecentPostIterator implements Iterator<Post> {

    private Iterator<Post> internalIterator;

    public RecentPostIterator(List<Post> posts) {
        Collections.sort(posts, (p1, p2) -> p2.getCreatedDateTime().compareTo(p1.getCreatedDateTime()));
        this.internalIterator = posts.iterator();
    }

    @Override
    public boolean hasNext() {
        return this.internalIterator.hasNext();
    }

    @Override
    public Post next() {
        return this.internalIterator.next();
    }

}

Client.class

public class Client {

    public static void main(String[] args) {

        //생략
        
         //board 의 메소드 부분만 변경하면 됨
        Iterator<Post> recentPostIterator = board.getRecentPostIterator();
        while (recentPostIterator.hasNext()) {
            System.out.println(recentPostIterator.next().getTitle());
        }
    }
}

 

 

→ Board 는 지금 ConcreateAggregate 의 역할을 수행하고 있습니다.

이러한 클라이언트의 변경마저도 최소화 하고 싶다면 Aggreate 인터페이스를 정의하고 클라이언트는 Aggregate인터페이스만을 바라본다면 조금 더 변경을 최소하 하는 구조로 점진적으로 개선해 나갈 수 있을 것 같습니다.

 


✔️ 이터레이터 패턴 장단점

장점

  • 집합체 클래스의 응집도를 높여준다.
  • 집합체 내에서 어떤 식으로 일이 처리되는지 알 필요 없이, 집합체 안에 들어있는 모든 항목에 접근 할 수 있게 해준다.
  • 모든 항목에 일일이 접근하는 작업을 컬렉션 객체가 아닌 이터레이터 객체에서 맡게 된다. 이렇게 하면, 집합체의 인터페이스 및 구현이 간단해질 뿐만 아니라, 집합체에서는 반복 작업에서 손을 떼고 원래 자신이 할 일에만 전념할 수 있다.

 단점

  • 단순한 순회를 구현하는 경우 클래스만 많아져 구조 복잡도가 증가할 수 있다.

 

이렇게 이터레이터 패턴도 공부해보았습니다!

실무에서 이터레이터 패턴을 어떻게 적용할 수 있을까 고민해보았는데

저라면, 일급 컬렉션 객채로 사용하고 있는 클래스에서, 외부에 조회하고 소비만 하는 리스트를 호출할 때 조금 더 명확하게 iterator 를 제공해준다면 변경에 더 유연하고 응집도가 높은 객체가 되겠구나 생각이 들었습니다

 

 

그럼 여기서 끝내겠습니다!