정상에서 IT를 외치다

[디자인패턴] 이터레이터와 컴포지트 패턴 본문

디자인패턴

[디자인패턴] 이터레이터와 컴포지트 패턴

Black-Jin 2019. 6. 13. 00:23
반응형

안녕하세요. 블랙진입니다.

책 HeadFirstDesignPattern 을 보며 코드를 Kotlin 으로 바꿔가며 공부한 내용입니다.



이터레이터 패턴


이터레이터 패턴은 컬렉션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는 모든 항목에 접근할 수 있게 해주는 방법을 제공해 줍니다. 이 패턴을 이용하면 집합체 내에서 어떤 식으로 일이 처리되는지에 대해서 전혀 모르는 상태에서 그 안에 들어있는 모든 항목들에 대해서 반복작업을 수행할 수 있습니다. 즉 내부적인 구현 방법을 외부로 노출시키지 않으면서도 집합체에 있는 모든 항목에 일일이 접근할 수 있습니다



예제


객체지향 마을에 팬케이크 하우스 메뉴와 저녁 메뉴를 가지고 있는 음식점이 있습니다. 각 메뉴는 리스트와 배열의 서로 다른 형식으로 저장합니다. 이 음식점의 웨이트리스트가 음식들을 출력할려면 어떻게 해야 될까요?


data class MenuItem(
val name: String,
val description: String,
val vegetarian: Boolean,
val price: Double
)

메뉴에는 이름, 설명, 채식주의자용인지 그리고 가격이 있습니다.


class PancakeHouseMenu {

private val menuItems: ArrayList<MenuItem> = arrayListOf()

init {
addItem("펜케이크 세트 1", "세트 1", true, 2.99)
addItem("펜케이크 세트 2", "세트 2", true, 1.99)
addItem("펜케이크 세트 3", "세트 3", true, 0.99)
}

fun addItem(name: String, description: String, vegetarian: Boolean, price: Double) {
val menuItem = MenuItem(name, description, vegetarian, price)
menuItems.add(menuItem)
}

fun getItem() = menuItems
}

팬케이크 하우스 메뉴에서는 ArrayList 로 메뉴를 저장합니다.


class DinerMenu {

private val MAX_ITEMS = 6
private val menuItems: Array<MenuItem?> = Array(MAX_ITEMS) { null }

private var numberOfItems = 0

init {
addItem("음식 1", "채식주의자용", true, 2.99)
addItem("음식 2", "혼합", false, 1.99)
addItem("음식 3", "육식", false, 0.99)
}

fun addItem(name: String, description: String, vegetarian: Boolean, price: Double) {
val menuItem = MenuItem(name, description, vegetarian, price)

if(numberOfItems >= MAX_ITEMS) {
println("죄송합니다. 메뉴가 꽉 찼습니다.")
} else {
menuItems[numberOfItems] = menuItem
numberOfItems++
}

}

fun getItem() = menuItems
}

저년 메뉴에서는 Array를 만들어 메뉴를 저장하고 있습니다.


class Waitress(
private val pancakeHouseMenu: PancakeHouseMenu,
private val dinerMenu: DinerMenu) {

fun printMenu() {

val pancakeHouseMenu = pancakeHouseMenu.getItem()

val dinerMenu = dinerMenu.getItem()

println("-- 팬케이크 하우스 메뉴 --")
printMenu(pancakeHouseMenu)

println("-- 저녁메뉴 --")
printMenu(dinerMenu)

}

private fun printMenu(arrayList: ArrayList<MenuItem>) {

//ArrayList
for(menu in arrayList) {
println(menu)
}
}

private fun printMenu(array: Array<MenuItem?>) {

//Array
for(menu in array) {
println(menu)
}
}
}

웨이트리스에서 펜케이크 하우스 메뉴와 저녁 메뉴를 출력하기 위해서는 서로 다른 함수를 사용해야 됩니다.



해결


다른 종류의 컬렉션들을 하나의 함수로 확인하기 위해 Iterator 인터페이스를 만들어서 처리합니다.

interface Iterator<T> {

fun hasNext(): Boolean

fun next(): T
}

hasNext 와 next 함수를 가지는 제네릭 인터페이스를 만들어 줍니다. 이제 이터레이터를 구현하는 팬케이크 하우스 메뉴와 저녁 메뉴를 구현해 줍니다.


class PancakeHouseMenuIterator(
private val menuItems: ArrayList<MenuItem>
): Iterator<MenuItem> {

private var position = 0

override fun next(): MenuItem {

val menuItem = menuItems[position]
position++
return menuItem
}

override fun hasNext(): Boolean {
return position < menuItems.size
}
}
class DinerMenuIterator(
private val menuItems: Array<MenuItem?>
) : Iterator<MenuItem> {

private var position = 0

override fun next(): MenuItem {

val menuItem = menuItems[position]
position++

return menuItem!!
}

override fun hasNext(): Boolean {
return !(position >= menuItems.size || menuItems[position] == null)
}

}

이렇게 팬케이크 하우스 메뉴와 저녁 메뉴를 Iterator 를 사용해 구현해 주었습니다. 


