로컬에서는 잘 되는데 ☘️

[행동 패턴] 이터레이터 패턴

by youngjun._.

1. 이터레이터 패턴(Iterator Pattern)이란? 

집합 객체 내부 구조를 노출시키지 않고 순회하는 방법을 제공하는 패턴

집합 객체를 순회하는 클라이언트 코드를 변경하지 않고 다양한 순회 방법을 제공할 수 있다.

 

우리에게 익숙한 Iterator 인터페이스가 가장 대표적인 이터레이터 패턴이 적용된 예시이다.

 

어떤 경우에 필요하고, 어떻게 적용할 수 있는지 예제로 살펴보자.

 

2. 코드로 알아보는 이터레이터 패턴 필요한 상황

클라이언트 요구사항은 다음과 같다.

1. 게시판에 글을 적고 싶어요.
2. 게시글을 최근글, 작성순으로 정렬해서 볼 수 있으면 좋겠어요.

 

2-1. 패턴 적용 전 코드

먼저 게시글(Post)을 표현한 인스턴스부터 만들어보자.

@Getter
@Setter
public class Post {

    private String title;

    private LocalDateTime createdDateTime;

    public Post(String title) {
        this.title = title;
        this.createdDateTime = LocalDateTime.now();
    }
}
  • 편의상 Title만 적을 수 있도록 정의하였다.

 

다음은 게시판(Board)이다. 

@Getter
@Setter
public class Board {
    List<Post> posts = new ArrayList<>();

    public void addPost(String content) {
        this.posts.add(new Post(content));
    }
}
  • 게시글(Post)을 List로 가지고 있는 집합객체이다.

 

클라이언트 요구사항을 구현해보자.

public class Client {

    public static void main(String[] args) {
        Board board = new Board();
        board.addPost("디자인 패턴 게임");
        board.addPost("선생님, 저랑 디자인 패턴 하나 학습하시겠습니까?");
        board.addPost("지금 이 자리에 계신 여러분들은 모두 디자인 패턴을 학습하고 계신 분들입니다.");

        // TODO 들어간 순서대로 순회하기
        List<Post> posts = board.getPosts();
        for (int i = 0 ; i < posts.size() ; i++) {
            Post post = posts.get(i);
            System.out.println(post.getTitle());
        }

        // TODO 가장 최신 글 먼저 순회하기
        Collections.sort(posts, (p1, p2) ->
            p2.getCreatedDateTime().compareTo(p1.getCreatedDateTime()));
        for (int i = 0 ; i < posts.size() ; i++) {
            Post post = posts.get(i);
            System.out.println(post.getTitle());
        }
    }

}
// 실행 결과

디자인 패턴 게임
선생님, 저랑 디자인 패턴 하나 학습하시겠습니까?
지금 이 자리에 계신 여러분들은 모두 디자인 패턴을 학습하고 계신 분들입니다.
선생님, 저랑 디자인 패턴 하나 학습하시겠습니까?
지금 이 자리에 계신 여러분들은 모두 디자인 패턴을 학습하고 계신 분들입니다.
디자인 패턴 게임
  • 첫번째 for loop를 보면 List 컬렉션의 순서대로 순회해서 작성한 글 순서대로 정렬된다.
  • 두번째 for loop는 최신 글 먼저 순회한다.

2-2. 문제점

  • Board에 들어간 Post를 순회할 때, Board가 어떠한 구조로 이루어져 있는지를 Client가 알게된다.
    • ex) Board에 Post가 List로 담겨있다.
  • Board가 자료구조를 List에서 Set이나 Array로 바꾸게 되면 Client 코드도 변경에 영향을 받게 된다.

 

3. 이터레이터 패턴 적용하기

백기선님 강의 중 이터레이터 패턴 구조

