개발/java

Iterator design pattern

소확행개발자 2020. 5. 28. 16:56

흔히 개발할때 컬렉션 프레임워크를 사용해서 데이터를 처리할때 for 문을 사용했었다.

 

그렇게 개발하던 도중에 다음과 같은 로직이 필요했고 지난번과 같이 for 문으로 처리할 계획을 했다.

 

요구사항

도서관 에서 '칼의 노래'는 제거하고 나머지는 2 를 붙이는 요구사항이 왔으면 어떻게 할 것인가?

List<Book> books;

    @Before
    public void setUp() throws Exception {

        books = new ArrayList<>();

        Book book = new Book("해리포터");
        books.add(book);

        Book book2 = new Book("반지의 제왕");
        books.add(book2);

        Book book3 = new Book("헝거게임");
        books.add(book3);

        Book book4 = new Book("종이여자");
        books.add(book4);

        Book book5 = new Book("칼의노래");
        books.add(book5);

    }


@Test(expected = ConcurrentModificationException.class)
    public void forEachTest() {

        for(Book book : books) {

            if(book.getName().equals("칼의노래")){
                books.remove(book);
            } else {
                String name = book.getName();
                book.setName(name + "2");
            }
        }

    }

다음과 같이 생각할 수도 있다.

하지만 위와 같은 exception 이 발생한다.

 

이를 해결하려고 다음과 같은 해결책을 제시했다. 

@Test
public void iteratorTest(){

        Iterator<Book> bookIterator = books.iterator();

        while(bookIterator.hasNext()){

            Book book = bookIterator.next();

            if(book.getName().equals("칼의노래")){
                bookIterator.remove();
            } else {
                String name = book.getName();
                book.setName(name + "2");
            }

        }

        Assert.assertEquals(4, books.size());


    }

여기서 iterator 에 대한 관심을 가지게 되었다.

 

그때 때마침 iterator 디자인 패턴에 대해서 공부하게 되었다.

 

내가 만드는 구조는 다음과 같다.

 

public interface Iterator<T> {

    boolean hasNext();
    T next();

}

 

Iterator 인터페이스를 만들고 

abstract method 를 정의한다.

 

public interface Aggregate<T> {

    Iterator<T> iterator();

}

 

그리고 해당 Iterator 를 구현하는 interface 를 만든다.

 

/**
 *
 * 서가 ( 도서관 ) 를 나타내는 클래스이다.
 *
 *
 */
public class BookShelf implements Aggregate<Book> {

    private Book[] books;
    private int last = 0;

    public BookShelf(int maxSize){
        this.books = new Book[maxSize];
    }

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

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

    public int size(){
        return last;
    }

    @Override
    public Iterator<Book> iterator() {

        return new Itr();
    }


    /**
     * 비정적 멤버 클래스의 인스턴스와 바깥 인스턴스 사이의 관계는 멤버 클래스가 인스턴스화될 때 확립되며, 더 이상 변경할 수 없다.
     * 이 관계는 바깥 클래스의 인스턴스 메서드에서 비정적 멤버 클래스의 생성자를 호출할 때 자동으로 만들어지는게 보통이다.
     *
     * 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static 을 붙여서 정적 멤버 클래스로 만들자.
     *
     * static 을 생략하면 바깥 인스턴스로의 숨은 외부 참조를 갖게 된다.
     * -> 이 참조를 저장하려면 시간과 공간이 소비된다.
     * 그리고 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있다는 점이다.
     *
     * 참조가 눈에 보이지 않으니 문제의 원인을 찾기 어려워 때떄로 심각한 문제를 발생시킬 수 있다.
     *
     * 결론
     *
     * 멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적으로,
     * 그렇지 않으면 정적으로 만들자.
     */
    private class Itr implements Iterator<Book> {

        int index;

        @Override
        public boolean hasNext() {
           return index != books.length;
        }


        @Override
        public Book next() {

            if(index >= books.length){
                // runtime exception 인 이유 설명
                throw new NoSuchElementException();
            }

            Book book = books[index];
            index++;


            return book;


        }
    }
}

  

마지막으로 도서관 class 를 구현하면 iterator 디자인 패턴이 완성된다.

 

여기서 Iterator 는 어디서 본적이 있지 않은가 ?

 

실제로 우리가 흔히 사용하는 Collection 프레임워크가 Aggregate 역할이 있다.

 

즉 Collection framework 에 iterator 디자인 패턴이 적용된 것이다.

 

그렇다면 왜 Iterator 를 사용할까?

그냥 배열을 이용하면 안되나? 라고 의문을 가질 수 있다.

 

 

한번 생각해보자 Iterator interface 를 구현한다. -> 분리되어 있다. 즉 분리되어 하나씩 센다.

ArrayList 와 분리되어 하나씩 센다.

 

테스트 코드

 

package main.service;

import main.model.Book;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;

public class BookShelfTest {

    private BookShelf bookShelf;

    @Before
    public void setUp() throws Exception {

        bookShelf = new BookShelf(5);
        bookShelf.add(new Book("해리포터"));
        bookShelf.add(new Book("반지의 제왕"));
        bookShelf.add(new Book("헝거게임"));
        bookShelf.add(new Book("종이여자"));
        bookShelf.add(new Book("칼의노래"));

    }


    @Test
    public void iterator_Test(){

        Iterator<Book> bookIterator = bookShelf.iterator();

        while(bookIterator.hasNext()){

            Book book = bookIterator.next();
            System.out.println(book.getName());

        }


    }
}

 

에서 우리가 사용한 작업은 BookShelf 내부에 있는 Array 를 직접 건들지 않으면서 작업처리를 

Iterator  가 제공하는 메소드 hasNext 와 next 를 사용해서 한다.

 

즉 BookShelf 의 구현에는 의존하지 않는다는 이야기가 된다.

 

우리가 ArrayList 를 사용하든 LinkedList 를 내부적으로 바꾸든 내부적으로 배열을 사용하든 백터를 사용하든 iterator 로 구현된 로직은 영향을 받지 않는다는 이야기가 된다. 

 

그렇기 때문에 관심사의 분리가 일어나고 OCP 적인 코드가 되는 디자인 패턴이 되는 것이다.