이터레이터(Iterator) 패턴은 컬렉션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는 모든 항목에 접근할 수 있게 해 주는 방법을 제공해 줍니다.
위 그림을 통해 구조를 살펴보면 Client가 사용할 Iterator 인터페이스가 정의되어 있습니다. 이 Iterator는 어떻게 순회해야할지 방법을 가지고 있는 인터페이스입니다. 이 인터페이스에 다음으로 넘어가도록 하는 getNext메서드와 다음 요소들이 있는지 확인하는 hasNext메서드를 정의합니다. 그리고 이 Iterator를 구현한 ConcreteIterator안에 구체적인 로직이 들어가게 됩니다. 오른쪽의 Aggregate인터페이스와 ConcreteAggregate는 집합 객체를 뜻하는데 Aggregate 인터페이스 없이 구체적인 클래스로 존재할 수도 있고 위 그림처럼 Aggregate인터페이스를 구현한 ConcreteAggregate로 존재할 수 있습니다. 이 Aggregate에서 Iterator를 리턴해서 Client코드에서 사용할 수 있도록 할수도 있고 Aggregate필요없이 직접 Iterator를 사용할 수도 있습니다.
이터레이터 패턴적용하기전 예제 코드를 살펴보겠습니다. 게시글을 저장하는 Board클래스 입니다. Post를 가지고있는 집합 객체입니다.
public class Board {
List<Post> posts = new ArrayList<>();
public List<Post> getPosts() {
return posts;
}
public void setPosts(List<Post> posts) {
this.posts = posts;
}
public void addPost(String content) {
this.posts.add(new Post(content));
}
}
게시글을 나타내는 Post클래스 입니다.
public class Post {
private String title;
private LocalDateTime createdDateTime;
public Post(String title) {
this.title = title;
this.createdDateTime = LocalDateTime.now();
}
}
Client코드에서 Board인스턴스를 생성해 3개의 Post를 add했습니다. 그리고 그 Post들을 하나씩 순회할 예정입니다.
public class Client {
public static void main(String[] args) {
Board board = new Board();
board.addPost("디자인 패턴 게임");
board.addPost("선생님, 저랑 디자인 패턴 하나 학습하시겠습니까?");
board.addPost("지금 이 자리에 계신 여러분들은 모두 디자인 패턴을 학습하고 계신 분들입니다.");
}
}
만약 post가 들어간 순서대로 순회를 하고 싶다면 for문으로 차례대로 순회를 하면 됩니다.
List<Post> posts = board.getPosts();
for (int i = 0 ; i < posts.size() ; i++) {
Post post = posts.get(i);
System.out.println(post.getTitle());
}
만약 요구사항이 변경되어 Post의 가장 최신 글을 먼저 순회하고 싶다면 Post의 createDateTime기준으로 내림차순정렬을 하여 순회를 합니다.
Collections.sort(posts, (p1, p2) -> p2.getCreatedDateTime().compareTo(p1.getCreatedDateTime()));
for (int i = 0 ; i < posts.size() ; i++) {
Post post = posts.get(i);
System.out.println(post.getTitle());
}
이런 코드들의 문제는 Board에 들어가있는 Post들을 순회할 때 Board클래스가 어떻게 구성되어 있는지 Client코드에서 알게됩니다. Client코드에서 Board에 Post들을 List로 구현하고 있다는 것을 알 수 있습니다. 그래서 나중에 Board의 List가 Set로 바뀌거나 배열로 바뀐다면 Client코드에도 영향을 받게 됩니다.
List<Post> posts = board.getPosts();
이터레이터 패턴을 사용해서 기존 코드를 개선해 보겠습니다. 기존 코드는 집합객체인 Board의 내부 구조를 알아야만 Client에서 순회를 할 수 있었습니다.
List<Post> posts = board.getPosts();
for (int i = 0 ; i < posts.size() ; i++) {
Post post = posts.get(i);
System.out.println(post.getTitle());
}
하지만 자바는 내부구조와 상관없이 iterator라는 인터페이스를 알고있습니다. iterator를 통해서 순회를 하는 방식으로 바꾸게 되면 위의 코드와는 다르게 Board내부에 Post를 어떤 타입의 데이터구조를 사용하는지 전혀 몰라도 됩니다.
Iterator<Post> iterator = board.getPosts().iterator();
while(iterator.hasNext()){
System.out.println(iterator.next().getTitle());
}
사실 board.getPosts()를 호출하지 않고 Board내부에서 Iterator를 바로 반환해서 사용하도록 할 수 있도록 getDefaultIterator메서드를 정의합니다.
public class Board {
List<Post> posts = new ArrayList<>();
// 코드 생략....
public Iterator<Post> getDefaultIterator() {
return post.iterator();
}
}
이렇게 되면 Client코드에서는 Board인스턴스에서 직접 Iterator를 호출해서 순회를 할 수 있습니다.
public class Client {
public static void main(String[] args) {
Board board = new Board();
board.addPost("디자인 패턴 게임");
board.addPost("선생님, 저랑 디자인 패턴 하나 학습하시겠습니까?");
board.addPost("지금 이 자리에 계신 여러분들은 모두 디자인 패턴을 학습하고 계신 분들입니다.");
Iterator<Post> defaultIterator = board.getDefaultIterator();
while(defaultIterator.hasNext()) {
System.out.println(defaultIterator.next().getTitle());
}
}
}
그렇다면 최신 Post를 순서대로 순회하도록 하는 ConcreteIterator를 구현합니다. Post 스트를 생성자로 주입받아서 createDateTime의 내림차순으로 정렬 후 iterator를 생성하도록 합니다. 그렇게 생성된 internalIterator를 통해서 hasNext와 next를 재정의 합니다.
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();
}
}
이렇게 생성한 Iterator를 Board에서 제공하도록 합니다.
public class Board {
List<Post> posts = new ArrayList<>();
public List<Post> getPosts() {
return posts;
}
public void addPost(String content) {
this.posts.add(new Post(content));
}
public Iterator<Post> getRecentPostIterator() {
return new RecentPostIterator(this.posts);
}
}
개선을 통해서 Client에서는 Board인스턴스에서 getRecentPostIterator메서드를 호출해서 Iterator를 가져와 순회할 수 있습니다. 순회를 할 때 중요한점은 Board의 내부구조가 어떻게 되어있는지 신경쓸 필요가 없습니다.
public class Client {
public static void main(String[] args) {
Board board = new Board();
board.addPost("디자인 패턴 게임");
board.addPost("선생님, 저랑 디자인 패턴 하나 학습하시겠습니까?");
board.addPost("지금 이 자리에 계신 여러분들은 모두 디자인 패턴을 학습하고 계신 분들입니다.");
Iterator<Post> recentPostIterator = board.getRecentPostIterator();
while(recentPostIterator.hasNext()) {
System.out.println(recentPostIterator.next().getTitle());
}
}
}
실행결과 가장 최근에 생성된 Post 먼저 출력되고 있습니다.
지금 이 자리에 계신 여러분들은 모두 디자인 패턴을 학습하고 계신 분들입니다.
선생님, 저랑 디자인 패턴 하나 학습하시겠습니까?
디자인 패턴 게임
이터레이터패턴을 사용하면 집합 객체가 가지고 있는 객체들에 Iterator만을 리턴받아서 손 쉽게 접근할 수 있습니다. 이렇게 되면 Iterator에 정의된 일관된 인터페이스를 사용해 여러 형태의 집합 구조를 순회할 수 있습니다. 하지만 순회방식이 다를때마다 클래스를 새로 생성해서 제공해야하는 점에서 클래스가 늘어나고 복잡도가 증가하게 됩니다.