본문 바로가기
Book

[독서 기록] 헤드퍼스트 디자인 패턴 9장 | 컬렉션 잘 관리하기 - 반복자 패턴과 컴포지트 패턴

by Renechoi 2022. 11. 15.

 

 

 
헤드 퍼스트 디자인 패턴
이유 1. 흥미로운 이야기와 재치 넘치는 구성이 담긴 〈헤드 퍼스트〉 시리즈! 하나의 패턴에 하나의 이야기를 담았습니다. 틀에 박히지 않아 지루할 틈이 없는 구성과 친구와 이야기하듯 편안한 대화체로 이야기를 풀어냅니다. 이야기 속에 다양한 방법으로 해결할 수 있는 질문과 90개 이상의 연습문제를 담았습니다. 마치 게임 퀘스트를 해결하듯 문제를 하나하나 해결하다 보면 학습한 내용이 머릿속에 강렬하게 남습니다. 이유 2. 원스톱으로 배우는 14가지 GoF 핵심 디자인 패턴과 9가지 객체지향 디자인 원칙! 현장에서 자주 사용되는 옵저버, 어댑터, MVC 패턴 등 14가지 GoF 객체지향 패턴을 중점으로 패턴의 정의, 사용 시기, 사용처, 사용 이유, 즉시 디자인에 적용하는 방법을 알려줍니다. 이와 더불어 객체지향 프로그래밍에 광범위하게 적용할 수 있는 OCP, 할리우드 원칙 등 9가지 객체지향 디자인 원칙과 패턴으로 생각하는 방법도 알려줍니다. 이유 3. 시대의 변화에 맞춘 개정과 한국 독자만을 위한 특별판! 자바 8과 자바 16 이상에서 무리 없이 동작할 수 있도록 예제 코드를 수정했으며, 부가적인 설명과 Q&A 질문을 추가했습니다. 또한 16여 년 만의 개정을 기념해 오직 한국 독자만을 위한 새로운 삽화를 사용하고 한글 친화적인 구성했습니다. 원서를 읽을 때보다 더욱 편안하게 디자인 패턴을 학습할 수 있습니다. ▶ 이 책을 읽어야 하는 당신! ● 소프트웨어 출시는 완벽 그 자체! “어?~ 코드 수정하려고 다시 보니까 난리…” → 유지보수만 생각하면 그저 눈물인 주니어 (자바) 개발자 ● 코딩 실력은 장판파의 장비! “어?~ 팩토리 메소드 패턴을 이렇게 적용했던가?” → 디자인 패턴을 다시 한번 살펴보고 싶은 시니어 (자바) 개발자 ● 혼자 공부해서 다진 프로그래밍 언어 실력! “어?~ 근데 패턴이 뭐야?” → 개발 현장의 소프트웨어 디자인 방법이 궁금한 개발자 지망생
저자
에릭 프리먼, 엘리자베스 롭슨, 케이시 시에라, 버트 베이츠
출판
한빛미디어
출판일
2022.03.16

 

헤드퍼스트 디자인 패턴 9장 | 컬렉션 잘 관리하기 - 반복자 패턴과 컴포지트 패턴 

(에릭 프리먼 외 4인, 서환수 옮김, 한빛미디어)

 


 

 

 

객체 저장 방식을 보여주지 않으면서도 클라이언트가 객체에 일일이 접근할 수 있게 해주는 방법을 알아보겠습니다.

- 351p

 

 

메뉴 항목 살펴보기 

 

public class MenuItem {
	String name;
    String description;
    boolean vegetarian;
    double price;
    
    public MenuItem(String name, String description, boolean vegetarian, double price) {
    	this.name = name;
        this.description = description;
        this.vegetarian = vegetarian;
        this.price = price;
    }
    
    public String getName() {
    	return name;
   }
   ...

 

 

루와 멜의 메뉴 구현법 비교 

 

public class PancakeHouseMenu {
	ArrayList<String> menuItems;
 
	public PancakeHouseMenu() {
		menuItems = new ArrayList<String>();
    
		addItem("K&B 팬케이크 세트", 스크램블 에그와 토스트가 곁들여진 팬케이크, true, 2.99);
	}

