//게시글 클래스@Getter@SetterpublicclassPost {privateString title;privateLocalDateTime createdDateTime;publicPost(String title) {this.title= title;this.createdDateTime=LocalDateTime.now(); }}//게시판 클래스@Getter@SetterpublicclassBoard {List<Post> posts =newArrayList<>();publicvoidaddPost(String content) {this.posts.add(newPost(content)); }}//클라이언트publicclassClient {publicstaticvoidmain(String[] args) {Board board =newBoard();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는 최신 글 먼저 순회한다.
문제점
Board에 들어간 Post를 순회할 때, Board가 어떠한 구조로 이루어져 있는지를 Client가 알게된다. →메뉴판 제작자(클라이언트)가 라멘과 김치찌개를 파는 가게의 메뉴판을 만든다고 가정하겠습니다.메뉴판 제작자(클라이언트)가 라멘과 김치찌개를 파는 가게의 메뉴판을 만든다고 가정하겠습니다. Board에 Post가 List로 담겨있다.
Board가 자료구조를 List에서 Set이나 Array로 바꾸게 되면 Client 코드도 변경에 영향을 받게 된다. → List인 경우에만 쓰일 수 있음
publicclassClient {publicstaticvoidmain(String[] args) {Board board =newBoard();board.addPost("디자인 패턴 게임");board.addPost("선생님, 저랑 디자인 패턴 하나 학습하시겠습니까?");board.addPost("지금 이 자리에 계신 여러분들은 모두 디자인 패턴을 학습하고 계신 분들입니다.");// TODO 들어간 순서대로 순회하기Iterator<Post> iterator =board.getPosts().iterator();while(iterator.hasNext()) {System.out.println(iterator.next().getTitle()); } }}
위 코드를 보면 클라이언트는 이제 Iterator 객체를 가지고 hasNext(), next() 메소드만 사용해 순회를 할 수 있게 되었다. 좀 더 클라이언트가 쉽게 사용할 수 있게 하고 쉽다면 아래처럼 Iterator를 반환해주는 메소드를 만들어 줄 수 있다.
publicclassRecentPostIteratorimplementsIterator<Post> {privateIterator<Post> internalIterator;publicRecentPostIterator(List<Post> posts) { //Board로 받아도 된다. (선택사항)//최신 글 목록이 먼저 오도록 정렬Collections.sort(posts, (p1, p2) ->p2.getCreatedDateTime().compareTo(p1.getCreatedDateTime()));this.internalIterator=posts.iterator(); } @OverridepublicbooleanhasNext() {returnthis.internalIterator.hasNext(); //위임 } @OverridepublicPostnext() {returnthis.internalIterator.next(); //위임 }}publicclassBoard {List<Post> posts =newArrayList<>();publicList<Post> getPosts() {return posts; }publicvoidaddPost(String content) {this.posts.add(newPost(content)); }// Iterator 반환publicIterator<Post> getRecentPostIterator() {// this로 Board를 넘겨줘도 되고, posts를 넘겨줘도 됨returnnewRecentPostIterator(this.posts); }}publicclassClient {publicstaticvoidmain(String[] args) {Board board =newBoard();board.addPost("디자인 패턴 게임");board.addPost("선생님, 저랑 디자인 패턴 하나 학습하시겠습니까?");board.addPost("지금 이 자리에 계신 여러분들은 모두 디자인 패턴을 학습하고 계신 분들입니다.");// TODO 가장 최신 글 먼저 순회하기Iterator<Post> recentPostIterator =board.getRecentPostIterator(); //Iterator 받아와서 사용while(recentPostIterator.hasNext()) {System.out.println(recentPostIterator.next().getTitle()); } }}
위처럼 최근 글로 정렬할 Iterator구현체(ConcretIterator) 인 RecentPostIterator 를 구현해 Board에서 반환해주면 사용자는 이 Iterator가 어떻게 정렬됐는지 알 필요 없이 hasNext(), next()만으로 최근 글로 정렬된 List를 순회할 수 있다.
위 처럼 식당마다 메뉴관리 방식이 달라 클라이언트 코드에서 메뉴 생성 메소드를 식당마다 따로 정의해준다. 즉 클라이언트와의 종속성이 높아지게 된다.
이터레이터 패턴 적용 후 코드
//김치찌개 이터레이터publicclassKimchiStewIteratorimplementsIterator<KimchiStew> {privateKimchiStew[] menus;privateint curPosition =0;publicKimchiStewIterator(KimchiStew[] menus) { super();this.menus= menus; } @OverridepublicbooleanhasNext() {return curPosition <menus.length&& menus[curPosition] !=null; } @OverridepublicKimchiStewnext() {KimchiStew menu = menus[curPosition]; curPosition++;return menu; }}publicclassRamenStore {...// public HashMap<Integer, Ramen> getMenus() {// return menus;// }publicIterator<Ramen> createIterator() {returnmenus.values().iterator(); }}publicclassKimchiStewStore {...// public KimchiStew[] getMenus() {// return menus;// }publicIterator<KimchiStew> createIterator() {returnnewKimchiStewIterator(menus); }}publicclassMenuBoardCreator {publicstaticvoidmain(String[] args) {MenuBoardCreator menuBoardCreator =newMenuBoardCreator();System.out.println(menuBoardCreator.makeKimchiStewMenuBoard());System.out.println(menuBoardCreator.makeRamenMenuBoard()); }privateStringmakeRamenMenuBoard() {String menuBoard ="============ 메뉴판 ============\n";RamenStore ramenStore =newRamenStore();Iterator<Ramen> menu =ramenStore.createIterator();int i =1;// 기존의 방식과 다르게 hasnext()와 next()를 이용해 순회while(menu.hasNext()) {Ramen ramen =menu.next(); menuBoard +=""+ i +"."+ramen.getName() +" : "+ramen.getPrice() +"\n";++i; } menuBoard +="===============================";return menuBoard; }privateStringmakeKimchiStewMenuBoard() {String menuBoard ="============ 메뉴판 ============\n";KimchiStewStore kimchiStewStore =newKimchiStewStore();Iterator<KimchiStew> menu =kimchiStewStore.createIterator();int i =1;// 기존의 방식과 다르게 hasnext()와 next()를 이용해 순회while(menu.hasNext()) {KimchiStew kimchiStew =menu.next(); menuBoard +=""+ i +"."+kimchiStew.getName() +" : "+kimchiStew.getPrice() +"\n";++i; } menuBoard +="===============================";return menuBoard; }}// 출력 결과(동일)============ 메뉴판 ============1.햄 김치찌개 :70002.참치 김치찌개 :80003.삼겹살 김치찌개 :90004.오겹살 김치찌개 :10000=========================================== 메뉴판 ============1.라멘1 :71002.라멘2 :72003.라멘3 :7300......48.라멘48 :1180049.라멘49 :1190050.라멘50 :12000===============================
이터레이터를 사용하게 되면 클라이언트에서 각 가게의 자료구조를 알 필요 없이, 각 가게의 이터레이터만을 이용해 메뉴판을 제작할 수 있어 종속성이 낮아집니다.
위 클라이언트 코드는 이터레이터 패턴 적용전 코드와 비교하기 위해 메소드 두개를 통해 구현했지만 만약 각 식당이 하나의 Store를 상속받는다면 Store를 순회하면서 Iterator를 호출 시키는 방식으로 더 간단한 코드를 만들 수 있을 것이다.
3. Collection의 구조
아래와 같이 Collection은 Iterable을 상속받기 때문에 Collection 자료구조를 사용하면 Iterator를 사용할 수 있습니다.
하지만 Collection을 상속받지 않는 Map은 자체적으로 Iterator를 사용할 수 없기 때문에 위의 예시와 같이 Map의 Key나 Value를 keySet() 이나 values()를 통해 Collection을 상속받는 Set이나 Collection을 반환 후 Iterator를 사용할 수 있습니다.
4. 이터레이드 패턴의 장단점
장점
집합 객체가 가지고 있는 객체들에 손쉽게 접근할 수 있다.
Iterator가 제공하는 인터페이스만 알면되고, 집합 객체의 구조는 알 필요없다.
일관된 인터페이스를 사용해 여러 형태의 집합 구조를 순회할 수 있다.
단일책임원칙 - hasNext()와 getNext()만 호출해도 됨.
OCP - 기존 코드의 큰 변경 없음 (상황에 따라 변경되기도 함)
단점
클래스가 늘어나고 복잡도가 증가한다.
이터레이터를 만드는 것이 유용한 상황인지 판단할 필요가 있다.
다양한 방법으로 순회하는 방법이 필요하고 내부의 집합 구조가 객체가 변경될 가능성이 있다면, 내부 구조를 클라이언트 쪽에게 숨기는 방법으로 이터레이터 패턴을 적용하는 것이 좋은 방법이 될 수 있다.
5. Java, Spring 적용 사례
자바
java.util.Enumeration과 java.util.Iterator
Java StAX (Streaming API for XML)의 Iterator 기반 API
XmlEventReader, XmlEventWriter
스프링
CompositeIterator
자바 - java.util.Enumeration
Iterator가 만들어지기 전 java 1.0부터 있었던 API
hasMoreElements(), nextElement()
Iterator로 거의 대체되었다.
java 9 에서는 Iterator로 변환해주는 코드가 추가되었다.
자바 - java.util.Iterator
hasNext(), next(), remove(), forEachRemainint()
remove() : Iterator에서 next()로 받았던 해당 엘리먼트를 삭제해준다.
모든 이터레이터에서 다 지원하는 것은 아니다.
→ UnsupportedOperationException()을 던지는 경우가 많다.
보통 이 기능은 Concurrent Modification 동시에 다발적으로 같은 오퍼레이션을 수행해도 안전한 컬렉션에서 제공한다.
forEachRemainint()
순회를 좀 더 쉽게 해주도록 도와준다.
Consumer 라는 Functional Interface를 파라미터로 받아서 컬렉션을 순회하면서 다 적용해준다.
publicclassIteratorInJava {publicstaticvoidmain(String[] args) throwsFileNotFoundException,XMLStreamException {Enumeration enumeration;Iterator iterator;Board board =newBoard();board.addPost("디자인 패턴 게임");board.addPost("선생님, 저랑 디자인 패턴 하나 학습하시겠습니까?");board.addPost("지금 이 자리에 계신 여러분들은 모두 디자인 패턴을 학습하고 계신 분들입니다.");// forEachRemaining()// 단순히 소모하는 Functional Interfaceboard.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()); } }}
자바 - 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(newFileInputStream("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(newQName("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을 읽기만 가능하다.