Notice
Recent Posts
Recent Comments
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

Iterator Pattern / Iterable Interface 본문

Design Pattern/Behavioral Pattern

Iterator Pattern / Iterable Interface

defacto standard 2018. 2. 15. 19:56

1. 가정


 - 책을 등록하고, 등록한 책의 내용을 모두 출력하는 프로그램을 작성한다.

 - 책은 4개 존재한다.


 

2. Naive Code

 - Book

public class Book {
private String name;

public Book(String name){
this.name = name;
}

public String getName() {
return this.name;
}

}

책에 대한 클래스이다.


 - NaiveBookShelf1

public class NaiveBookShelf1 {
private Book[] books;
private int last = 0;

public NaiveBookShelf1(int maxsize){
this.books = new Book[maxsize];
}

public Book getBookFromNaiveBookShelf1At(int index){
return this.books[index];
}

public void appendBook(Book book){
this.books[last++] = book;
}

public int getLengthFromNaiveBookShelf1(){
return this.last;
}
}

배열을 사용하여 잭을 저장하고, index값을 주면 해당 자리에 위치한 책을 반환한다.

 

 - NaiveBookPrinter

public class NaiveBookPrinter {
NaiveBookShelf1 naiveBookShelf1;

public void setNaiveBookShelf(NaiveBookShelf1 naiveBookShelf1){
this.naiveBookShelf1 = naiveBookShelf1;
}

public void print() {
for(int i=0; i<naiveBookShelf1.getLengthFromNaiveBookShelf1(); i++){
Book book = naiveBookShelf1.getBookFromNaiveBookShelf1At(i);
System.out.println(book.getName());
}
}
}

BookShelf를 세터로 받아서 저장하고, print 메서드에서는 for문을 통해 책의 정보를 받아 출력한다.

 

- NaiveClient

public class NaiveClient {
public static void main(String args[]) {
NaiveBookShelf1 naiveBookShelf1 = new NaiveBookShelf1(4);


naiveBookShelf1.appendBook(new Book("책1"));
naiveBookShelf1.appendBook(new Book("책2"));
naiveBookShelf1.appendBook(new Book("책3"));
naiveBookShelf1.appendBook(new Book("책4"));


NaiveBookPrinter naiveBookPrinter = new NaiveBookPrinter();
naiveBookPrinter.setNaiveBookShelf(naiveBookShelf1);
naiveBookPrinter.print();

}
}

결과는 다음과 같다.

 

3. 문제점

 

 

현재 NaiveBookShelf1은 Array를 사용하여 Book을 저장한다.

그리고, NaiveBookPrinter는 이 NaiveBookShelf1에서 정보를 얻어서 출력을 하게 되어있다.

 

하지만 NaiveBookPrinter의 경우, NaiveBookShelf1가 아닌 다른 다른 자료구조를 사용하게 된다면 문제가 생길 수 있다.


NaiveBookShelf1 아니라 NaiveBookShelf2라는 자료구조를 사용한다고 생각해보자. 


예를 들어서 다음과 같은 요구사항의 변화가 생겼다고 생각해보자.

 - 책이 추가되는 데 갯수 제한 없이 추가가 유연해야한다.

 

기존의 NaiveBookShelf1은 배열이기 때문에 자료 저장의 한계가 있고, 유연하지가 않다.


이 요구사항을 만족하기 위해, NaiveBookShelf2의 경우는 Array가 아닌, Collection Framework에서 ArrayList를 사용한다.


 - NaiveBookShelf2

public class NaiveBookShelf2 {
private List<Book> books;

public NaiveBookShelf2(){
this.books = new ArrayList<>();
}

public Book getBookFromNaiveBookShelf2At(int index){
return this.books.get(index);
}

public void appendBook(Book book){
this.books.add(book);
}

public int getLengthFromNaiveBookShelf2(){
return this.books.size();
}
}


 이 자료구조를 사용하여 BookPrinter를 사용하려면 NaiveBookShelf1과 관련된 메서드와, 레퍼런스를 변경해야한다.


