컴퓨터과학/0 + 소프트웨어 아키텍처(디자인 패턴)

[소프트웨어 아키텍처] 7. 반복자 패턴과 컴포지트 패턴(Iterator Pattern, Composite Pattern -java)

힘들면힘을내는쿼카 2022. 11. 18. 23:15
728x90
반응형

반복자 패턴과 컴포지트 패턴(Iterator Pattern, Composite Pattern)

반복자 패턴

 

컬렉션을 캡슐화!!!!!!!!
컬렉션의 구현 방법을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공합니다.
자료구조와 접근 방법을 분리시켜 객체화 하는 방법

서로 다른 구조를 가지고 있는 저장 객체에 대해서 접근하기 위해서 interface를 통일시키고 싶을 때 사용하는 패턴입니다.

만약 MenuItem에 대하여
DinerMenu 클래스는 배열을, PancakeHouseMenu 클래스는 List를 사용한다고 하자.

 

DinerMenu

배열 사용

public class DinerMenu {
    private static final int MAX_ITEMS = 6;
    private int numOfItems = 0;
    private MenuItem[] menuItems; //배열 사용

    public DinerMenu() {
        menuItems = new MenuItem[MAX_ITEMS];

        addItems("스테이크",
                "맛잇는 스테이크",
                5.99);

        addItems("샐러드",
                "맛잇는 샐러드",
                3.49);

        addItems("파스타",
                "맛잇는 파스타",
                4.19);
    }

    public void addItems(String name, String description, double price) {
        MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
        if(numOfItems >= MAX_ITEMS) {
            System.out.println("죄송합니다. 메뉴가 꽉 찼습니다. 더 이상 추가할 수 없습니다.");
        } else {
            menuItems[numOfItems] = menuItem;
            numOfItems = numOfItems + 1;
        }
    }
}

PancakeHouseMenu

List 사용

public class PancakeHouseMenu {
    private List<MenuItem> menuItems; //리스트 사용

    public PancakeHouseMenu() {
        menuItems = new ArrayList<>();

        addItem("K&B 팬케이크 세트",
                "스크램블 에그와 토스트가 곁들어진 팬케이크",
                2.99);

        addItem("레귤러 팬케이크 세트",
                "달걀프라이와 소시지가 곁들어진 팬케이크",
                2.99);

        addItem("블루베리 팬케이크 세트",
                "블루베리와 블루베리 시럽이 곁들어진 팬케이크",
                3.49);

        addItem("와플",
                "취향에 따라 블루베리나 딸기를 얹을 수 있는 와플",
                3.59);
    }

    public void addItem(String name, String description, double price) {
        MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
        menuItems.add(menuItem);
    }

    public List<MenuItem> getMenuItems() {
        return menuItems;
    }
}

그렇다면 종업원(클라이언트)는 메뉴를 생성할 때 두 가지를 고려해야 한다…
만약 다른 타입의 메뉴가 추가된다면…???
이러한 애로사항을 해결하기위해 반복자 패턴을 사용한다.!

반복자 패턴을 사용하면….
컬렉션 객체 안에 들어있는 모든 항목에 접근하는 방식이 통일되어 종류에 상관 없이 모든 집합체에 사용할 수 있는 다형적인 코드를 만들 수 있다…!

즉, 반복자 패턴을 사용하면 내부 구현 방법을 외부로 노출하지 않으면서
집합체에 있는 모든 항목에 일일이 접근할 수 있습니다.
또한, 각 항목에 일일이 접근할 수 있게 해주는 기능을 집합체가 아닌 반복자 객체가 책임진다는 장점도 있습니다.
그결과 집합체 인터페이스 구현이 간단해지고, 각자에게 중요한 작업만 처리할수 있게 됩니다.!!!!

 

반복자 패턴 구조

