지난 포스팅에선 알고리즘의 골격을 정의하여 일부 단계를 서브클래스에서 구현하게 하는 템플릿 메소드 패턴에 대해 알아보았다. 이번엔 컬렉션의 구현 방법을 노출하지 않으면서도 그 집합체 안에 들어있는 모든 항목에 접근할 수 있게 하는 이터레이터 패턴에 대해 복습한다.

정의

이터레이터 패턴은 집합체 내에서 어떤 식으로 일이 처리되는지에 대해서는 전혀 모르는 상태에서 그 안에 들어있는 모든 항목들에 대해 반복작업을 수행할 수 있게 해준다. **반복자(Iterator)**를 만들어서 이터레이터 패턴을 사용하면 컬렉션 객체 안에 들어있는 모든 항목에 접근하는 방식을 통일할 수 있다.

접근 방식을 통일하게 되면 어떤 종류의 집합체에 대해서도 사용할 수 있는 다형적인 코드를 만들 수 있다. 또 다른 중요한 점은 모든 항목에 일일이 접근하는 작업을 컬렉션 객체가 아닌 반복자 객체에서 맡게 된다는 점이다. 집합체의 인터페이스 및 구현이 간단해질 뿐 아니라, 집합체에서는 반복작업에서 손을 떼고 객체 컬렉션을 관리하는 것과 같은 자신의 작업에만 전념할 수 있게 된다.

자바에서는 List나 Set등의 컬렉션에서 내부 요소를 순차적으로 접근해서 반복적인 작업을 처리할 때, Iterator 인터페이스를 사용한다.

위의 그림의 코드처럼 List, Map, Set 어느 집합체를 사용하던간에 모든 항목에 접근할 때는 Iterator만 있으면 된다. 컬렉션에서는 객체를 관리하는 원래 역할에 집중할 수 있다.

다이어그램

Aggregate 컬렉션 인터페이스는 자바의 컬렉션과 같은 집합체를 의미한다. 객체를 관리하는 집합체들의 공통적인 인터페이스가 있으면 클라이언트와 객체 컬렉션의 구현을 분리할 수 있다.

ConcreateAggregate 컬렉션 구현체에는 객체 컬렉션이 들어있으며 그 안에 들어있는 컬렉션에 대한 반복자를 반환하는 메소드를 구현해야 한다.

Iterator 인터페이스에서는 모든 반복자에서 구현해야 하는 인터페이스를 제공한다. 마찬가지로 클라이언트와 반복자를 분리할 수 있다. 컬렉션에 들어있는 원소들에 돌아가면서 접근할 수 있게 해주는 메소드들을 제공한다.

ConcreateIterator 이터레이터 구현체는 반복작업 중에 현재 위치를 관리하는 일을 맡는다. 모든 ConcreateAggregate는 그 안에 있는 객체 컬렉션에 대해 돌아가면서 반복 작업을 처리할 수 있게 해 주는 ConcreateIterator의 인스턴스를 만들 수 있어야 한다.

예시

이터레이터를 사용하는 이유에 대해서 다시 한번 살펴보자. 웹사이트에서 저그와 테란 유닛들의 정보를 제공한다. Unit은 이름과 종족을 String으로 관리하며, 저그유닛과 테란유닛 목록을 집합체로 관리하는 클래스가 있다. 저그의 목록를 개발한 개발자는 객체들을 관리하는 집합체의 구현에 배열을 사용했으며, 테란의 목록를 개발한 개발자는 객체들을 관리하는 집합체의 구현에 ArrayList를 사용했다. 웹사이트의 printUnits() 메소드는 저그와 테란 유닛의 정보를 모두 출력해야한다. 그런데 유닛 컬렉션 구현 방식이 각각 다르기 때문에 문제가 생긴다.

저그는 배열을 반환하고 테란은 어레이리스트를 반환

리턴형식이 다르다는 것은 서로 다른 형식으로 구현되어 있다는 의미이므로 두 개의 서로 다른 순환문을 만들어야한다. 배열의 순환문을 돌면서 배열의 각 요소를 출력하고 다시 어레이리스트의 순환문을 돌면서 각 요소를 다시 출력해야 한다. 만약 프로토스나 새로운 종족이 추가되면 각 종족에 맞는 순환문이 또 추가되며 코드의 중복이 발생한다.

인터페이스가 아닌 테란, 저그 집합체의 구상 클래스에 맞춰서 코딩하고 있다.

