정상에서 IT를 외치다

[디자인패턴] 컴파운드 패턴 본문

디자인패턴

[디자인패턴] 컴파운드 패턴

Black-Jin 2019. 7. 6. 11:01
반응형

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

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


컴파운드 패턴


두 개 이상의 패턴을 결합하여 일반적으로 자주 등장하는 문제들에 대한 해법을 제공합니다. 즉 일련의 패턴을 함께 사용하여 다양한 디자인 문제를 해결하는 것을 컴파운드 패턴이라고 부릅니다. 즉 패턴으로 이루어진 패턴인 셈이죠.



패턴 섞어 쓰기


- 하나의 디자인 문제를 해결하기 위해 여러 패턴을 함께 사용하는 경우가 종종 있습니다.

- 컴파운드 패턴이란 반복적으로 생길 수 있는 일반적인 문제를 해결하기 위한 용도로 두 개 이상의 패턴을 결합해서 사용하는 것을 뜻합니다.




오리와의 재희


1. 우선 Quackable 인터페이스를 만듭니다.

interface Quackable {
fun quack()
}


2. Quackable을 구현해서 오리 클래스를 만듭니다.

class MallardDuck : Quackable {
override fun quack() {
println("Quack")
}
}

class RedheadDuck : Quackable {
override fun quack() {
println("Quack")
}
}


3. 오리는 이제 다 준비된 것 같으니깐 시물레이터를 만들어야 되겠군요.

private fun simulate(duck: Quackable) {
duck.quack()
}


4. 오리 있는 곳에 거위도 있다.




어떤 패턴을 활용하면 거위들이 오리들하고 잘 어울려 놀 수 있게 될까요?


어댑터 패턴

한 인터페이스를 다른 인터페이스로 변환하기 위한 용도로 사용됩니다.



5. 거위용 어댑터가 필요합니다.

class GooseAdapter(
private val goose: Goose) : Quackable {

override fun quack() {
goose.hock()
}

}


6. 이제 거위들도 시물레이터에 들어갈 수 있습니다.

val goose: Quackable = GooseAdapter(Goose())


7. 잽싸게 한 번 돌려봅니다.

fun main() {
val mallardDuck: Quackable = MallardDuck()
val redheadDuck: Quackable = RedheadDuck()
val goose: Quackable = GooseAdapter(Goose())

println("Duck Simulator")

simulate(mallardDuck)
simulate(redheadDuck)
simulate(goose)

}




오리 클래스는 그대로 두면서 오리가 꽥소리를 낸 회수를 세려면 어떻게 해야 할까요?


데코레이턴 패턴

주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴으로 기능 확장이 필요할 때 서브클래스 대신 쓸 수 있습니다.


8. 불쌍한 꽥학자들에게 꽥소리를 낸 회수를 세 주는 기능을 선물해 봅시다.

class QuackCounter(
private val duck: Quackable) : Quackable {

companion object {

var numberOfQuacks = 0

fun getQuacks() = numberOfQuacks
}

override fun quack() {
duck.quack()
numberOfQuacks++
}
}


9. 시물레이터를 고쳐서 모든 오리들을 데코레이터로 감싸줍니다.

fun main() {
val mallardDuck: Quackable = QuackCounter(MallardDuck())
val redheadDuck: Quackable = QuackCounter(RedheadDuck())
val goose: Quackable = QuackCounter(GooseAdapter(Goose()))

println("Duck Simulator")

simulate(mallardDuck)
simulate(redheadDuck)
simulate(goose)

println("The ducks quacked ${QuackCounter.getQuacks()} times")
}




오리를 생성하고 데코레이터로 감싸는 부분을 따로 빼내서 캡슐화하면 어떨까요?


생성 부분을 캡슐화 해줄러면 팩토리 패턴이 필요합니다. 팩토리 패턴은 크게 팩토리 메소드 패턴과 추상 팩토리 패턴으로 나눠 볼 수 있습니다.


간단한 팩토리