	public void addItem(String name, ...) {
		menuItem menuItem = new MenuItem(name, description, vegetarian, price);
        menuItem.add(menuItem);
        => 메뉴 항목을 추가하고 싶으면 필요한 인자를 전달해서 menuItem 객체를 새로 만들고 그 객체를 ArrayList에 추가
	}
 
	public ArrayList<String> getMenuItems() {
		return menuItems;
	}
  
	public Iterator createIterator() {
		return new PancakeHouseMenuIterator(menuItems);
	}
  
	public String toString() {
		return "Pancake House Menu";
	}

=> ArrayList에 메뉴 항목을 저장

=> 각 메뉴 항목은 생성자 내에서 ArrayList에 추가

 

멜의 메뉴

public class DinerMenu {
	static final int MAX_ITEMS = 6;
    int numberOfItems = 0;
    MenuItem[] menuItems;
    
    public DinerMenu() {
    	menuItems = new MenuItem[MAX_ITEMS];
        
        addItem("채식주의자용 BLT", "통밀 위에...", true, 2.99);
        ...
   } 
   
   public void addItem(String name, ...){
   		MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
        if (numberOfItems >= MAX_LISTS) {
        	sout("죄송합니다")
        } else {
        	menuItem[numberOfItems] = menuItem;
            numberOfItems = numberOfItems + 1; 
       }  => 항목 개수를 제한해서, 최대 메뉴 항목 개수 초과 여부 확인
  }

 

- 353 ~ 355p 

 

 

printMenu(), printBreakfastMenu() 등을 하려고 할 때 각각 형식을 가져오고 다른 반복문을 돌려야 하는 문제 발생

- 357p

 

 

for (int i = 0; i < breakfastItems.size(); i++) {
	MenuItem menuItem = breakfastItems.get(i);;
}

ArrayList의 size()와 get() 메소드를 사용 

 

for (int i = 0; i < lunchItems.lengh; i++) {
	MenuItem menuItem = lunchItems[i];
}

배열의 length 필드와 배열 첨자를 사용 

 

이제 객체 컬렉션의 반복 작업 처리 방법을 캡슐화한 Iterator라는 객체를 만들면 어떨까요? 

 

Iterator iterator = breakfastMenu.createIterator();

while (iterator.hasNext()) {
	MenuItem menuItem = iterator.next();
}

런치 메뉴도 동일하게 적용 가능 

 

- 359 ~ 360p 

 

 

Iterator 인터페이스 정의하기 

public interface Iterator {
   boolean hasNext();
   Object next();
   
   => hasNext() 메소드는 반복 작업을 수행할 항목이 있는지 확인한 다음 그 결과를 불리언 값으로 리턴
   => next() 메소드는 다음 항목을 리턴
}

 

 

public class DinerMenuIterator implements Iterator {
   String[] items;
   int position = 0;
   => 반복작업이 처리되고 있는 위치를 저장 
 
   public DinerMenuIterator(String[] items) {	=> 생성자는 반복 작업을 수행할 메뉴 항목 배열을 인자로 받음
      this.items = items;
   }
 
   public String next() {		=> next() 메소드는 배열의 다음 원소를 리턴하고 position 값을 1 증가 
      String menuItem = items[position];
      position = position + 1;
      return menuItem;
   }
 
   public boolean hasNext() {
      if (position >= items.length || items[position] == null) {
         return false;
      } else {
         return true;
      }
   }
}

 

public Iterator createIterator() {
   return new DinerMenuIterator(menuItems);
}

=> getMenuItems() 메소드는 더 이상 필요 없습니다. 내부 구조를 다 드러내는 단점이 있어서 없애는 게 앗죠.

=> createIterator() 메소드 => menuItems 배열을 가지고 DinerMenuIteraotr를 생성한 다음 클라이언트에게 리턴합니다.

 

public class Waitress {
   Menu pancakeHouseMenu;
   Menu dinerMenu;
 
   public Waitress(Menu pancakeHouseMenu, Menu dinerMenu) {	=> 생성자에서 두 메뉴를 인자로 받아옵니다.
      this.pancakeHouseMenu = pancakeHouseMenu;
      this.dinerMenu = dinerMenu;
   }
 
