따봉도치야 고마워

Head First Design Patterns : (9)이터레이터와 컴포지트 패턴 본문

프로그래밍/공부

Head First Design Patterns : (9)이터레이터와 컴포지트 패턴

따봉도치 2020. 9. 24. 16:41

이터레이터(Iterator) 패턴

  • 컬렉션 구현 방법을 노출시키지 않으면서도 모든 항목에 접근할 수 있게 해주는 방법을 제공 (반복 작업 캡슐화)
  • 또한 각 항목에 접근하는 기능을 집합체가 아닌 반복자 객체에서 책임져 집합체 인터페이스 및 구현이 간단해짐

 

+ 컬렉션과 반복자

: 자바 Collection 인터페이스에선 집합체를 조작하기 위한 여러가지 유용한 메소드들을 포함하고 있음

: 그 중 하나가 iterator()

 


객체 마을 식당과 팬케이크 하우스의 합병 

  • 식당은 (점심) 메뉴를 ArrayList로, 하우스는 (아침메뉴를) 배열로 저장하고 있음. 
  • 두 가게의 메뉴 구현 방식이 달라 메뉴를 출력하고, 구별하는 웨이트리스 클래스를 만드는데 어려움이 생김
    • 각 타입에 맞춰 두 번씩 반복해야 함 (식당이 추가되면 그만큼 더 반복)
    • 각각에 대한 똑같은 처리를 위한 인터페이스를 구현한다면? Iterator

Iterator 인터페이스를 정의

  • 식당 메뉴와 하우스 메뉴에 대해 사용할 수 있는 구상 Iterator 클래스 생성 및 적용
//식당 메뉴에 대한 iterator 클래스
public class DinerMenuIterator implements Iterator{
    MenuItem[] items;
    int position = 0;
    
    public DinerMenuIterator(MenuItem[] items){
    	this.items = items;
    }
    
    public Object next(){
    	MenuItem menuItem = items[position];
        position ++;
        return menuItem;
    }
    
    public boolean hasNext(){
    	//식당은 메뉴 개수를 정해놓기 때문에 마지막에 도달했는지 뿐 아니라
        //현재 인덱스에 메뉴가 있는지도 확인을 해야함
    	if(position >= items.length || items[position] == null){
        	return false;
        }
        else{
        	return true;
        }
    }
}

//식당 메뉴에서 위의 Iterator 사용하기
public class DinerMenu{
    static final int MAX_ITEMS = 6;
    int numOfItems = 0;
    MenuItem[] menuItems;
    
    //식당 메뉴에 대한 Iterator를 생성해 리턴해줌
    //클라이언트에선 이제 반복자를 써서 메뉴에 들어있는 항목들에 접근 가능
    public Iterator createIterator(){
    	return DinerMenuIterator(menuItems);
    }
    
    //기타
}

 

  • 웨이트리스 코드 수정
public class Waitress{
    PancakeHouseMenu pancakeHouseMenu;
    DinerMenu dinerMenu;
    
    ..

    public void printMenu(){
    	Iterator pancakeIterator = pancakeHouseMenu.createIterator();
        Iterator dinerIterator = dinerMenu.createIterator();
        
        //각 반복자를 가지고 오버로드 된 printMenu 호출
        printMenu(pancakeIterator);
        printMenu(dinerIterator);
    }
    
    private void printMenu(Iterator iterator){
    	//기존엔 ArrayList와 배열 타입에 맞춰 반복문을 두번 돌려야했지만
        //iterator 인터페이스를 사용해 한 번만 순환하게 됨
    	while(iterator.hasNext()){
        	MenuItem menuItem = (MenuItem)iterator.next();
            System.out.println(menuItem.getName());
            ..
        }
    }
}

 

  • 여기까지의 클래스 다이어그램

+

자바에서 제공하는 Iterator 인터페이스를 사용하도록 수정하기

아직 똑같은 메소드를 사용하는 메뉴 클래스들이 추상화되지 않음

 


 

  • dinnerMenuIterator에선 java.util.Iterator; 를 구현하도록 수정
  • pancakeHouse에선 사실 개별 Iterator 클래스를 만들 필요가 없음 : ArrayList는 iterator()라는 메소드를 이미 제공함
    • pancakeHouseMenu::createIterator() { return menuItems.iterator(); }로 수정하면 됨.
//PancakeHouseMenu Class
public Iterator createIterator(){
	return menuItems.iterator();
}


//DinerMenuIterator Class
import java.util.Iterator;
public class DinerMenuIterator implements Iterator{
   ..
   
