Dev/디자인패턴

[타입스크립트로 살펴보는 디자인패턴 10] 반복자 패턴

싯벨트 2025. 1. 3. 09:31
728x90

🙋‍♂️ 디자인패턴 구현코드 깃헙

반복자 패턴(Interator Pattern)

반복자 패턴은 컬렉션의 구현 방법을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공한다.

반복자 패턴을 사용하면 컬렉션 객체 안에 들어있는 모든 항목에 접근하는 방식이 통일되어 있기 때문에 종류에 관계없이 모든 집합체에 사용할 수 있는 다형적인 코드를 만들 수 있다. 또한 모든 항목에 일일이 접근하는 작업을 컬렉션 객체가 아닌 반복자 객체가 맡게 되면서 집합체의 인터페이스와 구현이 간단해지고, 집합체는 원래 자신이 하는 일인 객체 컬렉션 관리에만 전념할 수 있게 된다.

반복자 패턴 클래스 다이어그램

메뉴판 시스템

코드

배열 컬렉션과 Set 컬렉션으로 이루어진 메뉴판에서 컬렉션 타입에 무관하게 반복자를 활용하여, 두 메뉴판에서 비건메뉴를 출력하는 코드를 짜보자.

pancake.house.menu.ts

팬케익하우스의 메뉴는 배열 컬렉션을 사용한다.

import { MenuItem } from "./menu.item";

export class PancakeHouseMenu {
    private menuItems: MenuItem[];

    constructor() {
        this.menuItems = [];
				//addItem 메서드로 메뉴 추가
    }

    addItem(name: string, description: string, vegetarian: boolean, price: number): void {
        this.menuItems.push(new MenuItem(name, description, vegetarian, price));
    }

    getMenuItems(): MenuItem[] {
        return this.menuItems;
    }
}

dinner.menu.ts

저녁 메뉴는 Set 컬렉션을 사용한다.

import { MenuItem } from "./menu.item";

export class DinnerMenu {
    private menuItems: Set<MenuItem>;

    constructor() {
        this.menuItems = new Set();
				//addItem 메서드로 메뉴 추가
    }

    addItem(name: string, description: string, vegetarian: boolean, price: number): void {
        this.menuItems.add(new MenuItem(name, description, vegetarian, price));
    }

    getMenuItems(): Set<MenuItem> {
        return this.menuItems;
    }
}

waitress.ts

종업원 코드는 객체 구성을 사용하여 각 메뉴를 생성자의 파라미터로 받고, 각 메뉴의 아이템을 가지고 와서 개별적인 반복문을 돌며 비건 메뉴를 출력한다.

import { DinnerMenu } from "./dinner.menu";
import { MenuItem } from "./menu.item";
import { PancakeHouseMenu } from "./pancake.house.menu";

export class Waitress {
    constructor(
        private pancakeHouseMenu: PancakeHouseMenu,
        private dinnerMenu: DinnerMenu,
    ) {}

    printVegetarian(): void {
        const vegetarianMenu: MenuItem[] = [];
        const pancakeHouseMenuItems = this.pancakeHouseMenu.getMenuItems();
        const dinnerMenuItems = this.dinnerMenu.getMenuItems();

        for (const item of pancakeHouseMenuItems) {
            if (item.getVegetarian() === true) vegetarianMenu.push(item);
        }

        for (const item of dinnerMenuItems) {
            if (item.getVegetarian() === true) vegetarianMenu.push(item);
        }

        console.log(vegetarianMenu);
    }
}

반복자 패턴 적용

iterator.ts

반복자 인터페이스는 다음 요소가 있는지 체크하는 메서드와, 다음 요소가 있을 경우 그 요소를 출력하는 메서드를 가지고 있다.

import { MenuItem } from "./menu.item";

export interface Iterator {
    hasNext(): boolean;
    next(): MenuItem;
}

pancake.house.menu.iterator.ts