이는 유지보수를 힘들게 하는 주 원인이다.



4. 해결방안


현재 문제가 되는 부분은, 자료를 순회하여 조회하는 기능을 가진 BookPrinter의 재사용이 불가능 하다는 것이다.


그 원인은 BookPrinter와 NaiveBookShelf1 (또는 2) 간의 강한 커플링이다.


따라서 NaiveBookShelf1, 2에 대한 추상적인 개념을 정의하고, BookPrinter에서는 이 추상적인 개념을 사용하여 연산하는 것이 좋다.


여기서 고려할 것이 한가지 더 있다.


NaiveBookShelf1의 경우에는 Array

NaiveBookShelf2의 경우에는 ArrayList를 사용한다는 차이점이 있다.


이 외에도 (Collection Framework에 한정하여) Vector, LinkedList 등과 같은 여러가지 자료구조로 상황에 따라 얼마든지 변경될 수 있다.


이러한 자료구조까지 고려하여 클래스의 관계에 대한 재설계가 필요하다.


여기서 2가지 경우를 생각해볼 수 있다.

1. 추상 클래스를 통해 추상화

2. 인터페이스를 통해 추상화


2가지 모두 장단점이 있지만, 추상클래스보단 인터페이스를 사용하는 것이 '보통의 경우' 더 유리하다.


그 이유는 다음과 같다. Java에 한정한다.


 - 추상 클래스는 단일 상속만을 지원하기에, 추상 클래스를 무조건 상속을 받아야 한다.

 이 경우, 만약 다른 클래스를 상속받고 있었다면 복잡한 상속도를 가져야 한다.

반면에, 인터페이스는 다중 구현이 가능하기 때문에 유연하다.


 - 공통적인 연산은 필요하지만 상속이 부적절한 경우(예를 들어 IS-A 관계가 성립되지 않는 경우) 인터페이스를 사용하면 해결할 수 있다.


따라서, BookShelf라는 개념을 추상화 시키되, 순회에 관련된 부분 역시 추상화 시킬 수 있는 인터페이스가 적절하다.

위에서 말했듯이, 인터페이스를 활용하여 클래스 간 결합도를 감소(디커플링)시키고, 순회 로직을 추상화 시킨다.


로직을 추상화시키는 이유는, 구현체마다 순회 로직에 사용되는 메서드명이 다를 수 있기 때문에, 이를 일관성있는 메서드명으로 맞추기 위함이다.


BookShelf1, 2를 추상화 시킨 개념을 Aggregate(집합체) 라고 한다. 

그리고 BookShelf1, 2와 같은 자료구조를 ConcreteAggregate(구현 집합체)라고 한다.


전자의 경우 Collection Framework에서 'Collection'인터페이스에 해당하며,

후자의 경우 Collection Framework에서 'ArrayList', 'LinkedList' 등에 해당한다.



Aggregate는 단순히 BookShelf에 대한 print 로직을 추상화 하는 것이 아니다.

순회에 필요한 인덱스를 다루는 개체를 리턴하는 메서드를 선언하는데, 이 개체를 Iterator(반복자) 라고 한다.


print 로직을 추상화 하는 것이 아니라 Iterator 개체를 리턴하는 이유는, '로직의 추상화를 클래스로 다루어 결합도를 낮추기 위함'이다.


위에서 말했듯이 ConcreteAggregate1, 2는 자료구조가 다르다. 따라서, 인덱스가 주어진다면 해당 인덱스로 값을 가져오는 방법이 다르다.


예를 들어

Array같은 경우는 array[index] 와 같은 방식으로 가져오고

ArrayLsit같은 경우는 arrayList.get(index)와 같은 방식으로 가져온다.

(ArrayList의 내부 구현은 고려하지 않는다.)