    //자바 Iterator 인터페이스에 remove()가 정의되어 있기 때문에 구현해야 함.
    public void remove(){ 
    	if(position <= 0){
        	throw new IllegalStateException("next()를 호출하지 않은 상태에선 삭제 불가");
        }
        if(items[position-1] != null){
        	for(int i=position-1; i<(items.length-1); i++){
            	items[i] = items[i+1];
            }
            items[items.length-1] = null;
        }
    }
}

 

  • 통합 메뉴 인터페이스를 정의하고, 각 구상 메뉴 클래스에서 구현 + Waitress 코드 수정
//Menu Interface를 정의
public interface Menu{
    public Iterator createIterator();
}

//Waitress 클래스에서 구상 메뉴 클래스를 사용하는 부분을 전부 Menu로 바꿔주기
public class Waitress{
    Menu pancakeHouseMenu;
    Menu dinerMenu;
    
    ..
}

 


최종적으로 완성된 클래스 다이어그램

 

 

 

개선점

  • 각자 쓰던 코드를 수정하지 않고도 통합적인 처리가 가능해짐
  • 각 메뉴 구현법이 캡슐화됨 - Waitress입장에선 메뉴 항목의 컬렉션이 무슨 타입인지 알 수 없음
  • Iterator만 구현한다면 어떤 컬렉션이든 한 개의 순환문으로 처리가 가능
  • "특정 구현이 아닌 인터페이스에 맞춰 프로그래밍한다"는 원칙을 지켜 Waitress와 구상 클래스 간 의존성 감소

 

Q&A

1. 다중 스레드 상황에서 같은 객체 컬렉션에 대해 여러 반복자가 있는 경우 remove()가 어떻게 작동하는지

  • 따로 정의되어 있지 않기 때문에 컬렉션에 동시 접근하는 멀티 스레드 코드를 디자인할 땐 매우 유의해야 함

2. 내부 반복자와 외부 반복자는 무슨 뜻인지

  • 예제에서 사용한 것이 외부 반복자 : 클라이언트에서 next()를 호출해 항목을 가져오고 작업을 제어
  • 내부 반복자 : 반복자 자신에 의해 제어됨. 반복자한테 작업을 주고 다음 원소에 대한 작업을 반복자가 직접 처리
  • 내부 반복자는 클라가 직접 제어할 수 없어 유연성이 떨어지지만, 할 일을 넘겨주기만 하면 나머지를 알아서 해 주기 때문에 편리

3. Hashtable처럼 정해진 순서가 없는 컬렉션에서의 반복 작업 순서는 어떻게 정해지는지

  • 반복자에선 특별한 순서가 정해져 있지 않음. 컬렉션의 특성 및 구현하고 연관되어 있음
  • 일반적으로 컬렉션 문서에 특별히 언급되어있지 않은 이상 순서에 대한 것은 가정하면 안 됨.

디자인 원칙

  • 클래스를 바꾸는 이유는 한 가지 뿐이어야 한다.
  • 어떤 클래스에서 맡고 있는 모든 역할들은 나중에 코드 변화를 불러올 수 있으므로, 한 클래스에선 한 가지 역할만 맡도록 하기
    • ex) 집합체에서 컬렉션 기능과 반복자용 메소드를 모두 구현했다면 ?  두 가지 이유로 인해 클래스가 변경 될 것.
  • 응집도(cohesion) : 한 클래스 또는 모듈이 특정 목적 또는 역할을 얼마나 일관되게 지원하는지를 나타내는 척도
  • 응집도가 낮다는 건 서로 관련없는 기능들이 묶여있다는 것

 


컴포지트 패턴

  • 컴포지트 패턴을 이용하면 객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층구조로 만들 수 있음
  • 클라이언트에서 개별 객체와 다른 객체들로 구성된 복합 객체(composite)를 똑같은 방법으로 다룰 수 있음
  • 장점 : 클라이언트 단순화, 메소드 하나로 전체 구조에 대한 반복 가능
  • 고려해야 할 점들
    • 자식 간의 특별한 순서가 필요한 경우, 추가/제거/조회 시 더 복잡한 관리 방법을 사용해야함
    • 복합구조가 너무 복잡하거나 많아져 많은 자원이 필요한 경우, 특정 작업에 대한 결과를 캐싱해 둘 수도 있음

 

 

  1. Component : 복합 객체내에 들어있는 모든 객체들에 대한 인터페이스를 정의 (복합노드 뿐 아니라 Leaf 에 대한 것도)
  2. Composite : 자식이 있는 구성요소의 행동을 정의하고 자식 구성요소를 저장
    •  Composite에서 Leaf와 관련된 기능도 구현해야하지만 그런 경우 예외를 던지는 식으로 처리 가능
  3. Leaf : 자식이 없는 노드

 

 