class Waitress(var pancakeHouseMenu: PancakeHouseMenu, var dinerMenu: DinerMenu) {

fun printMenu() {

val pancakeHouseMenu = pancakeHouseMenu.createIterator()

val dinerMenu = dinerMenu.createIterator()

println("-- 아침메뉴 --")
printMenu(pancakeHouseMenu)

println("-- 저녁메뉴 --")
printMenu(dinerMenu)
}

private fun printMenu(iterator: Iterator<MenuItem>) {
while (iterator.hasNext()) {
val menuItem = iterator.next()
println(menuItem.name)
}
}
}

위와 같이 printMenu() 에서는 Iterator<MenuItem> 하나를 통해 서로 다른 컬렉션을 사용하는 메뉴를 출력할 수 있게됩니다.



개선


팬케이크 하우스 메뉴와 저녁 메뉴 에서는 createIterator() 라는 메소드를 공통으로 구현합니다. 객체 지향에서 공통으로 생성하는 메소드는 추상화를 통해 묶을 수 있습니다. 이를 통해 Waitress의 매개변수 또한 추상화 할 수 있습니다.

interface Menu {
fun createIterator(): Iterator<MenuItem>
}

createIterator() 를 공통으로 사용하기 때문에 인터페이스를 선언해 줍니다.


class PancakeHouseMenu(
private val menuItems: ArrayList<MenuItem> = arrayListOf()
) : Menu {

    ...

override fun createIterator() = menuItems.iterator()
}

class DinerMenu(
private val menuItems: Array<MenuItem> = Array(6) { MenuItem() }
) : Menu {

...

override fun createIterator() = menuItems.iterator()
}

createIterator() 를 구현해 줍니다.


class Waitress(var pancakeHouseMenu: Menu, var dinerMenu: Menu) {

fun printMenu() {

...
}

private fun printMenu(iterator: Iterator<MenuItem>) {
...
}
}

Waitress의 매개변수를 Menu 인터페이스로 추상화 했습니다. 이렇게 추상화를 함으로써 변화에 더욱 유동적으로 대응할 수 있게 됩니다.



문제


이번에는 카페 메뉴를 추가해 달라는 요청이 왔습니다. 여기 카페 메뉴에서는 해쉬테이블을 사용해 메뉴를 관리합니다. 우리는 이터레이터를 사용해 메뉴를 보여주고 있기 때문에 어렵지 않게 대응할 수 있습니다.


class CafeMenu : Menu {

private val menuItems = Hashtable<String, MenuItem>()

init {

addItem("카페 1", "초코케이크", true, 2.99)
addItem("카페 2", "딸기케이크", false, 1.99)
addItem("카페 3", "바나나케이크", false, 0.99)
}

fun addItem(name: String, description: String, vegetarian: Boolean, price: Double) {
val menuItem = MenuItem(name, description, vegetarian, price)
menuItems[name] = menuItem
}

override fun createIterator(): Iterator<MenuItem> {
return menuItems.values.iterator()
}
}

하지만 우리의 웨이트 리스는 메뉴를 출력하는데 printMenu() 함수를 1개 더 생성해야 되는 번거로움이 있습니다.


class Waitress(var pancakeHouseMenu: Menu, var dinerMenu: Menu, var cafeMenu: Menu) {

fun printMenu() {

val pancakeHouseMenu = pancakeHouseMenu.createIterator()

val dinerMenu = dinerMenu.createIterator()

val cafeMenu = cafeMenu.createIterator()

println("-- 팬케이크 하우스 메뉴 --")
printMenu(pancakeHouseMenu)

println("-- 저녁 메뉴 --")
printMenu(dinerMenu)

println("-- 카페 메뉴 --")
printMenu(cafeMenu)
}

private fun printMenu(iterator: Iterator<MenuItem>) {
...
}
}

printMenu()를 세번이나 호출하고 있습니다. 이렇게 되면 새로운 메뉴가 추가될 때마다 Waitress에 코드를 추가해 줘야되는데 이는 OCP에 위배됩니다. 


메뉴 구현을 분리시키고 반복작업에 필요한 부분은 반복자로 뽑아낸 것만 해도 매우 훌륭했습니다. 하지만 여전히 메뉴를 서로 다른 독립적인 객체로 다루고 있다는 문제가 있습니다. 여러 메뉴를 한꺼번에 관리할 수 있는 방법이 필요합니다.



해결

class Waitress(private val menu: ArrayList<Menu>) {

fun printMenu() {

val menuIterator = menu.iterator()

while (menuIterator.hasNext()) {
val menu = menuIterator.next()
printMenu(menu.createIterator())
}
}

private fun printMenu(iterator: Iterator<MenuItem>) {
while (iterator.hasNext()) {
val menuItem = iterator.next()

if(menuItem.name.isNotEmpty())
println(menuItem)
}
}
}

웨이트리스의 매개변수를 위와 같이 Menu를 가지는 리스트로 변경해주면 쉽게 처리할 수 있습니다. 이제 메뉴의 종류가 눌어나도 웨이트리스에 printMenu() 함수를 추가해 주지 않아도 됩니다. 즉 변화에는 열려있고 수정에는 닫혀있는 OCP 원칙이 지켜지고 있는 거죠