3-1. 구조

  • Iterator (Iterator)
    • Client가 사용하는 인터페이스
    • 어떻게 순회할지에 대한 방법을 가지고 있다.
    • getNext() : 다음 Element로 넘어간다.
    • hasNext() : 다음 Element가 있는지 확인한다.
  • ConcreteIterator (ArrayList$Itr)
    • Iterator를 구현한 구현체
    • 구체적인 로직을 구현
  • Aggregate (List)
    • 있을 수도 있고, 없을 수도 있다.
  • ConcreteAggregate (ArrayList)
    • Aggregate 구현체

3-2. 적용하기

아래 예제에서 List Aggregate Interface, Iterator Iterator Interface가 된다.

posts의 실제 구현체인 ArrayList ConcreteAggregate, 그리고 Iterator에 실질적으로 넘어올 수 있는 여러 타입들이 ConcreteIterator가 된다.

출력해서 확인해보면 ArrayListIterator ConcreteIterator이다.

List<Post> posts = board.getPosts(); // List<Post> posts = new ArrayList<>();
Iterator<Post> iterator = posts.iterator();

자바에서 제공하는 Iterator 인터페이스를 사용하자!

  • Board 내부에서 Post를 어떤 자료구조로 사용하고 있는지 알 필요가 없어진다.
public class Client {

    public static void main(String[] args) {
        Board board = new Board();
        board.addPost("디자인 패턴 게임");
        board.addPost("선생님, 저랑 디자인 패턴 하나 학습하시겠습니까?");
        board.addPost("지금 이 자리에 계신 여러분들은 모두 디자인 패턴을 학습하고 계신 분들입니다.");

        // TODO 들어간 순서대로 순회하기
        Iterator<Post> iterator = board.getPosts().iterator();
        while(iterator.hasNext()) {
          System.out.println(iterator.next().getTitle());
        }
    }

}

 

더 좋은 방법은 Board에 Iterator를 반환해주는 기능을 추가하는 것이다.

public class Board {

    List<Post> posts = new ArrayList<>();

    public List<Post> getPosts() {
        return posts;
    }

    public void addPost(String content) {
        this.posts.add(new Post(content));
    }

  // Iterator 제공
    public Iterator<Post> getDefaultIterator() {
        return posts.iterator();
    }

}

그러면, 클라이언트는 getDefaultIterator()로 Iterator를 받아와서 사용할 수 있다.

public class Client {

    public static void main(String[] args) {
        Board board = new Board();
        board.addPost("디자인 패턴 게임");
        board.addPost("선생님, 저랑 디자인 패턴 하나 학습하시겠습니까?");
        board.addPost("지금 이 자리에 계신 여러분들은 모두 디자인 패턴을 학습하고 계신 분들입니다.");

        // TODO 들어간 순서대로 순회하기
        Iterator<Post> iterator = board.getDefaultIterator;
        while(iterator.hasNext()) {
          System.out.println(iterator.next().getTitle());
        }
    }

}

지금은 Board 라는 ConcreteAggregate 뿐이지만 Interface가 있으면 더 유연해진다.

기본 Iterator를 ConcreateAggregate에 따라서 각기 다른 Iterator를 넘겨줄 수 있다.

 

3-2-1. Iterator 구현체를 정의

Iterator 인터페이스는 자바에서 제공하고 있으므로 구현체(ConcreteIterator)만 생성하면 된다. 

  • 자바의 Iterator를 내부적으로 사용 (internalIterator)
  • RecentPostIterator() 로 Iterator를 반환한다.
    • Board를 받아서 posts를 꺼내서 써도 되고, posts를 바로 전달받아도 된다.
    • 이를 기준에 맞게 정렬한 뒤, Iterator를 반환한다.
public class RecentPostIterator implements Iterator<Post> {

    private Iterator<Post> internalIterator;

    public RecentPostIterator(List<Post> posts) { //Board로 받아도 된다. (선택사항)
      //최신 글 목록이 먼저 오도록 정렬
        Collections.sort(posts, (p1, p2) -> 
        	p2.getCreatedDateTime().compareTo(p1.getCreatedDateTime()));
        this.internalIterator = posts.iterator();
    }

