본문 바로가기

OOP, FP/디자인패턴

헤드퍼스트 디자인 패턴: 10. 이터레이터 패턴


지난 포스팅에선 알고리즘의 골격을 정의하여 일부 단계를 서브클래스에서 구현하게 하는 템플릿 메소드 패턴에 대해 알아보았다.


이번엔 컬렉션의 구현 방법을 노출하지 않으면서도 그 칩합체 안에 들어있는 모든 항목에 접근할 수 있게 하는 이터레이터 패턴에 대해 복습한다.


이터레이터 패턴

집합체 내에서 어떤 식으로 일이 처리되는지에 대해서는 전혀 모르는 상태에서 그 안에 들어있는 모든 항목들에 대해 반복작업을

수행할 수 있게 해주는 패턴.


반복자(Iterator)를 만들어서 이터레이터 패턴을 사용하면 컬렉션 객체 안에 들어있는 모든 항목에 접근하는 방식을 통일할 수 있다.

이렇게 접근 방식을 통일하게 되면 어떤 종류의 집합체에 대해서도 사용할 수 있는 다형적인 코드를 만들 수 있다.


또 다른 중요한 점은 모든 항목에 일일이 접근하는 작업을 컬렉션 객체가 아닌 반복자 객체에서 맡게 된다는 점이다.

이를 통해 집합체의 인터페이스 및 구현이 간단해질 뿐 아니라, 집합체에서는 반복작업에서 손 뗴고 원래 자신이 할 일

(객체 컬렉션을 관리하는 역할)에만 전념할 수 있게 된다.


우리는 흔히 자바에서 List나 Set등의 컬렉션에서 내부 요소를 순차적으로 접근해서 출력한다거나 하는 반복적인 작업을 처리할 때,

Iterator 인터페이스를 사용한다.




위와 같이 List, Map, Set 어느 집합체를 사용하던간에 모든 항목에 접근할 때는 Iterator만 있으면 된다.

put, add와 같이 컬렉션에서는 객체를 관리하는 원래 역할만 수행할 수 있게 되었다.


그동안 사용해오던 이터레이터 패턴의 다이어그램을 살펴보자.


(1) Aggregate 

- 컬렉션 인터페이스는 자바의 컬렉션과 같은 집합체를 의미한다.

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


(2) ConcreateAggregate

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


(3) Iterator

- 이터레이터 인터페이스에서는 모든 반복자에서 구현해야 하는 인터페이스를 제공한다. 

  마찬가지로 클라이언트와 반복자를 분리할 수 있다.

  컬렉션에 들어있는 원소들에 돌아가면서 접근할 수 있게 해주는 메소드들을 제공한다.


(4) ConcreateIterator

- 이터레이터 구현체는 반복작업 중에 현재 위치를 관리하는 일을 맡는다.


* 모든 ConcreateAggregate는 그 안에 있는 객체 컬렉션에 대해 돌아가면서 반복 작업을 처리할 수 있게 해 주는 ConcreateIterator의 인스턴스를 만들 수 있어야 한다.



이터레이터를 사용하는 이유에 대해서 다시 한번 살펴보자.





좀 이상하지만 우선 어떤 홈페이지에서 저그와 테란 유닛들의 정보를 관리한다고 치자.

Unit에는 자신의 이름과 종족이 있다.(종족을 String으로 관리하는건 이상하지만 일단.....)


저그유닛과 테란유닛 목록을 집합체로 관리하는 클래스가 있다.

저그의 목록를 개발한 개발자는 객체들을 관리하는 집합체의 구현에 배열을 사용했으며

테란의 목록를 개발한 개발자는 객체들을 관리하는 집합체의 구현에 어레이리스트를 사용했다.


웹사이트에서 printUnits()이라는 메소드는 저그와 테란 유닛의 정보를 모두 출력해야한다.

그런데 유닛 컬렉션 구현 방식이 각각 다르기 때문에 문제가 생긴다.

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

(2) 각각 리턴형식이 다르다는 것은 서로 다른 형식으로 구현되어 있다는 의미이므로 두 개의 서로 다른 순환문을 만들어야한다.

- 배열의 순환문을 돌면서 배열의 각 요소를 출력하고 다시 어레이리스트의 순환문을 돌면서 각 요소를 다시 출력해야 한다....


여기서 프로토스나 새로운 종족이 추가되면 각 종족에 맞는 순환문이 또 추가되어야 한다.

그런데 그 순환문은 전부 비슷한 형식이므로 결국 코드 중복이 되버린다.


왜 이런 문제가 생길까?

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

(2) 각 구상 클래스에서 서로 다른 방식으로 집합체를 구현했다.

(3) 캡슐화의 기본 원칙이 지켜지지 않아 웹사이트에서는 각 종족마다 컬렉션을 표현하는 방법을 알야아 한다. 

(4) printUnits()에서 두 개의 순환문의 코드가 중복된다.