컬렉션은 객체를 모아 놓은것에 불과합니다.!
리스트, 배열, 해시테이블과 같이 다양한 자료구조에 컬렉션을 보관할 수 있는데,
어떤 자료구조를 사용하든 결국 컬렉션은 컬렉션입니다…!!!
컬렉션을 집합체(aggregate)라고 부르기도 한다.

  • Iterator
    • 모든 종류의 객체 컬렉션에 반복자를 구현 할 수 있는 인터페이스
    • 컬렉션에 들어있는 원소에 돌아가면서 접근할 수 있게 해주는 메소드 제공
    • 주로 java.util.Iterator 사용
  • ConcreteIterator
    • Iterator의 구현부
  • Aggregate
    • 컬렉션 집합체 인터페이스
    • 객체 컬렉션이 들어있으며, 그안에 들어있는 컬렉션을 iterator로 리턴하는 메소드를 구현
  • ConcreteAggregate
    • Aggregate의 구현부
    • 모든 ConcreteAggregate는 그 안에 있는 객체 컬렉션을 대상으로 돌아가면서 반복 작업을 처리할 수 있게 해주는 ConcreteIterator의 인스턴스를 만들 수 있어야 합니다.

 

배열, List, HashMap 타입의 메뉴 출력 종업원 애플리케이션

배열, List, HashMap 과 같이 컬렉션 타입에 관계없이 메뉴를 출력하는 종업원(Waitress) 애플리케이션을 개발해보자.

Item

@Getter //롬북 사용
@AllArgsConstructor //롬북 사용
public class Item {
    private String name;
    private String description;
    private double price;
}

Menu

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

StarbucksMenu

public class StarbucksMenu implements Menu {
    private final int MAX_ITEMS = 6;
    private int numOfItems = 0;
    private Item[] items;

    public StarbucksMenu() {
        items = new Item[MAX_ITEMS];

        addItems("아이스 아메리카노",
                "디카페인",
                4800);

        addItems("아이스 아메리카노",
                "카페인",
                4500);

    }

    private void addItems(String name, String description, double price) {
        Item item = new Item(name, description, price);
        if(numOfItems >= MAX_ITEMS) {
            throw new RuntimeException("죄송합니다. 메뉴가 가득 찼습니다.");
        } else {
            items[numOfItems] = item;
            numOfItems++;
        }
    }

    @Override
    public Iterator<Item> createIterator() {
        return new StarbucksMenuIterator(items);
    }
}

StarbucksMenuIterator

배열은 Iterator를 상속받지 않는다. 따라서 따로 구현체를 만들었다.
아래처럼 Iterator를 직접 사용하지 못하는 경우 구현해주면된다.!!!

public class StarbucksMenuIterator implements Iterator<Item> {

    private Item[] items;
    private int position = 0;

    public StarbucksMenuIterator(Item[] items) {
        this.items = items;
    }

    @Override
    public boolean hasNext() {
        if(position >= items.length || items[position] == null) {
            return false;
        }
        return true;
    }

    @Override
    public Item next() {
        Item item = items[position];
        position++;

        return item;
    }
}

JangsMenu

public class JangsMenu implements Menu {
    private List<Item> items;

    public JangsMenu() {
        this.items = new ArrayList<>();

        addItems("장세웅의 시그니처 메뉴",
                "매일 바뀌는 메뉴 입니다.",
                10000);
    }

    private void addItems(String name, String description, double price) {
        items.add(new Item(name, description, price));
    }

    @Override
    public Iterator<Item> createIterator() {
        return items.iterator();
    }
}

ShinsMenu

public class ShinsMenu implements Menu {

    private Map<String, Item> map;

    public ShinsMenu() {
        this.map = new HashMap<>();

        addItems("신영이의 뚝딱 한끼 메뉴",
                "가성비 식사 메뉴 입니다.",
                8000);

        addItems("신영이의 고급 한끼 메뉴",
                "고급 재료로 만드는 식사 메뉴 입니다.",
                25000);

        addItems("신영이의 특별한 날 메뉴",
                "특별한 날을 위해 신영이가 엄선한 재료로 만드는 식사 메뉴 입니다.",
                45000);
    }

    private void addItems(String name, String description, double price) {
        map.put(name, new Item(name, description, price));
    }