    @Override
    public boolean hasNext() {
        return this.internalIterator.hasNext(); //위임
    }

    @Override
    public Post next() {
        return this.internalIterator.next(); //위임
    }
}

3-2-2. Board에서 앞서 만든 ConcreteIterator를 제공하도록 추가

  • getRecentPostIterator() 추가
public class Board {

    List<Post> posts = new ArrayList<>();

    public List<Post> getPosts() {
        return posts;
    }

    public void addPost(String content) {
        this.posts.add(new Post(content));
    }

    // Iterator 반환
    public Iterator<Post> getRecentPostIterator() {
    	// this로 Board를 넘겨줘도 되고, posts를 넘겨줘도 됨
        return new RecentPostIterator(this.posts); 
    }

}

3-2-3. Client에서 Board의 Iterator를 꺼내서 사용

public class Client {

    public static void main(String[] args) {
        Board board = new Board();
        board.addPost("디자인 패턴 게임");
        board.addPost("선생님, 저랑 디자인 패턴 하나 학습하시겠습니까?");
        board.addPost("지금 이 자리에 계신 여러분들은 모두 디자인 패턴을 학습하고 계신 분들입니다.");

        // TODO 가장 최신 글 먼저 순회하기
        Iterator<Post> recentPostIterator = board.getRecentPostIterator(); //Iterator 받아와서 사용
        while(recentPostIterator.hasNext()) {
            System.out.println(recentPostIterator.next().getTitle());
        }
    }

}

 

 

 

 

이제 클라이언트는 게시글을 순회할 때 Board 내부가 어떻게 구현(Set, Array, List, Stack 등)되어 있는지 전혀 신경 쓸 필요가 없다.

4. 장점과 단점

4-1. 장점

  • 집합 객체가 가지고 있는 객체들에 손쉽게 접근할 수 있다.
    • Iterator가 제공하는 인터페이스만 알면되고, 집합 객체의 구조는 알 필요없다.
  • 일관된 인터페이스를 사용해 여러 형태의 집합 구조를 순회할 수 있다.
    • 단일책임원칙 - hasNext()와 getNext()만 호출해도 됨.
    • OCP - 기존 코드의 큰 변경 없음 (상황에 따라 변경되기도 함)

4-2. 단점

  • 클래스가 늘어나고 복잡도가 증가한다.
    • 이터레이터를 만드는 것이 유용한 상황인지 판단할 필요가 있다.
    • 다양한 방법으로 순회하는 방법이 필요하고 내부의 집합 구조가 객체가 변경될 가능성이 있다면, 내부 구조를 클라이언트 쪽에게 숨기는 방법으로 이터레이터 패턴을 적용하는 것이 좋은 방법이 될 수 있다.

5. Java와 Spring에 적용된 사례

  • 자바
    • java.util.Enumeration과 java.util.Iterator
    • Java StAX (Streaming API for XML)의 Iterator 기반 API
      • XmlEventReader, XmlEventWriter
  • 스프링
    • CompositeIterator

 

5-1. 자바 - java.util.Enumeration

  • Iterator가 만들어지기 전 java 1.0부터 있었던 API
  • hasMoreElements(), nextElement()
  • Iterator로 거의 대체되었다.
  • java 9 에서는 Iterator로 변환해주는 코드가 추가되었다.

 

5-2. 자바 - java.util.Iterator

  • hasNext(), next(), remove(), forEachRemainint()
    • remove() : Iterator에서 next()로 받았던 해당 엘리먼트를 삭제해준다.
      • 모든 이터레이터에서 다 지원하는 것은 아니다.
        •  UnsupportedOperationException()을 던지는 경우가 많다.
      • 보통 이 기능은 Concurrent Modification 동시에 다발적으로 같은 오퍼레이션을 수행해도 안전한 컬렉션에서 제공한다.
    • forEachRemainint()
      • 순회를 좀 더 쉽게 해주도록 도와준다.
      • Consumer 라는 Functional Interface를 파라미터로 받아서 컬렉션을 순회하면서 다 적용해준다.