따라서, Iterator는 특정 자료구조만을 지원할 필요가 있는데, 이를 ConcreteIterator라고 한다.

Printer 클래스는 이 ConcreteIterator가 바인딩된, 추상화된 Iterator를 활용하여 순회하는 로직을 짜게 된다.


모든 ConcreteAggregate가 이러한 Iterator를 통해 순회하도록 구현한다면, 어떤 ConcreteAggregate가 추가되거나 변경되더라도

Printer 클래스는 변경되지 않는다.


자바에서는 ForEach를 사용하여 순회연산이 가능하기도 한데, 이 역시도 Iterator Pattern과 연관되어있다.

이 부분에 대해서는 Iterable Interface의 사용법을 알려주는 8번 항목을 참고하기 바란다.


5. Solution Code


 - Aggregate

// 반복자인 Iterator를 만들어내는 인터페이스를 결정.
// 추상적인 집합체를 나타내므로 Collection에 해당
public interface Aggregate {
public Iterator iterator();
}

집합체의 추상적 개념이다.


단순히 로직을 추상화하기 위해 추상 클래스의 단점을 안으면서 까지 사용하는 것 보단, 인터페이스를 활용하여 다중 구현에 좀 더 힘을 실어주기 위해 interface를 사용하였다.


이 인터페이스가 말하는 것은 '이 인터페이스를 구현하는 구체 집합체 ConcreteAggregate는 Iterator를 제공한다'는 것을 의미한다.


Collection Framework에서 'Collection' 인터페이스에 해당한다.


 - ConcreteAggregate1, 2

// ConcreteIterator1 인스턴스를 만들어냄
// Collection의 ArrayList 등에 해당
// 구체 집합체 - Array로 구현
public class ConcreteAggregate1 implements Aggregate {
private Book[] books;
private int last = 0;

public ConcreteAggregate1(int maxsize) {
this.books = new Book[maxsize];
}

public Book getBookAt(int index) {
return books[index];
}

public void appendBook(Book book) {
this.books[last++] = book;
}

public int getLength(){
return last;
}

@Override
public Iterator iterator() {
return new ConcreteIterator1(this);
}
}
// ConcreteIterator2 인스턴스를 만들어냄
// Collection의 ArrayList 등에 해당
// 구체 집합체 - List Interface로 구현
public class ConcreteAggregate2 implements Aggregate {
private List<Book> books;

public ConcreteAggregate2() {
this.books = new ArrayList<Book>();
}

public Book getBookAt(int index) {
return books.get(index);
}

public void appendBook(Book book) {
this.books.add(book);
}

public int getLength(){
return this.books.size();
}

@Override
public Iterator iterator() {
return new ConcreteIterator2(this);
}
}

구체집합체이다. 내부 구현이 각 자료구조에 맞추어져있다.


여기서는 소스코드의 일관성을 위해 ConcreteAggregate의 메서드 명을 동일하게 하였지만, Naive Code의 예제처럼 구현체에 따라 얼마든지 달라질 수 있다.


중요한 것은, 로직을 추상화 한다면 이러한 ConcreteAggregate와의 결합도가 떨어져 메서드명은 이 ConcreteAggregate의 인덱스 연산을 지원하는 ConcreteIterator에만 영향이 가고, Printer 클래스는 변함이 없다는 점이다.


ConcreteAggreagte는 Aggregate로부터 상속받은 메서드인 iterator()를 구현하여야 하는데,

이 메서드는 해당 ConcreteAggregate에 대한 인덱스 연산을 지원하는 ConcreteIterator를 반환한다.


Collection Framework에서 ArrayList, Vector, LinkedList에 해당한다.


- Iterator

// 요소를 순서대로 검색해가는 인터페이스를 결정
// 인덱스를 다루는 로직을 추상화한다.
public interface Iterator {
// '다음 요소'가 존재하는지를 조사
public boolean hasNext();

// '다음 요소'를 얻어옴
public Object next();
}