각 구상 클래스에서 서로 다른 방식으로 집합체를 구현했다. 캡슐화의 기본 원칙이 지켜지지 않아 웹사이트에서는 각 종족마다 컬렉션을 표현하는 방법을 알야아 한다. 이 문제의 해결법은 컬렉션과 반복을 분리하여 캡슐화 하는 것이다.

위의 예시에서 코드가 변경되는 부분은 리턴되는 컬렉션의 형식이 다르기 때문에 반복 작업을 하는 방법이 달라지는 지점이다. 배열에서는 length 필드와 배열 첨자를 사용해야 하고, ArrayList에서는 size()와 get() 메소드를 사용해야 한다.

// 저그
Iterator iter = zergUnits.createIterator();
while(iter.hasNext()) {
	Unit unit = (Unit)iter.next();
}

// 테란
Iterator iter = terranUnits.createIterator();
while(iter.hasNext()) {
	Unit unit = (Unit)iter.next();
}

각각 종족 목록에서 Iterator(반복자) 인스턴스를 만들게 되면 해당 Iterator하나로 모든 반복을 통일할 수 있게 된다.

내부 반복자와 외부 반복자

지금까지 살펴본 반복자는 외부 반복자이다. 클라이언트에서 next() 메소드를 호출해서 다음 항목을 가져오기 때문에 클라이언트가 반복작업을 제어한다. 반면에, 내부 반복자는 반복자 자신에 의해서 제어되어야 한다. 반복자가 다음 원소에 대한 작업을 직접 처리하기 때문에 어떤 작업을 처리할 것인지 클라이언트가 알려줘야 한다. 내부 반복자를 사용하는 경우 클라이언트가 반복작업을 마음대로 제어할 수 없기 때문에 외부 반복자를 쓰는 경우에 비해 유연성이 떨어진다. 그러나 할 일을 넘겨주기만 하면 나머지는 반복자에서 알아서 처리해주기 때문에 그게 더 편리할 수도 있다.

의문점

반복자는 반드시 단방향으로 움직여야 하는가?

자바의 컬렉션 프레임워크에 ListIterator라는 반복자 인터페이스가 있다. 이 인터페이스에는 이전 항목으로 가기 위한 previous()와 같은 메소드가 추가 되어있다. 이처럼 반대 방향으로도 움직이는 반복자를 만들어도 상관없다.

반복자에는 특별한 순서가 정해져 있어야 하는가?

List와 같이 중복된 항목이 들어있는 경우도 있고, Hashtable같은 경우에는 객체를 관리하는데 있어 정해진 순서가 없다. 접근 순서라는 것은 사용된 컬렉션의 특성 및 구현과 연관이 있을 뿐이다. 컬렉션 문서에 특별하게 언급이 되어있지 않은 이상 순서에 대해서는 그 어떤 것도 가정하면 안 된다.

반복자를 이용한 다형적인 코드가 무엇인가?

Iterator를 매개변수로 받아들이는 메소드를 만들면 다형성에 의해 반복작업을 통일할 수 있게 된다. Iterator를 지원하기만 하면 어떤 컬렉션에 대해서도 써먹을 수 있는 코드를 만들게 되는 것이다. 컬렉션의 구현 방식에는 신경쓰지 않고 원하는 반복작업을 수행할 수 있게 된다.

단일 역할 원칙과의 연관성은 무엇인가?

이터레이터 패턴에서 마지막 원칙인 SRP가 등장한다. 클래스를 바꾸는 이유는 단 한 가지 뿐이어야 한다는 원칙이다. 컬렉션은 집합체를 관리하는 역할을 가진다. 이 안에 반복자를 처리하는 다른 역할을 부여하게 되면 추후에 이 클래스가 변경되는 이유는 두 가지가 된다. 첫 번째는 컬렉션이 어떤 이유로 인해 바뀌었을 때, 두 번째는 반복자 관련 기능이 바뀌었을 때이다. 이터레이터 패턴은 컬렉션으로부터 반복자 작업을 캡슐화하여 분리한다.

응집도(cohesion)

응집도란 클래스 또는 모듈이 특정 목적과 역할을 얼마나 일관되게 지원하는지를 나타내는 척도다. 어떤 모듈 또는 클래스의 응집도가 높다는 것은 일련의 서로 연관된 기능이 묶여있다는 것을 의미하며 응집도가 낮다는 것은 관련없는 기능들이 묶여있다는 것을 의미한다.

단일 역할 원칙을 잘 따르는 클래스는 두 개 이상의 역할을 맡고 있는 클래스에 비해 응집도가 높고, 관리하기에 더 용이한 편이다.

이터레이터 패턴 예제