    @Override
    public Iterator<Item> createIterator() {
        return map.values().iterator();
    }
}

Waitress

public class Waitress {
    List<Menu> menus = new ArrayList<>();

    public Waitress(List<Menu> menus) {
        this.menus = menus;
    }

    public void printMenu() {
        for(Menu menu : menus) {
            printMenu(menu.createIterator());
        }
    }

    private void printMenu(Iterator<Item> iterator) {
        while (iterator.hasNext()) {
            Item item = iterator.next();
            System.out.print(item.getName()+", ");
            System.out.print(item.getDescription()+" -- ");
            System.out.println(item.getPrice()+"원");
        }
        System.out.println("==========  ===========  ==========  ==========  ==========  ========== ==========");
    }
}

Client

public class Client {
    public static void main(String[] args) {
        Waitress waitress = new Waitress(
                List.of(new JangsMenu(),
                        new ShinsMenu(),
                        new StarbucksMenu()));

        waitress.printMenu();
    }
}

결과

장세웅의 시그니처 메뉴, 매일 바뀌는 메뉴 입니다. -- 10000.0원
==========  ===========  ==========  ==========  ==========  ========== ==========
신영이의 고급 한끼 메뉴, 고급 재료로 만드는 식사 메뉴 입니다. -- 25000.0원
신영이의 뚝딱 한끼 메뉴, 가성비 식사 메뉴 입니다. -- 8000.0원
신영이의 특별한 날 메뉴, 특별한 날을 위해 신영이가 엄선한 재료로 만드는 식사 메뉴 입니다. -- 45000.0원
==========  ===========  ==========  ==========  ==========  ========== ==========
아이스 아메리카노, 디카페인 -- 4800.0원
아이스 아메리카노, 카페인 -- 4500.0원
==========  ===========  ==========  ==========  ==========  ========== ==========

 

단일 책임 원칙

집합체(StarbucksMenu)에서 내부 컬렉션 관련 기능과 반복자용 메소드 관련 기능을 전부 구현한다면 어떻게 될까요?

클래스에서 원래 그 클래스의 역할(집합체 관리) 외에 다른 역할(반복자 메소드)을 처리할 때,
2가지 이유로 그 클래스가 바뀔 수 있습니다.

어떤 클래스가 바뀌는 이유는 하나뿐이어야 합니다.
즉, 하나의 클래스는 하나의 역할만 맡아야 합니다.!!!

 

리팩토링 준비

자, 이번에는 디저트 서브 메뉴를 추가해달라고 요구사항이 들어왔습니다.!🤗
흠…. 메뉴(Menu)안에 서브메뉴(SubMenu)라..
지금 구현되어 있는 코드를 가지고는 할수 없을 것 같습니다.

모든 메뉴(서브메뉴)를 대상으로 제대로 작동할 수 있도록 코드를 수정해 봅시다.!

  • 메뉴, 서브메뉴, 메뉴 항목 등을 모두 넣을 수 있는 트리 형태의 구조가 필요함
  • 각 메뉴에 있는 모든 항목을 대상으로 특정 작업을 할 수 있는 방법을 제공해야 하며, 그 방법은 적어도 지금 사용 중인 반복자 만큼 편리해야함
  • 더 유연한 방법으로 아이템을 대상으로 반복 작업을 수행할 수 있어야함
    • JangsMenu에 있는 메뉴를 대상으로만 반복 작업을 할 수 있으면서도 서브메뉴까지 포함한, 모든 식당 메뉴(StarbucksMenu, ShinsMenu)를 대상으로 반복작업을 할수 있어야 한다.

 

 

728x90

 

컴포지드 패턴

 

 

메뉴 관리는 반복자 패턴만으로 처리하기 어려우니 컴포지트 패턴을 사용해봅시다.(여전히 반복자 패턴도 사용됩니다.)

객체를 트리구조로 구성해서 부분-전체 계층구조(part-whole hierarchy)를 구현합니다.
컴포지트 패턴을 사용하면 클라이언트에서 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있습니다.
객체의 구성과 개별 객체를 노드로 가지는 트리형태의 객체 구조를 만들 수 있습니다.