결국 이 문제의 해결법은 컬렉션과 반복을 분리하여 캡슐화 하는 것이다.

캡슐화의 기본 원칙은 바뀌는 부분을 캡슐화하는 첫 번째 원칙이다.


위의 예시에서 바뀌는 부분은 각 종족 목록에서 리턴되는 컬렉션의 형식이 다르기 때문에 반복 작업을 하는 방법이 달라지는 부분이다.

배열에서는 length 필드와 배열 첨자를 사용해야 하고, 어레이리스트에서는 size()와 get() 메소드를 사용해야 한다.


결국 반복 작업을 하기 위한 부분을 캡슐화해야한다.

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


저그

Iterator iter = zergUnits.createIterator();

while(iter.hasNext()) {

Unit unit = (Unit)iter.next();

}


테란

Iterator iter = terranUnits.createIterator();

while(iter.hasNext()) {

Unit unit = (Unit)iter.next();

}



의문점

(1) 내부 반복자와 외부 반복자는 무엇인가?

- 우선 지금까지 살펴본 반복자는 외부 반복자이다. 클라이언트에서 next()를 호출해서 다음 항목을 가져오기 때문에

  클라이언트가 반복작업을 제어한다.

  반면에, 내부 반복자는 반복자 자신에 의해서 제어되어야 한다. 반복자가 다음 원소에 대해서 어떤 작업을 직접 처리하기 때문에

  모든 원소에 대해서 어떤 일을 할 것인지 클라이언트가 알려줘야 한다.

  내부 반복자를 사용하는 경우 클라이언트가 반복작업을 마음대로 제어할 수 없기 때문에 외부 반복자를 쓰는 경우에 비해

  유연성이 좀 떨어진다. 하지만 할 일을 넘겨주기만 하면 나머지는 반복자에서 알아서 처리해주기 때문에 그게 더 편리할 수도 있다.


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

- 자바의 컬렉션 프레임워크에 ListIterator라는 반복자 인터페이스가 있다.

  이 인터페이스에는 이전 항목으로 가기 위한 previous()와 같은 메소드가 추가 되어있다.

  이와 같이 반대 방향으로도 움직이는 반복자를 만들어도 상관없다.


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

- List와 같이 중복된 항목이 들어있는 경우도 있고, Hashtable같은 경우에는 객체를 관리하는데 있어 정해진 순서가 없다.

  따라서 접근 순서라는 것은 사용된 컬렉션의 특성 및 구현하고 연관이 있을 뿐이다.

  컬렉션 문서에 특별하게 언급이 되어있지 않은 이상 순서에 대해서는 그 어떤 것도 가정하면 안 된다.


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

- 예를 들어 Iterator를 매개변수로 받아들이는 메소드를 만들면 다형성에 의해 반복작업을 통일할 수 있게 된다.

  Iterator를 지원하기만 하면 어떤 컬렉션에 대해서도 써먹을 수 있는 코드를 만들게 되는 것이다.

  컬렉션의 구현 방식에는 신경쓰지 않고 원하는 반복작업을 수행할 수 있게 된다.



단일 역할 원칙

이터레이터 패턴에서 마지막 원칙인 단일 역할 원칙이 등장한다.

클래스를 바꾸는 이유는 단 한 가지 뿐이어야 한다는 원칙이다.


컬렉션의 역할은 집합체를 관리하는 역할이다. 그런데 이 안에 반복자를 처리하는 다른 역할을 부여하게 되면

추후에 이 클래스를 바꾸어야 하는 이유가 두 가지가 된다.

첫 번째는 컬렉션이 어떤 이유로 인해 바뀌었을 때, 두 번째는 반복자 관련 기능이 바뀌었을 때이다.

이터레이터 패턴을 사용하면 컬렉션으로부터 반복자 작업을 캡슐화시킬 수 있다.


이러한 이유로 인해 "변경"과 관련된 단일 역할 원칙이 이터레이터 패턴에 등장한 것 같다.


응집도(cohesion)

응집도란 클래스 또는 모듈이 특정 목적 또는 역할을 얼마나 일관되게 지원하는지를 나타내는 척도다.

어떤 모듈 또는 클래스의 응집도가 높다는 것은 일련의 서로 연관된 기능이 묶여있다는 것을,

응집도가 낮다는 것은 서로 상관 없는 기능들이 묶여있다는 것을 의미한다.


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



컬렉션의 구현 방법을 노출하지 않으면서도 그 칩합체 안에 들어있는 모든 항목에 접근할 수 있게 하는 이터레이터 패턴에 대해 복습을 마치겠다.


1. 스트래티지 패턴

2. 옵저버 패턴

3. 데코레이터 패턴

4-1. 팩토리 메서드 패턴

4-2. 추상 팩토리 패턴

5. 싱글턴 패턴

6. 커맨드 패턴

7. 어댑터 패턴

8. 퍼사드 패턴

9. 템플릿 메소드 패턴

10. 이터레이터 패턴