   public void printMenu() {
      Iterator pancakeIterator = pancakeHouseMenu.createIterator();
      Iterator dinerIterator = dinerMenu.createIterator();
      
      => 2개의 반복자를 생성합니다. 메뉴마다 하나씩 필요하니까요.

      System.out.println("MENU\n----\nBREAKFAST");
      printMenu(pancakeIterator);
      System.out.println("\nLUNCH");
      printMenu(dinerIterator);
      
      => 반복자를 가지고 오버로드된 printMenu() 메소드를 호출합니다. 

   }
 
   private void printMenu(Iterator iterator) { => 오버로드된 printMenu() 메소드는 반복자를 써서 모든 메뉴 항목에 접근하여 그 내용을 출력
      while (iterator.hasNext()) {		남은 항목이 있는지 확인하고 
         MenuItem menuItem = iterator.next();	=> 다음 항목 가져와서 
         System.out.print(menuItem.getName() + ", ");	=> 출력
         System.out.print(menuItem.getPrice() + " -- ");
         System.out.println(menuItem.getDescription());
      }
   }

 

종업원 코드 테스트 

 

public class MenuTestDrive {
   public static void main(String args[]) {
        Menu pancakeHouseMenu = new PancakeHouseMenu();
        Menu dinerMenu = new DinerMenu();
 
      Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);
      
      waitress.printMenu();
      
      printMenus();
   }

=> 메뉴 생성 = > 두 메뉴를 인자로 전달 = > 종업원 생성 => 메뉴 출력 

 

- 362 ~ 364p

 

 

반복자를 사용하면 그 안에 들어 있는 모든 항목에 접근할 수 있게 하려고 여러 메소드를 외부에 노출시키지 않으면서도, 컬렉션에 들어있는 모든 객체에 접근할 수 있습니다. 그리고 반복자를 구현한 코드를 컬렉션 밖으로 끄집어낼 수 있다는 장점도 있죠. 그러니까 반복 작업을 캡슐화했다고 말할 수 있습니다.

- 367p 

 

 

java.util.Iterator 적용하기 

반복자를 직접 만드는 대신 menuItems ArrayList의 iterator() 메소드만 호출하면 됩니다. 

 

public interface Menu {
   public Iterator<MenuItem> createIterator();
}

=> 클라이언트에서 메뉴에 들어있는 항목의 반복자를 획득할 수 있게 해주는 간단한 인터페이스 

 

- 369 ~ 370p 

 

 

 

ArrayList도 Iterable 이므로 다음과 같이 간편하게 자바의 향상된 for 순환문을 사용할 수 있습니다. 

for (MenuItem item : menu) {
	///
}

=> hasNext(), next() 메소드를 직접 사용하지 않아도 됩니다.

 - 378p

 

 

하지만 배열은 iterable 인터페이스를 구현하지 않았기 때문에 향상된 for문 사용 불가 

 

breakfastItems는 forEach 문으로도 출력 가능 

- 379p 

 

 

 

반복자와 컬렉션 활용하기 

- 387p 

 

 

 

종업원 코드 개선하기 

 

public void printMenu() {
   Iterator<MenuItem> pancakeIterator = pancakeHouseMenu.createIterator();
   Iterator<MenuItem> dinerIterator = dinerMenu.createIterator();
   Iterator<MenuItem> cafeIterator = cafeMenu.createIterator();

   System.out.println("MENU\n----\nBREAKFAST");
   printMenu(pancakeIterator);
   System.out.println("\nLUNCH");
   printMenu(dinerIterator);
   System.out.println("\nDINNER");
   printMenu(cafeIterator);
}

=> createIterator()를 3번 호출합니다.

=> printMenu()도 3번 호출해야 하죠

=> 메뉴를 추가하거나 삭제할 때마다 이 코드를 직접 수정해야 합니다. 

 

public class Waitress {
	List<Menu> menus;
    
    public Waitress(List<Menu> menus) {
    	this.menus = menus;
    }
    
    public void printMenu() {
    	Iterator<Menu> menuIterator = menus.iterator();
        while(menuIterator.hasNext()) {
        	Menu menu = menuIterator.next();
            printMenu(menu.createIterator());
        }
   }
   