public class IteratorInJava {

    public static void main(String[] args) throws FileNotFoundException, XMLStreamException {
        Enumeration enumeration;
        Iterator iterator;

        Board board = new Board();
        board.addPost("디자인 패턴 게임");
        board.addPost("선생님, 저랑 디자인 패턴 하나 학습하시겠습니까?");
        board.addPost("지금 이 자리에 계신 여러분들은 모두 디자인 패턴을 학습하고 계신 분들입니다.");
      
        // forEachRemaining()
        // 단순히 소모하는 Functional Interface
        board.getPosts().iterator().forEachRemaining(p -> System.out.println(p.getTitle()));
        
        // 위 코드와 동일
        Iterator<Post> iterator1 = board.getPosts().iterator();
        while (iterator1.hasNext()) {
          Post next = iterator1.next();
          System.out.println(next.getTitle());
        }
    }
}

5-3. 자바 - Java StAX (Streaming API for XML)의 Iterator 기반 API

  • 자바가 제공해주는 라이브러리
  • XML을 만들거나 읽을 때 사용할 수 있다.

콘솔 기반의 API, 이터레이터 기반의 API를 제공

  • 이터레이터 기반의 API
    • XMl Element 당 이벤트가 지나가면서 캡쳐한다. 해당 영역을 지나가면서 그 영역을 표현하는 XMLEvent라는 인스턴스가 새롭게 만들어진다.
  • 콘솔 기반의 API
    • 커서 기반의 API - 하나의 인스턴스가 Element를 지나가면서 안의 내용들이 갱신되는 방식이다.
콘솔 기반의 API가 메모리 리소스를 덜 사용하므로 더 효율적이다.

하지만, 일반적으로 이터레이터 기반의 API를 사용하는 것을 권장한다.

만들어진 인스턴스를 재사용하거나 변경하는 등의 유연한 처리를 하기 위해서는 (안의 상태가 변경되는 것보다) Immutable하게 하나하나 제 각각의 XML Element를 표현하는 이터레이터 기반의 API가 다루기 용이하다.

이터레이터 기반의 API 코드 예제

  • XmlEventReader, XmlEventWriter
// TODO Streaming API for XML(StAX), 이터레이터 기반의 API - XML 읽어오는 코드
XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
XMLEventReader reader = xmlInputFactory.createXMLEventReader(new FileInputStream("Book.xml"));

while (reader.hasNext()) {
    XMLEvent nextEvent = reader.nextEvent();
    if (nextEvent.isStartElement()) {
        StartElement startElement = nextEvent.asStartElement();
        QName name = startElement.getName();
        if (name.getLocalPart().equals("book")) {
            Attribute title = startElement.getAttributeByName(new QName("title"));
            System.out.println(title.getValue());
        }
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<books>
    <book title="오징어 게임"/>
    <book title="숨바꼭질"/>
    <book title="우리집에 왜 왔니"/>
</books>

 

  • Book.xml의 title만 읽어들인다.
  • hasNext(), nextEvent() XMl 엘리먼트를 순회한다.
    •  isStartElement() 시작하는 태그만
    •  nextEvent.asStartElement().getName().getLocalPart().equals("book") 태그 이름이 "book"인 것만
    • → 그 중 "title" 속성값만 출력

 

참고
SAX (Simple API for XML)와 다른 것임. SAX는 XML을 읽기만 가능하다.

 

5-4. 스프링 - CompositeIterator

  • 기존의 Interator에 기능만 하나 추가한 것이다.
  • add()
    • 여러 Iterator들을 Composite(조합)해서 사용할 수 있다.
public class IteratorInSpring {

    public static void main(String[] args) {
        CompositeIterator iterator;
    }
}
CompositeIterator는 컴포짓 패턴과 겹치는 내용이 있어서 비교하면 좋을 것 같다.

블로그의 정보

개발하는만두

youngjun._.

활동하기