본문 바로가기
  • Where there is a will there is a way.
개발/java

Iterator design pattern

by 소확행개발자 2020. 5. 28.

흔히 개발할때 컬렉션 프레임워크를 사용해서 데이터를 처리할때 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 적인 코드가 되는 디자인 패턴이 되는 것이다.

'개발 > java' 카테고리의 다른 글

객체의 생성  (0) 2020.07.09
빌더로 생성자 대체하기  (0) 2020.07.06
JPA 2차 캐시란  (0) 2020.04.26
JPA 락  (0) 2020.04.25
JPA 기초 프록시란 무엇인가  (0) 2020.04.12

댓글