객체 생성을 처리하는 클래스를 팩토리라고 부릅니다. 간단한 팩토리는 디자인 패턴이라고는 할 수 없습니다. 프로그래밍을 하는데 있어서 자주 쓰이는 관용주에 가깝다고 할 수 있습니다.


팩토리 메소드 패턴

팩토리 메소드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만듭니다. 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것이죠.


추상 팩토리 패턴

추상 팩토리 패턴에서는 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있습니다.


10. 오리를 생산하기 위한 팩토리가 필요합니다.


팩토리 메소드 패턴을 구현하기 위해 객체를 생성하기 위한 인터페이스를 정의합니다.

abstract class AbstractDuckFactory {

abstract fun createMallardDuck(): Quackable
abstract fun createRedheadDuck(): Quackable
abstract fun createGoose(): Quackable
}


오리를 생성하기 위한 DuckFactory, 카운팅이 가능한 오리를 생성하기 휘한 CountingDuckFactory를 만들어 줍니다.

class DuckFactory : AbstractDuckFactory() {

override fun createMallardDuck(): Quackable {
return MallardDuck()
}

override fun createRedheadDuck(): Quackable {
return RedheadDuck()
}

override fun createGoose(): Quackable {
return GooseAdapter(Goose())
}
}

class CountingDuckFactory : AbstractDuckFactory() {

override fun createMallardDuck(): Quackable {
return QuackCounter(MallardDuck())
}

override fun createRedheadDuck(): Quackable {
return QuackCounter(RedheadDuck())
}


override fun createGoose(): Quackable {
return QuackCounter(GooseAdapter(Goose()))
}
}


자 이제 우리는 오리만을 생성하고 싶을 때는 DuckFactory를 생성하고 카운팅이 가능한 오리를 생성하기 휘한 CountingDuckFactory를 생성하여 사용하면 됩니다. 여기서 클라이언트는 팩토리를 생성해 사용할 뿐 어떤 클래스가 만들어 지는지 모릅니다. 아래 시물레이터를 보고 확인해 보겠습니다.


11. 이제 팩토리를 쓰도록 시물레이터를 고쳐 봅시다.

fun main() {

val duckFactory: AbstractDuckFactory = CountingDuckFactory()

val mallardDuck: Quackable = duckFactory.createMallardDuck()
val redheadDuck: Quackable = duckFactory.createRedheadDuck()
val goose: Quackable = duckFactory.createGoose()

println("Duck Simulator")

simulate(mallardDuck)
simulate(redheadDuck)
simulate(goose)

println("The ducks quacked ${QuackCounter.getQuacks()} times")
}


CountingDuckFactory 를 생성하여 꿱 소리를 셀 수 있는 오리들을 가져와 시물레이팅 하고 있습니다. main 함수 즉 클라이언트를 보면 오리 객체가 어떻게 만들어지는 지는 전혀 알 수 없습니다.




여러 오리들에 대해서 한꺼번에 같은 작업을 적용할 수 있는 방법은 무엇일까요?


컴포지트 패턴

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


12. 오리떼를 만들어 봅시다.

class Flock : Quackable {
private val mQuackers = arrayListOf<Quackable>()

fun add(vararg quackers: Quackable) {
for(quacker in quackers) {
mQuackers.add(quacker)
}
}

override fun quack() {
for (quacker in mQuackers) {
quacker.quack()
}
}
}


13. 시물레이터를 고쳐 봅시다.