각 메뉴는 각각 Iterator 인터페이스를 구현한 이터레이터 클래스를 가지며, 생성자에 메뉴 아이템의 컬렉션을 넣어서 구현된 이터레이터의 메서드를 호출하도록 했다.

import { Iterator } from "./iterator";
import { MenuItem } from "./menu.item";

export class PancakeHouseMenuIterator implements Iterator {
    private position = 0;
    constructor(private items: MenuItem[]) {}

    hasNext(): boolean {
        return this.position < this.items.length;
    }

    next(): MenuItem {
        const menuItem = this.items[this.position];
        this.position += 1;
        return menuItem;
    }
}

dinner.menu.iterator.ts

Set 컬렉션은 특정 위치의 요소를 지정해서 불러올 수 없으므로, Array.from()을 사용해서 배열로 변환한 후 사용했다. Set 컬렉션을 활용할 경우, size를 hasNext() 메서드에 활용할 수도 있다.

import { Iterator } from "./iterator";
import { MenuItem } from "./menu.item";

export class DinnerMenuIterator implements Iterator {
    private position = 0;
    private items: MenuItem[];

    constructor(private menuItems: Set<MenuItem>) {
        this.items = Array.from(menuItems);
    }

    hasNext(): boolean {
        return this.position < this.items.length;
    }

    next(): MenuItem {
        const menuItem = this.items[this.position];
        this.position += 1;
        return menuItem;
    }
}

waitress.ts

반복문 패턴을 적용함으로써 반복문을 도는 로직을 하나로 통일할 수 있다. 추후 메뉴들이 추가됐을 때 printVegetarian() 내부 코드만 변경하여 대응할 수 있다.

import { DinnerMenu } from "./dinner.menu";
import { Iterator } from "./iterator";
import { PancakeHouseMenu } from "./pancake.house.menu";

export class Waitress {
    constructor(
        private pancakeHouseMenu: PancakeHouseMenu,
        private dinnerMenu: DinnerMenu,
    ) {}

    printVegetarian(): void {
        const pancakeHouseMenuItems = this.pancakeHouseMenu.createIterator();
        const dinnerMenuItems = this.dinnerMenu.createIterator();

        this.printVegetarianMenuItems(pancakeHouseMenuItems);
        this.printVegetarianMenuItems(dinnerMenuItems);
    }

    private printVegetarianMenuItems(iterator: Iterator): void {
        while (iterator.hasNext()) {
            const iteratorNext = iterator.next();
            if (iteratorNext.getVegetarian()) console.log(iteratorNext);
        }
    }
}

tsc *.ts 변환 시 에러

Set 컬렉션의 반복문 사용 때문에 아래와 같은 에러가 발생할 수 있다.

이때는 tsc --downlevelIteration *.ts 처럼 '--downlevelIteration' flag 를 추가하면 정상적으로 동작한다.

waitress.ts:28:28 - error TS2802: Type 'Set<MenuItem>' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher.

28         for (const item of dinnerMenuItems) {
~~~~~~~~~~~~~~~

Found 1 error in waitress.ts:28

디자인 원칙

단일 역할 원칙(단일 책임 원칙)

어떤 클래스가 바뀌는 이유는 하나뿐이어야 한다.
(클래스는 단 하나의 책임을 가져야만 한다)

클래스는 하나의 기능이나 역할에 집중해야 하고, 그렇기 때문에 이 클래스가 변경될 이유도 단 하나여야 한다는 원칙이다. 이때 응집도(cohesion)라는 개념을 함께 살펴보면, 응집도란 한 클래스 또는 모듈이 특정 목적이나 역할을 얼마나 일관되게 지원하는지를 나타내는 척도이다. 단일 역할 원칙을 잘 따르는 클래스는 그렇지 않은 클래스보다 응집도가 높다고 말한다.

참고문서

헤드퍼스트 디자인패턴, 한빛미디어