이러한 복합 구조(composite structure)를 사용하면 복합 객체와 개별 객체를 대상으로 똑같은 작업을 적용할 수 있습니다.

 

복합 객체와 개별 객체를 구분할 필요가 거의~~ 없어진다.!!!!!

 

 

이 패턴을 사용하면 중첩되어 있는 메뉴 그룹과 항목을 똑같은 구조 내에서 처리 할 수 있습니다.
메뉴와 항목을 같은 구조에 넣어서 부분-전체 계층 구조(part-whole hierarchy)를 생성할 수 있습니다.

 

부분-전체 계층 구조란, 부분(메뉴 및 메뉴 항목)들이 계층을 이루고 있지만,
모든 부분을 묶어서 전체로 다룰 수 있는 구조를 의미합니다.

 

쉽게 이야기 하면 메뉴, 서브메뉴, 서브서브메뉴, 서브서브서브메뉴로 구성된 트리구조가 있다고 하면
각각이 모두 복합 객체가 될 수 있다는 말입니다. 😎

컴포지트 패턴을 사용하면 간단한 코드만 가지고도(출력 같은) 똑같은 작업을 전체 메뉴 구조를 대상으로 반복해서 적용할 수 있습니다.

 

컴포지트 패턴 구조

  • Component
    • 복합 객체 내에 들어있는 모든 객체의 인터페이스를 정의
    • 복합 노드와 잎에 관한 메소드까지 정의함
  • Leaf
    • Composite에서 지원하는 기능 구현
  • Composite
    • 자식이 있는 구성 요소의 행동을 정의하고 자식 구성 요소를 저장
    • Leaf와 관련된 기능도 구현
      • 관련 기능이 필요 없다면 예외를 던지는 방법으로 처리

 

컴포지트 패턴으로 메뉴 디자인 하기

 

MenuComponent

public abstract class MenuComponent {
    public String getName() {
        throw new UnsupportedOperationException();
    }
    public String getDescription() {
        throw new UnsupportedOperationException();
    }
    public double getPrice() {
        throw new UnsupportedOperationException();
    }

    public void add(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }
    public void remove(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }
    public MenuComponent getChild(int position) {
        throw new UnsupportedOperationException();
    }

    public void print() {
        throw new UnsupportedOperationException();
    }
}

MenuComposite

public class MenuComposite extends MenuComponent {

    private List<MenuComponent> menuComponents = new ArrayList<>();
    private String name;
    private String description;

    public MenuComposite(String name, String description) {
        this.name = name;
        this.description = description;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public void add(MenuComponent menuComponent) {
        menuComponents.add(menuComponent);
    }

    @Override
    public void remove(MenuComponent menuComponent) {
        menuComponents.remove(menuComponent);
    }

    @Override
    public MenuComponent getChild(int position) {
        return menuComponents.get(position);
    }

    @Override
    public void print() {
        System.out.print("\n"+ getName());
        System.out.println(", "+getDescription());
        System.out.println("----------------------");

        for(MenuComponent menuComponent : menuComponents) {
            menuComponent.print();
        }
    }
}

Item

MenuComponent를 상속 받았다.

@Getter //롬북 사용
@AllArgsConstructor //롬북 사용
public class Item extends MenuComponent {
    private String name;
    private String description;
    private double price;

      @Override
    public void print() {
        System.out.print("  "+getName());
        System.out.print(", "+getPrice());
        System.out.println("    -- "+getDescription());
    }
}

Waiter

public class Waiter {
    private MenuComponent allMenus;

    public Waiter(MenuComponent allMenus) {
        this.allMenus = allMenus;
    }