fun main() {

val duckFactory: AbstractDuckFactory = CountingDuckFactory()

val mallardDuck: Quackable = duckFactory.createMallardDuck()
val redheadDuck: Quackable = duckFactory.createRedheadDuck()
val goose: Quackable = duckFactory.createGoose()

val flockOfDucks = Flock()

flockOfDucks.add(mallardDuck, redheadDuck, goose)

val flockOfMallards = Flock()

val mallardOne = duckFactory.createMallardDuck()
val mallardTwo = duckFactory.createMallardDuck()
val mallardThree = duckFactory.createMallardDuck()
val mallardFour = duckFactory.createMallardDuck()

flockOfMallards.add(mallardOne, mallardTwo, mallardThree, mallardFour)

flockOfDucks.add(flockOfMallards)

println("모든 오리들에 대한 테스트")
//모든 오리들에 대한 테스트
simulate(flockOfDucks)

println("\n물오리떼에 대해서만 테스트")
//물오리떼에 대해서만 테스트
simulate(flockOfMallards)

println("\nThe ducks quacked ${QuackCounter.getQuacks()} times")
}




꽥꽥거리는 오리들을 하나씩 실시간으로 추적할 수 있는 기능을 적용할 수 있을까요?


옵저버 패턴

한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의합니다.



출처 : 위키피디아


위키피디아의 설명을 좀 더 보겠습니다. 이 패턴의 핵심은 옵저버 또는 리스너라 불리는 하나 이상의 객체를 관찰 대상이 되는 객체에 등록시킵니다. 그리고 각각의 옵저버들은 관찰 대상인 객체가 발생시키는 이벤트를 받아 처리합니다.UML 다이어그램에서 보시는 바와 같이 관찰 대상인 객체는 "이벤트를 발생시키는 주체"라는 의미에서 Subject로 표시됩니다. 안드로이드를 예로 보면 버튼에 클릭 리스너를 등록시킵니다. 여기서 클릭 리스너가 옵저버가되고 관찰 대상이되는 객체는 버튼은 Subject 즉 주제가 됩니다.


button.setOnClickListener(object : View.OnClickListener {
override fun onClick(p0: View?) {
TODO("not implemented")
}

})


어기서 관찰 대상이 되는 객체인 button이 subject 입니다. 그리고 관찰 대상인 객체를 계속 바라보며 어떤 신호가 오는지를 항상 체크해야되는 익명 클래스인 View.OnClickLIstener는 observer가 되겠죠? 


14. Observable 인터페이스가 필요합니다.


관찰 대상 되는 객체 subject를 (여기서는 오리가 관찰 대상입니다) 만들기 위한 인터페이스 입니다. 클래스 다이어그램에서 보시는 바와 같이 registerObserver와 notifyObservers 함수를 선언해 줍니다.


interface QuackObservable {
fun registerObserver(observer: Observer) // 관찰자 등록
fun notifyObservers() // 관찰자에게 연락을 돌리기 위한 메소드
}


15. Quackable을 구현하는 모든 구상 클래스에서 QuackObservable에 있는 메소드를 구현하도록 만듭니다.


오리를 구현하기 위한 인터페이스인 Quackable에 오리를 Subject로 만들어주기 위해 추가로 QuackObservable을 구현해줍니다


interface Quackable : QuackObservable {
fun quack()
}


.아래 코드를 보면 여러 관찰자(obsever)를 등록하기 위한 arraylist인 obsevers를 생성하여 observer를 등록해줍니다. 그리고 subject에서 notifyObservers가 발생하면 등록되어 있는 옵저버들에게(여기서는 꽥 학자들 입니다) 알림을 보내줍니다. 


class MallardDuck : Quackable {
private val observers = arrayListOf<Observer>()

override fun quack() {
println("Quack")
notifyObservers()
}

override fun registerObserver(observer: Observer) {
observers.add(observer)
}

override fun notifyObservers() {
val iterator = observers.iterator()
while (iterator.hasNext()) {
val observer = iterator.next()
observer.update(this)
}
}

override fun toString(): String {
return "MallardDuck"
}
}


16. Observer보조 객체와 Quackable 클래스를 결합합니다.


Subject가 되는 오리들은 다양합니다. 이에 registerObservers와 notifyObservers에 들어갈 코드를 하나의 class로 묶어서 구현해주겠습니다.