Menu와 Waitress 코드 개선

  • 지금 코드는 여러 메뉴를 서로 다른 독립적 객체로 다루고 있다는 문제가 있음
  • 또한 서브 메뉴 추가나 기타 변경에 대해 자유롭지 못함
  • 필요한 부분
    1. 메뉴, 서브메뉴, 메뉴항목 등을 모두 넣을 수 있는 트리 형태 구조
    2. 모든 항목, 각 항목에 대한 유연한 반복 작업

컴포지트 패턴 적용하기

    1. 구성요소 인터페이스 생성 - MenuComponent :  Menu와 MenuItem 모두에 적용되는 인터페이스

 

 

 

파란색은 Left(MenuItem) 노드에서만, 초록색은 Composite(Menu) 복합 노드에서만 사용하는 메소드

 

          • Menu와 MenuItem은 MenuComponent 추상 클래스에서 쓰일만한 메소드만 오버라이딩해서 사용
            • 각각 사용하는 메소드가 다르기 때문에 기본적으로 모두 UnsupportedOperationException 에러를 반환하도록 함
            • 자기 역할에 맞지 않은 메소드는 오버라이드 하지 않으면 됨
            • Waitress에선 MenuComponent를 이용해 모두 접근 가능

 

public abstract class MenuComponent{
    //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();
    }
    
    //MenuItem에서 작업을 처리하기 위해 사용하는 메소드
    //이 중 몇 개는 Menu에서 사용하기도 함
    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();
    }
}

 

  1. 메뉴 항목 구현