인덱스를 다루는 로직을 추상화한 Iterator Interface이다.


 - ConcreteIterator1, 2

// Iterator가 결정한 인터페이스를 실제로 구현, 검색을 위해 필요한 정보를 가지고 있어야 함
// 구체적인 개념인 ConcreteAggregate에 대한 검색을 지원한다.
public class ConcreteIterator1 implements Iterator {
private ConcreteAggregate1 concreteAggregate1;
private int index;

public ConcreteIterator1(ConcreteAggregate1 concreteAggregate1){
this.concreteAggregate1 = concreteAggregate1;
this.index = 0;
}

@Override
public boolean hasNext() {

if(index < concreteAggregate1.getLength())
return true;
else
return false;
}

@Override
public Object next() {
Book book = concreteAggregate1.getBookAt(index++);
return book;
}
}
// Iterator가 결정한 인터페이스를 실제로 구현, 검색을 위해 필요한 정보를 가지고 있어야 함
// 구체적인 개념인 ConcreteAggregate에 대한 검색을 지원한다.
public class ConcreteIterator2 implements Iterator {
private ConcreteAggregate2 concreteAggregate2;
private int index;

public ConcreteIterator2(ConcreteAggregate2 concreteAggregate2){
this.concreteAggregate2 = concreteAggregate2;
this.index = 0;
}

@Override
public boolean hasNext() {

if(index < concreteAggregate2.getLength())
return true;
else
return false;
}

@Override
public Object next() {
Book book = concreteAggregate2.getBookAt(index++);
return book;
}
}

특정 구현체(ConcreteAggregate1, 2)에 대한 인덱스 관련 연산을 지원한다. 


hasNext()메서드는 다음 요소가 있는지 없는지에 대한 정보를 제공하며

next()메서드는 현재 포인터(index값)가 가리키는 요소를 반환한 후, 다음 요소로 포인터(index값)를 옮긴다.


만약, 자료구조체에서 마지막 요소를 반환한 다음 index값이 증가한다면, hasNext()에서 false를 리턴하여 더이상 진행하지 않게된다.


이를 응용하여서, 홀/짝수 번째의 요소 등을 얻어내는 Iterator도 작성이 가능하다.

물론, 이 로직은 추상개념인 Iterator를 사용하기 때문에 역시 Printer 클래스에는 영향이 없다.

 - Printer

public class BookPrinter {
private Aggregate aggregate;

public void setAggregate(Aggregate aggregate) {
this.aggregate = aggregate;
}

public void print(){
Iterator iterator = aggregate.iterator();

while(iterator.hasNext()){
Book book = (Book)iterator.next();
System.out.println(book.getName());
}
}
}

추상 집합체인 Aggregate를 DI받아 추상 반복자 Iterator를 활용해 요소를 꺼내어 프린트를 하는 클래스이다.

추상적인 개념만 사용하므로, 구체적인 클래스(ConcreteAggregate, ConcreteIterator)의 변경에도 소스코드가 변하지 않는다.


6. Iterator Pattern

Iterator는 순회하는 것에 특화되어있다. 

구현체와 순회로직을 추상화하여 디커플링을 도모하여 자료구조의 변경에도 이를 사용하는 클래스의 변경은 일어나지 않는다.


자바에서는 Iterable, Iterator라는 인터페이스로 구현되어있다.



- UML & Sequence Diagram




7. 장단점

장점 : 자료구조가 순회하는데 사용하는 메서드를 숨길 수 있다.

자료구조가 변경되더라도, Iterator만 제공된다면 되기 때문에 이를 순회하는 로직을 사용하는 클래스의 소스코드의 변경이 없다.

(자바 한정) 이미 제공하는 인터페이스를 사용하면 쉽게 구현이 가능하다.