class Observable(
private val duck: QuackObservable
) : QuackObservable {
private val observers = arrayListOf<Observer>()

override fun registerObserver(observer: Observer) {
observers.add(observer)
}

override fun notifyObservers() {
val iterator = observers.iterator()
while (iterator.hasNext()) {
val observer = iterator.next()
observer.update(duck)
}
}
}


보조클래스인 Observable을 만들어 줍니다. 등록 및 연락용 코드를 Observable이라는 한 클래스에 캡슐화해 놓은 다음 구성을 통해서 QuackObservable에 포함시키는 겁니다. 이렇게 하면 실제 코드는 한 군데에만 작성해 놓고, Quackable에서는 필요한 작업을 Observable이라는 보조 클래스에 전부 위임하면 됩니다. 아래 코드는 오리에서 registerObserver와 notifyObservers를 구현해야 되는 코드를 Observable에 위임한 모습니다.  다른 오리들도 동일하게 작업하면 되겠죠?


class MallardDuck : Quackable {
private val observable = Observable(this)

override fun quack() {
println("Quack")
notifyObservers()
}

override fun registerObserver(observer: Observer) {
observable.registerObserver(observer)
}

override fun notifyObservers() {
observable.notifyObservers()
}

override fun toString(): String {
return "MallardDuck"
}
}


17. 옵저버 패턴의 옵저버 쪽만 완성하면 됩니다.


interface Observer {
fun update(duck: QuackObservable)
}

class Quackologist : Observer {

override fun update(duck: QuackObservable) {
println("Quackologist : $duck just quacked.")
}
}


오리를 관찰하기 위한 옵저버, 꽥하자를 구현해줍니다. 오리에서 quack() 함수가 실행되면 옵저버의 update 함수가 호출됩니다.


18. 시물레이터를 고치고 다시 테스트해 봅시다.


fun main() {

// 오리 팩토리와 오리 생성
val duckFactory: AbstractDuckFactory = DuckFactory04()

val mallardDuck: Quackable = duckFactory.createMallardDuck()
val redheadDuck: Quackable = duckFactory.createRedheadDuck()

println("\nDuck Simulator : With Observer")
val quackologist = Quackologist()

println()

mallardDuck.registerObserver(quackologist)
redheadDuck.registerObserver(quackologist)

simulate(mallardDuck)
simulate(redheadDuck)

println("\nThe ducks quacked ${QuackCounter04.getQuacks()} times")
}


추가로 우리는 컴포지트 패턴을 사용해 오리떼를 구현해 주었습니다. 옵저버 패턴을 구현하기 위해서는 오리떼의 코드도 수정해야합니다.


class Flock : Quackable {

private val mQuackers = arrayListOf<Quackable>()

fun add(vararg quackers: Quackable) {
for(quacker in quackers) {
mQuackers.add(quacker)
}
}

override fun quack() {
for (quacker in mQuackers) {
quacker.quack()
}
}

override fun registerObserver(observer: Observer) {
for (quacker in mQuackers) {
quacker.registerObserver(observer)
}
}

override fun notifyObservers() {
//Quackable 객체에서 알아서 옵저버한테 연락을 돌리기 때문에 Flock 자체에서는 아무 일도 하지 않아도 됩니다.
}
}


이렇게 오리와의 재회에서 어댑터, 데코레이터, 팩토리 그리고 컴포지트 패턴을 활용해 보았습니다. 하지만 이렇게 다양한 패턴들을 단순히 함께 구현한다고 해서 컴파운드 패턴이라고 부르지 않습니다. 책에서는 컴파운드 패턴의 예로 MVC를 소개합니다.




컴파운드 패턴


MVC에서 Mode은 옵저버 패턴, View는 컴포지트 패턴 그리고 C는 스트래티지 패턴을 사용해 구현하였습니다. 이렇듯 단순 패턴들을 같이 사용하는 것이 아니라 여려 패턴을 결합하여 일반적으로 자주 등장하는 문제들에 대한 해법을 제공해 주는것이 바로 컴파운드 패턴입니다.

반응형
Comments