디자인 패턴 공부 - 이터레이터 패턴

이혁진·2023년 1월 21일
0

이터레이터 패턴

어떤 컬렉션을 순회하는 클라이언트가 있다고 하자. 이러한 클라이언트로부터 컬렉션의 접근과 순회하는 로직을 분리하는 것이 이터레이터 패턴이다. 클라이언트는 컬렉션의 내부 구조를 모르게 되기 때문에, 그 코드를 변경하지 않고도 다양한 순회 방법으로 변경이 용이하다.

구현

여기에서는 Aggregate와 ConcreteAggregate를 분리해서 나타냈지만, 인터페이스 없이 그냥 구상클래스만 만들어도 이터레이터 패턴은 끝이다. 다만 접근, 순회 방식의 변화 뿐만 아니라(이터레이터 변화) 접근하는 컨테이너(Aggregate)에서까지도 유연성을 얻고 싶다면 Aggregate의 인터페이스 역시 추출하는 것이 타당하다. 근데 그건 클라이언트로부터 Aggregate를 분리하는 '다형성'을 활용하는 것일 뿐이고, 이터레이터 패턴의 핵심은 '클라이언트에서 순회, 접근을 분리한다.'라는 것을 명심하자.

일단 핵심은 ConcreteAggregate 별로 ConcreteIterator를 만들어 주고, 같은 Aggregate더라도 순회 방식이 역순이던가 아무튼 바뀌면 그거에 해당하는 ConcreteIterator를 만들어줘도 된다.

대략 무슨 느낌이냐면, 클라이언트가 ConcreteAggregate1이라는 컨테이너 래핑 클래스를 순회한다고 할 때, 그 클래스의 컨테이너가 바뀐다고 해보자.

// 원래꺼
public class ConcreteAggregate1 {
	private List ...
}

// 다음 변화
public class ConcreteAggregate1 {
	private Array ...
}

// 다다음 변화
public class ConcreteAggregate1 {
	private Set ...
}

위처럼 변한다고 할 때,

// 원래꺼
public class Client {
	public void doSomething(ConcreteAggregate1 agg) {
 		List<Item> list = agg.getContainer();
        for(int i = 0; i < list.size(); i++) {
        	System.out.println(list.get(i).info());
        }
    }
}
 
// 다음 변화
public class Client {
	public void doSomething(ConcreteAggregate1 agg) {
    	Item[] items = agg.getContainer();
        for(int i = 0; i < items.length; i++) {
        	System.out.println(items[i].info());
        }
    }
}
  
// 다다음 변화
public class Client {
	public void doSomething(ConcreteAggregate1 agg) {
		Set<Item> set = agg.getContainer();
		for (Item item : set) {
        	System.out.println(item.info());
        }
	}
}

이렇게 코드 전체가 다 바뀐다. 근데 이터레이터 패턴 쓰면,

// 원래꺼
public class Client {
	public void doSomething(ConcreteAggregate1 agg) {
 		Iterator iterator = agg.getIterator();
        while(iterator.hasNext()) {
        	System.out.println(iterator.next());
        }
    }
}
 
// 다음 변화
public class Client {
	public void doSomething(ConcreteAggregate1 agg) {
    	Iterator iterator = agg.getIterator();
        // 배열 순회 Concrete Iterator class 만든 후, getIterator()
        // 에서 리턴 값 바꾸기
        // 1줄 수정, 클래스 1개 확장
        while(iterator.hasNext()) {
        	System.out.println(iterator.next());
        }
    }
}
 
// 다다음 변화
public class Client {
	public void doSomething(ConcreteAggregate1 agg) {
		Iterator iterator = agg.getIterator();
        // 맵 순회 Concrete Iterator class 만든 후, getIterator()
        // 에서 리턴 값 바꾸기
        // 1줄 수정, 클래스 1개 확장
        while(iterator.hasNext()) {
        	System.out.println(iterator.next());
        }
	}
}

OCP를 만족하는 작업 방식이다. 확장 작업은 늘었지만, 기존 코드는 훨씬 덜 변하게 된다. 구체적인 구현까지 포함하여 위 코드를 완성해보자.

public class Item {
	private final String content;
    
    public Item(String content) {
    	this.content = content;
    }
}

public class ConcreteAggregate {
	private final List<Item> list;
    
    public ConcreteAggregate() {
    	this.list = new ArrayList<>();
    ]
    
    public void add(String content) {
    	list.add(new Item(content));
    }
    
    public Iterator getIterator() {
    	return new ItemListIterator(list);
    }
}

public class Client {
	public void doSomething(ConcreteAggregate agg) {
    	Iterator iterator = agg.getIterator();
    	while(iterator.hasNext()) {	
        	System.out.println(iterator.next());
        }
    }
}

public interface Iterator {
	public boolean hasNext();
    public Object next();
}

public class ItemListIterator implements Iterator {
	private final List<Item> list;
    private int cursor;
    
    public ItemListIterator(List list) {
    	this.list = list;
        this.cursor = 0;
    }
    
    @Override
    public boolean hasNext() {
    	return this.cursor != list.size();
	}
    
    @Override
    public Object next() {
    	return list.get(cursor++);
    }
}

장점과 단점

집합 객체(컬렉션)를 클라이언트에서 알 필요가 없다. 인터페이스 구조만 알아도 순회와 접근이 가능하다. 가령 집합 객체가 트리인지, 스택인지, 리스트인지, 힙인지 등 내부 구조를 몰라도 단순히 hasNext와 next만 알면 순회를 할 수 있는 것이다. 또한 순회와 접근을 이터레이터로 분리했으니(SRP) 다수의 클라이언트에서 이터레이터를 의존하는 경우 큰 효율을 얻을 수 있겠고, 순회 방식이 역순, 특정 필드 기준 정렬 등으로 확장될 필요가 있는 경우 좋다.(OCP) 물론 가장 적합한 상황은 언제나 컬렉션이 바뀔 가능성이 높은 경우라고 할 수 있겠다. 단점은 구조가 복잡해진다.

예시1 - java iterator

자바 코드 뜯어보면 모든 컬렉션은 이터레이터를 만들 수 있다.

List<String> list = new ArrayList<>();
Iterator<String> ite = list.iterator();

while(ite.hasNext()) {
	... ite.next() ... ;
}

이런 느낌이고, 새로운 이터레이터를 만들고 싶다면

public class RecentItemIterator implements Iterator<Item> {
	private Iterator<Item> internalIterator;
    
    public RecentIterator(Aggregate agg) {
    	... 정렬 ...
    	this.internalIterator = agg.getItems().iterator();
    }
    
    @Override 
    public boolean hasNext() {
    	return this.internalIterator.hasNext();
	}
    
    @Override
    public Item next() {
    	return this.internalIterator.next();
    }
}

이런 식으로 쓰면 된다.

profile
한양대학교 정보시스템학과 22학번 이혁진 입니다

0개의 댓글