단점 : 단순한 순회를 구현하는 경우 클래스만 많아져 복잡도가 증가할 수 있다.



8. Iterator/Iterable Interface

사실 자바에서는 Iterator Pattern을 제공하고 있어서, 실제로 위와같은 Iterator, Aggregate 등을 개발자가 이를 구현할 필요는 없다.

 

아무리 클래스를 새로 만든다고 하여도 내부적으로는 보통 List, Map, Set과 같은 Collection Framework를 사용하여 개발을 하는데, Collection Framework의 많은 구현체가 Iterable Interface를 구현한다.


즉, 자바에서 Iterator Pattern은 Collection Framework의 구현체에 이미 구현되어있다. 사용할지 말지는 개발자의 선택이다.


그렇다면 Iterable 인터페이스를 사용하는 방법에 대해 알아보자.


다음 소스코드는 동작하지 않는다.

 - Client

public class Client {
public static void main(String args[]) {
StringNumber stringNumber = new StringNumber();

stringNumber.addStringNumebr("One");
stringNumber.addStringNumebr("Two");
stringNumber.addStringNumebr("Three");

for(String string : stringNumber){
System.out.println(string.toString());
}
}
}

위 소스코드에서 Advanced-For (또는 forEach라고 부르기도 한다)를 사용하여 stringNumber에 저장된 값을 하나씩 제공받아서 출력하기 위함이다.


 - StringNumber

public class StringNumber{
List<String> stringList;

public StringNumber() {
this.stringList = new ArrayList<String>();
}

public void addStringNumebr(String stringNubmer) {
this.stringList.add(stringNubmer);
}
}

StringNumber에서는 생성자와 String 타입 객체를 추가하는 것 외에는 어떠한 메서드도 제공하지 않는다.


위 소스코드에서, Client에서는 컴파일 에러를 뿜는다.

그 이유는 Advanced-For는 Iterable 인터페이스를 구현한 자료구조만 지원하기 때문이다.


따라서, StringNumber 클래스에 Iterable 인터페이스를 구현할 필요가 있다.

Iterable 인터페이스를 구현하는 경우, 반드시 iterator()라는 메서드를 구현해야한다.


그리고, 어떤 타입을 지원하는지를 알리기 위해 Iterable의 제네릭 타입을 알려야한다.

수정된 StringNumber는 다음과 같다.

 - StringNumber

public class StringNumber implements Iterable<String>{
List<String> stringList;

public StringNumber() {
this.stringList = new ArrayList<String>();
}

public void addStringNumebr(String stringNubmer) {
this.stringList.add(stringNubmer);
}

@Override
public Iterator<String> iterator() {
return this.stringList.iterator();
}
}

이제 Client에서 컴파일 에러가 사라지며, 수행을 시키면 다음과 같은 결과가 나온다.


만약, Iterator Interface를 사용하고 싶다면, iterator()를 통해 Iterator를 얻어와서 Advanced-For가 아니라 while을 사용해도 상관없다.

Printer에서 Iterator를 사용하는 경우는 다음과 같이 작성되게 된다.


public class StringNumberPrinter {
StringNumber stringNumber;

public void setStringNumber(StringNumber stringNumber) {
this.stringNumber = stringNumber;
}

public void print() {
// Using Advanced - For : need "? implements Iterable"
for(String string : stringNumber){
System.out.println(string);
}

// Using Iterator
Iterator iterator = stringNumber.iterator();
while(iterator.hasNext()) {
String string = (String)iterator.next();
System.out.println(string);
}
}
}


결과는 다음과 같다.


'Design Pattern > Behavioral Pattern' 카테고리의 다른 글

Mediator Pattern  (0) 2018.02.16
Chain Of Responsibility Pattern  (0) 2018.02.16
Template Method Pattern (기본)  (0) 2017.09.26
Observer Pattern  (0) 2017.09.26
Command Pattern  (0) 2017.09.26
Comments