    public void print() {
        allMenus.print();
    }
}

Client

public class Client {
    public static void main(String[] args) {
        MenuComponent jansMenu = new MenuComposite("장세웅의 식당", "장세웅의 식당 메뉴 입니다.");
        MenuComponent shinsMenu = new MenuComposite("신영의 식당", "박신영의 식당 메뉴 입니다.");
        MenuComponent starbucksMenu = new MenuComposite("별다방", "별다방 메뉴 입니다.");
        MenuComponent dessertMenu = new MenuComposite("디저트 메뉴", "디저트 메뉴 입니다.");

        MenuComponent menuAll = new MenuComposite("전체 메뉴", "전체 메뉴입니다.");
        menuAll.add(jansMenu);
        menuAll.add(shinsMenu);
        menuAll.add(starbucksMenu);

        jansMenu.add(new Item("가성비 메뉴",
                "가성비 식사 입니다.",
                4000));

        shinsMenu.add(new Item("맛이 일품인 메뉴",
                "맛을 보면 신이 납니다.",
                5500));
        shinsMenu.add(dessertMenu);

        dessertMenu.add(new Item("바닐라 아이스크림",
                "맛있습니다.",
                3500));

        starbucksMenu.add(new Item("아이스 아메리카노",
                "시원합니다.",
                4500));

        Waiter waiter = new Waiter(menuAll);
        waiter.print();
    }
}

결과

전체 메뉴, 전체 메뉴입니다.
----------------------

장세웅의 식당, 장세웅의 식당 메뉴 입니다.
----------------------
  가성비 메뉴, 4000.0    -- 가성비 식사 입니다.

신영의 식당, 박신영의 식당 메뉴 입니다.
----------------------
  맛이 일품인 메뉴, 5500.0    -- 맛을 보면 신이 납니다.

디저트 메뉴, 디저트 메뉴 입니다.
----------------------
  바닐라 아이스크림, 3500.0    -- 맛있습니다.

별다방, 별다방 메뉴 입니다.
----------------------
  아이스 아메리카노, 4500.0    -- 시원합니다.

 

투명성(transparency)

컴포지트 패턴에서는 단일 역할 원칙을 깨는 대신 투명성(transparency)을 확보하는 패턴이라고 할 수 있습니다.

Component 인터페이스에 자식들을 관리하는 기능과 잎(leaf)으로써의 기능을 전부 넣어서 클라이언트가 복합 객체와 잎을 똑같은 방식으로 처리할 수 있도록 만들 수 있습니다.
어떤 원소가 복합 객체인지 잎(leaf)인지가 클라이언트에게는 투명하게 보입니다.

Component 클래스에는 2가지 기능이 모두 있다보니 단일책임의 원칙에는 위배됩니다.

이러한 내용은 상황에 따라 원칙을 적절하게 사용해야 함을 보여주는 사례입니다.!
디자인 원칙에서 제시하는 가이드라인을 따르면 좋지만 그 원칙이 디자인에 어떤 영향을 끼칠지를 항상 고민하고 원칙을 적용해야 합니다.

 

반응형

 

정리

  • 한 클래스에는 될 수 있으면 한 가지 역할만 부여하는것이 좋다.

반복자 패턴

  • 내부 구조를 드러내지 않으면서도 클라이언트가 컬렉션 안에 들어있는 모든 원소에 접근하도록 할 수 있습니다.
  • 집합체를 대상으로 하는 반복 작업을 별도의 객체로 캡슐화할 수 있습니다.
  • 컬렉션에 있는 모든 데이터를 대상으로 반복 작업을 하는 역할을 컬렉션에서 분리할 수 있습니다.
  • 반복 작업에 똑같은 인터페이스를 적용할 수 있으므로 코드를 만들 때 다형성을 활용할 수 있습니다.

컴포지트 패턴

  • 개별 객체와 복합 객체를 모두 담아 둘 수 있는 구조
  • 클라이언트가 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있습니다.
  • 복합 구조에 들어있는 것을 구성 요소라고 부릅니다.
    • 구성 요소에는 복합 객체(composite)와 잎(leaf) 객체가 존재한다.
  • 상황에 따라 투명성과 안전성 사이에서 적절한 균형을 찾아야합니다.

 

 

 

728x90
반응형