디자인 원칙


클래스를 바꾸는 이유는 한 사지 뿐이어야 한다.



응집도


응집도란 한 클래스 또는 모듈이 특정 목적 또는 역활을 얼마나 일관되게 지원하는지를 나타내는 척도라고 할 수 있습니다. 어떤 모듈 또는 클래스의 응집도가 높다는 것은 일련의 서로 연관된 기능이 묶여있다는 것을, 응집도가 낮다는 것은 서로 상관 없는 기능들이 묶여있다는 것을 뜻합니다. 이 원칙을 잘 따르는 클래스는 두 개 이상의 역활을 맡고 있는 클래스에 비해 응집도가 높고, 관리하기도 더 용이한 편입니다.




컴포지트 패턴


컴포지트 패턴을 이용하면 객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층구도로 만들 수 있습니다. 이 패턴을 이용하면 클라이언트에서 개별 객체와 다른 객체들로 구성된 복합 객체(composite)를 똑같은 방법으로 다룰 수 있습니다.



문제


새로운 요구사항이 생겼습니다. 저녁 메뉴 안에 디저트를 서브 메뉴로 추가해줘야 합니다.


abstract class MenuComponent {

open fun add(menuComponent: MenuComponent) {
throw UnsupportedOperationException()
}

open fun remove(menuComponent: MenuComponent) {
throw UnsupportedOperationException()
}

open fun getChild(i: Int): MenuComponent {
throw UnsupportedEncodingException()
}

/*fun getName(): String {
throw UnsupportedEncodingException()
}

fun getDescription(): String {
throw UnsupportedEncodingException()
}

fun getPrice(): Double {
throw UnsupportedEncodingException()
}*/

open fun isVegetarian(): Boolean {
throw UnsupportedEncodingException()
}

open fun print() {
throw UnsupportedEncodingException()
}

open fun createIterator(): Iterator<MenuComponent> {
throw UnsupportedEncodingException()
}
}

컴포지트 패턴을 구현하기 위해 추상 클래스 MenuComponent를 만들어 줍니다. 여기서 여러 역활이 한 클래스에 들어가게 됩니다.


1.  MenuComponent를 추가하고 제거하고 가져오기 위한 메소드

2. MenuItem에서 작업을 처리하기 위해 사용하는 메소드

3. Menu와 MenuItem에서 구현해야 되는 print() 메소드


class Menu(
val name: String,
val description: String
): MenuComponent() {

private val menuComponents = ArrayList<MenuComponent>()

override fun add(menuComponent: MenuComponent) {
menuComponents.add(menuComponent)
}

override fun remove(menuComponent: MenuComponent) {
menuComponents.remove(menuComponent)
}

override fun getChild(i: Int): MenuComponent {
return menuComponents[i]
}

override fun print() {
print("\n $name")
println(", $description")
println("-----------")

val iterator = menuComponents.iterator()
while (iterator.hasNext()) {
val menuComponent = iterator.next()
menuComponent.print()
}

}
}

메뉴를 보여주는 클래스 입니다.


class MenuItem(
val name: String,
val description: String,
val vegetarian: Boolean,
val price: Double
): MenuComponent() {

override fun isVegetarian(): Boolean {
return vegetarian
}

override fun print() {
print(" $name")
if(isVegetarian()) {
print("(v)")
}

println(" , $price")
println(" -- $description")
}

override fun createIterator(): Iterator<MenuComponent> {
return NullIterator()
}
}

class NullIterator : Iterator<MenuComponent> {

override fun hasNext(): Boolean {
return false
}

override fun next(): MenuComponent {
return object : MenuComponent() {}
}

}

메뉴 아이템을 보여줍니다.


class Waitress(
private val allMenus: MenuComponent
) {

fun printMenu() {
allMenus.print()
}

fun printVegetarianMenu() {
...
}
}

웨이트리스는 printMenu() 함수 하나를 통해 메뉴를 출력할 수 있습니다.


val pancakeHouseMenu = Menu("팬케이트하우스 메뉴","아침 메뉴")
val dinerMenu = Menu("객체마을식당 메뉴","점심 메뉴")
val cafeMenu = Menu("카페 메뉴","저녁 메뉴")
val dessertMenu = Menu("디저트 메뉴","디저트를 즐겨 보세요!")

val allMenus = Menu("전체 메뉴","전체 메뉴")

allMenus.add(pancakeHouseMenu)
allMenus.add(dinerMenu)
allMenus.add(cafeMenu)

dinerMenu.add(
MenuItem("파스타","미라나라 소스 스파게티, 효모빵도 드립니다.", true, 3.89))

dinerMenu.add(dessertMenu)

dessertMenu.add(
MenuItem("애플 파이", "바삭바삭한 크러스트에 바닐라 아이스크림이 얹혀 있는 애플 파이", true, 1.59))

Waitress(allMenus).printMenu()

Menu와 MenuItem은 둘다 MenuComponent를 상속하고 있습니다. 따라서 저녁 메뉴 안에 디저트 메뉴를 서브로 추가해 줄 수가 있습니다. 

반응형
Comments