   void printMenu(Iterator<MenuItem> iterator) {
   		while (iterator.hasNext()) {
        	MenuItem menuItem = iterator.next(); 
            print...

=> 이제 각 메뉴를 따로 받지 않고 목록으로 받아옵니다.

=> 각 메뉴에 반복 작업을 수행합니다. 

=> 각 메뉴의 반복자를 오버로드된 printMenu() 메소드에 넘겨주면 되죠. 

 

- 390p 

 

 

컴포지트 패턴의 정의 

 

컴포지트 패턴으로 객체를 트리구조로 구성해서 부분-전체 계층구조를 구현합니다. 컴포지트 패턴을 사용하면 클라이언트에서 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있습니다.

 - 394p 

 

 

 

 

- 402p 

 

 

컴포지트 적용한 종업원 코드 


public class Waitress {
   MenuComponent allMenus;
 
   public Waitress(MenuComponent allMenus) {
      this.allMenus = allMenus;
   }
 
   public void printMenu() {
      allMenus.print();
   }
}

=> 다른 모든 메뉴를 포함하고 있는 최상위 메뉴 구성 요소만 넘겨주면 됨

=> 최상위 메뉴 : allMenus

 

=> 메뉴 전체의 계층구조를 출력하고 싶다면 그냥 최상위 메뉴의 print()메소드만 호출 

 

 

메뉴 구성 요소 구현하기 


public abstract class MenuComponent {
   
   public void add(MenuComponent menuComponent) {
      throw new UnsupportedOperationException();
   }
   public void remove(MenuComponent menuComponent) {
      throw new UnsupportedOperationException();
   }
   public MenuComponent getChild(int i) {
      throw new UnsupportedOperationException();
   }
   
   
   => 추가하거나 제거하고 가져오는 메소드 
   
   
  
   public String getName() {
      throw new UnsupportedOperationException();
   }
   public String getDescription() {
      throw new UnsupportedOperationException();
   }
   public double getPrice() {
      throw new UnsupportedOperationException();
   }
   public boolean isVegetarian() {
      throw new UnsupportedOperationException();
   }
  
   public void print() {
      throw new UnsupportedOperationException();
   }
   
   
   => menuItem에서 작업을 처리하는 메소드
   
}

 

menuCompent 인터페이스를 확장한 MenuItem 

 

public class MenuItem extends MenuComponent {
   String name;
   String description;
   boolean vegetarian;
   double price;
    
   public MenuItem(String name, 
                   String description, 
                   boolean vegetarian, 
                   double price) 
   { 
      this.name = name;
      this.description = description;
      this.vegetarian = vegetarian;
      this.price = price;
      
      ...

=> 생성자는 이름, 설명, 채식주의자용 식단 여부, 가격을 인자로 받아서 저장 

 

 

메뉴 구현하기 

public class Menu extends MenuComponent {
   ArrayList<MenuComponent> menuComponents = new ArrayList<MenuComponent>();
   String name;
   String description;
  
   public Menu(String name, String description) {
      this.name = name;
      this.description = description;
   }
 
   public void add(MenuComponent menuComponent) {
      menuComponents.add(menuComponent);
   }
 
   public void remove(MenuComponent menuComponent) {
      menuComponents.remove(menuComponent);
   }
   
   => MenuItem이나 다른 menu를 추가하는 코드
   => menuItem과 menu 모두 MenuComponent이므로 한 메소드만 가지고 둘 다 처리할 수 있음 
   
   ...

=> 메뉴에는 MenuComponent 형식의 자식을 몇개든지 저장할 수 있음 = 여기서는 ArrayList에 저장

 

print() 메소드 고치기 

 

public void print() {
   System.out.print("\n" + getName());
   System.out.println(", " + getDescription());
   System.out.println("---------------------");
 
   Iterator<MenuComponent> iterator = menuComponents.iterator();
   while (iterator.hasNext()) {
      MenuComponent menuComponent = 
         (MenuComponent)iterator.next();
      menuComponent.print();
   }
}

= > iterator 반복자를 사용해서 menu에 있는 모든 구성 요소를 대상으로 반복 작업을 처리 

=> menu와 menuItem에서 모두 print()를 구현하므로 그냥 print()만 호출하고 나머지는 각 클래스에게 맡김 

 

- 396 ~ 402p

 

 

반응형