//MenuComponent 상속
public MenuItem extends MenuComponent{
    String name;
    String description;
    boolean vegetarian;
    double price;
    
    //생성자와 getter 메소드는 기존과 동일함
    public MenuItem(String name, String description, boolean vegetarian, double price){
    	..
    }
    
    public String getName(){
    	return name;
    }
    public String getDescription(){
    	return description;
    }
    public double getPrice(){
    	return price;
    }
    public boolean isVegetarian(){
    	return vegetarian;
    }
    
    public void print(){
    	System.out.print("  " + getName());
        if(isVegetarian()){
        	System.out.print("(v)");
        }
        System.out.print(", " + getPrice());
        System.out.println("  --" + getDescription());
    }

 

  1. 메뉴 구현
//마찬가지로 MenuComponent 상속
public class Menu extends MenuComponent{
	ArrayLisst menuComponents = new ArrayList();
    String name;
    String description;
    
    public Menu(String name, String description){
    	..
    }
    
    public void add(MenuComponent menuComponent){
    	menuComponents.add(menuComponent);
    }
     public void remove(MenuComponent menuComponent){
    	menuComponents.remove(menuComponent);
    }
     public void MenuComponent(int i){
    	return (MenuComponent)menuComponents.get(i);
    }
    
    
    //getter 구현, getPrice()와 isVegetarian()은 적합하지 않으므로 구현하지 않음
    public String getName(){
    	..
    }
    public String getDescription(){
    	..
    }
    
    //Menu 클래스에선 해당 메뉴에 속하는 모든 서브Menu와 MenuItem을 출력해줘야함
    //반복자를 사용해 각 자식의 print() 호출
    public void print(){
    	System.out.print("\n" + getName());
        System.out.println(", " + getDescription());
        System.out.println("---------------------");
        
        Iterator iterator = menuComponents.iterator();
        while(iterator.hasNext()){
            MenuComponent menuComponent = (MenuComponent)iterator.next();
            menuComponent.print();
        }
    }
}

 

  1. Waitress 수정
public class Waitress{
    MenuComponent allMenus;
    
    public Waitress(MenuComponent allMenus){
    	this.allMenus = allMenus;
    }
    
    //전체 계층구조를 출력하고 싶다면 최상위 메뉴의 print() 호출
    public void printMenu(){
		allMenus.print();
    }
}

 

Q. 이렇게 되면 컴포지트 패턴에선 '계층 구조를 관리와 메뉴 관리' 두가지 역할을 한 클래스에서 처리하는 것 아닌가?

  • 컴포지트 패턴은 단일 역할 원칙을 깨면서 투명성을 확보하기 위한 패턴
  • Component 인터페이스에 Leaf와 자식들의 기능을 모두 넣으면서 클라이언트 입장에서 둘을 똑같은 방식으로 처리할 수 있음
  • 물론 안정성이 떨어지긴함 (한 원소에 대해 무의미한/부적절한 작업을 실행하려고 할 수 있음)
  • 즉 원칙은 상황에 따라 적절히 사용할 것 - 안정성과 투명성 사이 어딘가..

 

+ 복합반복자

  • Waitress에서 채식주의자용 메뉴만 뽑아내거나, 복합 객체 전체에 대해 반복작업을 수행할 수 있도록 해보자
  • 먼저, MenuComponent에 createIterator() 추가
//MenuComponent에 createIterator() 메소드를 추가하고
//Menu, MenuItem 에서 각각 구현
public class Menu extends MenuComponent{
    ..
    public Iterator createIterator(){
	    return CompositeIterator(menuComponents.iterator());
    }
}

public class MenuItem extends MenuComponent{
    ..
    public Iterator createIterator(){
	    return new NullIterator();
    }
}

 

  • CompositeIterator : 어떤 복합 객체에 대해서도 반복작업을 할 수 있는 반복자
  • NullIterator : MenuItem은 반복 작업을 할 대상이 없기 때문에 createIterator()의 별도 처리가 필요함
    1. createIterator()에서 널 리턴 -> 클라이언트에서 null check를 해줘야함
    2. hasNext()에서 무조건 false를 리턴하는 반복자 생성
public class CompositeIterator implements Iterator{
	Stack stack = new Stack();
    
    //반복작업을 처리할 대상 중 최상위 복합 객체의 반복자가 전달되고
    //스택에 저장됨
    public CompositeIterator(Iterator iterator){
    	stack.push(iterator);
    }
    
    public Object next(){
    	if(hasNext()){
            //다음 원소가 남아있다면 스택에서 현재 반복자를 꺼내 다음 원소를 구함
            Iterator iterator = (Iterator) stack.peek();
            MenuComponent component = (MenuComponent)iterator.next();
            
            //다음 원소가 메뉴면 반복작업에 또다른 복합객체가 추가된 것이므로 스택에 집어 넣음
            if(component instanceof Menu){
            	stack.push(component.createIterator());
            }
            return component;
        }else{
        	return null;
        }
    }
    
    public boolean hasNext(){
    	if(stack.empty()){
        	return false;
        }else{
        	//스택이 비어있지 않아도 스택 맨 위 반복자를 꺼내 다음 원소가 있는지 확인
        	Iterator iterator = (Iterator) stack.peek();
            if(!iterator.hasNext()){
            	stack.pop();
                return hasNext();
            }else{
            	return true;
            }
        }
    }
    
    public void remove(){
    	throw new UnsupportedOperationException();
    }
}

ex) allMenus.createIterator()  ->  { 팬케이크, 식당, 카페 } 메뉴 이터레이터 스택에 push

-> next() 호출 시 팬케이크 메뉴 객체 리턴 및 팬케이크 메뉴 항목에 대한 이터레이터 스택에 push

-> 한 번 더 next() 호출 시 팬케이크 메뉴 항목에 대한 이터레이터 조회

-> 팬케이크 메뉴 항목에 대한 조회가 끝나면 식당 > .. > 카페 순으로 반복

 

  • 위의 반복자를 이용한 채식주의자 전용 메뉴 출력 함수
public void printVegetarianMenu(){
	Iterator iterator = allMenus.createIterator();
    System.out.println("\nVEGETARIAN MENU");
    
    while(iterator.hasNext()){
    	MenuComponent menuComponent = (MenuComponent)iterator.next();
        try{
        	if(menuComponent.isVegetarian()){
            	menuComponent.print();
            }
        }catch(UnsupportedOperationException e){}
        //Menu의 경우 isVegetarian() 실행 시 예외 발생
        //예외처리문을 작성해, 예외를 잡지만 반복작업이 지속될 수 있게 함
    }
}

 

Q. try/catch 문을 프로그램 로직을 처리하는데 사용하는 것은 지양해야하지 않는지

  • 맞음. Menu의 isVegetarian()이 무조건 false를 반환하도록 해도 되지만 해당 예제는 메소드 지원불가라는 점을 확실히 나타내기 위해 그냥 사용 

 


연필을 깎으며

page 360

  • ACDE

 

page 380

  1. Menu 인터페이스를 구현하기
  2. createIterator() 메소드 구현
  3. 기존 getMenu() 삭제

 

 

NOTE

제네릭(Generic)

  • 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다.
  • 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다. 

 

 

Comments