반복자(Iterator) 패턴
반복자(Iterator) 패턴은 컬렉션 객체의 내부 구조를 노출하지 않고도 그 안의 요소를 순차적으로 접근할 수 있게 해주는 행동 디자인 패턴입니다. 이 패턴을 사용하면 다양한 자료구조에서 일관된 방식으로 요소를 순회할 수 있습니다.
문제점: 컬렉션 구조와 순회 코드의 결합
컬렉션 객체의 요소를 순회하는 로직이 컬렉션 클래스에 포함되면, 컬렉션의 내부 구조에 의존하는 코드가 발생합니다.
이로 인해
- 컬렉션이 변경될 때 순회 코드도 변경해야 합니다.
- 동일한 방식으로 다른 종류의 컬렉션을 순회하기 어렵습니다.
class Book {
private String title;
public Book(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
class BookShelf {
private Book[] books;
private int count = 0;
public BookShelf(int size) {
books = new Book[size];
}
public void addBook(Book book) {
books[count++] = book;
}
public Book getBookAt(int index) {
return books[index];
}
public int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) {
BookShelf bookShelf = new BookShelf(3);
bookShelf.addBook(new Book("Design Patterns"));
bookShelf.addBook(new Book("Refactoring"));
bookShelf.addBook(new Book("Clean Code"));
// 컬렉션의 내부 구조에 의존한 순회 코드
for (int i = 0; i < bookShelf.getCount(); i++) {
System.out.println(bookShelf.getBookAt(i).getTitle());
}
}
}
문제점
- 컬렉션의 내부 구조에 의존: BookShelf의 배열 인덱스와 크기를 직접 사용합니다.
- 유연성 부족: 만약 BookShelf의 내부 구조가 배열에서 리스트로 바뀐다면 순회 코드를 수정해야 합니다.
- 다른 컬렉션과의 일관성 문제: 다른 자료구조를 순회할 때도 매번 다른 방식으로 코드를 작성해야 합니다.
해결 방법: 반복자 패턴 적용
반복자 패턴을 사용하면 컬렉션 객체와 순회 로직을 분리합니다. **반복자 객체(Iterator)**가 순회 작업을 전담하여 컬렉션의 내부 구조를 클라이언트에게 노출하지 않습니다.
Iterator 인터페이스 정의
interface Iterator<T> {
boolean hasNext();
T next();
}
구체적인 Iterator 구현
class BookShelfIterator implements Iterator<Book> {
private BookShelf bookShelf;
private int index = 0;
public BookShelfIterator(BookShelf bookShelf) {
this.bookShelf = bookShelf;
}
@Override
public boolean hasNext() {
return index < bookShelf.getCount();
}
@Override
public Book next() {
return bookShelf.getBookAt(index++);
}
}
Iterable 인터페이스와 컬렉션 클래스 구현
interface Iterable<T> {
Iterator<T> iterator();
}
class BookShelf implements Iterable<Book> {
private Book[] books;
private int count = 0;
public BookShelf(int size) {
books = new Book[size];
}
public void addBook(Book book) {
books[count++] = book;
}
public Book getBookAt(int index) {
return books[index];
}
public int getCount() {
return count;
}
@Override
public Iterator<Book> iterator() {
return new BookShelfIterator(this);
}
}
클라이언트 코드
public class Main {
public static void main(String[] args) {
BookShelf bookShelf = new BookShelf(3);
bookShelf.addBook(new Book("Design Patterns"));
bookShelf.addBook(new Book("Refactoring"));
bookShelf.addBook(new Book("Clean Code"));
// 반복자를 사용해 일관된 방식으로 순회
Iterator<Book> iterator = bookShelf.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next().getTitle());
}
}
}
반복자 패턴의 동작 원리
- 컬렉션 객체는
iterator()
메서드를 통해 반복자 객체를 반환합니다. - 반복자 객체는
hasNext()
와next()
메서드를 통해 요소를 순회합니다. - 클라이언트는 컬렉션의 내부 구조를 몰라도 동일한 방식으로 요소를 접근할 수 있습니다.
반복자 패턴의 장단점
장점
- 내부 구조 은닉: 컬렉션의 내부 구조를 클라이언트에게 노출하지 않습니다.
- 일관된 인터페이스: 다양한 자료구조에 대해 동일한 방식으로 순회할 수 있습니다.
- 유연한 확장성: 새로운 자료구조를 추가해도 순회 방식은 변경되지 않습니다.
- 단일 책임 원칙(SRP) 준수: 컬렉션은 데이터 저장에 집중하고, 반복자는 순회에 집중합니다.
단점
- 복잡성 증가: 단순한 컬렉션에서는 반복자 객체를 별도로 구현해야 하므로 코드가 복잡해질 수 있습니다.
- 성능 저하 가능성: 반복자를 생성하는 데 약간의 오버헤드가 발생할 수 있습니다.
결론
Iterator 패턴은 컬렉션의 내부 구조에 상관없이 일관된 방식으로 요소를 순회할 수 있도록 도와주는 유용한 패턴입니다. 이 패턴은 컬렉션과 순회 로직을 분리해 유연성과 확장성을 높이며, 단일 책임 원칙(SRP)을 준수합니다.
하지만, 단순한 자료구조에서는 반복자를 별도로 구현하는 것이 과도한 복잡성을 유발할 수 있으므로 상황에 따라 적절히 사용하는 것이 좋습니다. 이 패턴은 특히 컬렉션 프레임워크나 데이터베이스 처리, 파일 시스템 탐색 등에서 자주 활용됩니다.