본문 바로가기
SW Engineering/Design Patterns

(Design Pattern) 2. 구조 패턴

by jaesungLeee 2021. 9. 26.

출처 : https://korecm.github.io/DesignPatternIntro

1. 구조 패턴 (Structural Pattern)

구조 패턴 (Structural Pattern)은 클래스나 객체를 조합하여 더 큰 구조를 만드는 패턴이다. 예를 들어, 서로 다른 Interface를 갖는 2개의 객체를 하나로 묶어 단일 Interface를 제공하거나 객체들을 서로 묶어 새로운 기능을 제공하는 것이 구조 패턴을 사용하는 것이다. 즉, 개체 간의 관계를 실현할 수 있는 간단한 방법으로써 설계를 용이하게 하는 패턴이라고 볼 수 있다. 

구조 클래스 패턴은 상속을 통해 클래스나 인터페이스를 합성하고, 구조 객체 패턴은 객체를 합성하는 방법을 정의한다.

 

1-1. 어댑터 (Adapter)

특정 클래스의 Interface를 다른 Interface로 변환하여 다른 클래스가 사용할수 있도록 하는 패턴이다. 다른 말로, 'Adapter'를 Client가 요구하는 Interface를 구현하는 클래스로 감싸 호환되지 않는 두 가지 타입 관계의 연결을 제공하는 데 사용할 수 있다. 

이는 현재 존재하는 클래스가 다른 클래스와 함께 호환할 수 있도록 하는데, 코드의 수정없이 가능하다. 그렇기 때문에 기존 것을 재사용하거나 라이브러리를 수정할 수 없을 때 사용하게 된다.

 

예시.)

 

interface Temperature {
    var temperature: Double
}

class CelsiusTemperature(override var temperature: Double) : Temperature

class FahrenheitTemperature(var celsiusTemperature: CelsiusTemperature) : Temperature {

    override var temperature: Double
        get() = convertCelsiusToFahrenheit(celsiusTemperature.temperature)
        set(temperatureInF) {
            celsiusTemperature.temperature = convertFahrenheitToCelsius(temperatureInF)
        }

    private fun convertFahrenheitToCelsius(f: Double): Double = (f - 32) * 5 / 9

    private fun convertCelsiusToFahrenheit(c: Double): Double = (c * 9 / 5) + 32
}

 

Temperature Interface는 Double 형의 temperature의 변수를 갖고 있다. CelsiusTemperature 클래스는 Temperature Interface를 상속하여 temperature 변수를 오버라이딩한다. 또한, FahrenheitTemperature 클래스도 Temperature Interface를 상속하고, celsiusTemperature를 클래스 변수로 받아 convert.. Method를 사용하여 재정의 한다. 

아래와 같이 사용할 수 있다.

 

val celsiusTemperature = CelsiusTemperature(0.0)
val fahrenheitTemperature = FahrenheitTemperature(celsiusTemperature)

celsiusTemperature.temperature = 36.6
println("${celsiusTemperature.temperature} C -> ${fahrenheitTemperature.temperature} F")

fahrenheitTemperature.temperature = 100.0
println("${fahrenheitTemperature.temperature} F -> ${celsiusTemperature.temperature} C")

// 36.6 C -> 97.88000000000001 F
// 100.0 F -> 37.77777777777778 C

 

1-2. 브릿지 (Bridge)

브릿지 패턴은 구현뿐만 아니라 추상화 부분까지 변경시켜야 하는 경우에 사용할 수 있는 패턴이다. 구현 부분에서 추상층을 분리하여 각자 독립적으로 변형이 가능하고 확장이 가능하도록 하는 패턴이며 상속을 이용한 패턴으로 확장된 설계에 용이하다. 

브릿지 패턴을 사용하면 구현을 인터페이스에 결합시키지 않았기 때문에 구현과 추상화된 부분을 분리시킬 수 있는 장점이 있으며 추상화된 부분을 구현한 클래스를 바꿔도 클라이언트에는 영향을 끼치지 않는다.

 

1-3. 합성 (Composite)

객체를 component와 composite으로 구성하여 트리 구조로 구성하여 표현하는 패턴이다. 사용자가 단일 객체와 복합 객체 모두 동일하게 다루게 한다. 즉, 0개 이상의 유사한 객체를 구성하여 하나의 객체로 만들어 조작할 수 있도록 한다.

 

예시.)

 

open class Equipment(private var price: Int, private var name: String) {
    open fun getPrice(): Int = price
}

/* Composite */
open class Composite(name: String) : Equipment(0, name) {
    val equipments = ArrayList<Equipment>()

    fun add(equipment: Equipment) {
        this.equipments.add(equipment);
    }

    override fun getPrice(): Int {
        return equipments.map { it -> it.getPrice() }.sum()
    }
}

/* Leafs */
class Cabbinet : Composite("cabbinet")
class FloppyDisk : Equipment(70, "Floppy Disk")
class HardDrive : Equipment(250, "Hard Drive")
class Memory : Equipment(280, "Memory")

 

Equipment 클래스는 pricename 변수를 가지고 있고, getPrice( ) Method를 이용하여 price를 return할 수 있다. Composite 클래스는 Equipment 클래스를 상속받아 add( ) Method를 통해 equipments ArrayList에 추가하고 있다. 또한, getPrice( ) Method를 오버라이딩 하여 equipments ArrayList의 가격들의 합을 return 한다.

 

아래와 같이 사용할 수 있다.

 

var cabbinet = Cabbinet()
cabbinet.add(FloppyDisk())
cabbinet.add(HardDrive())
cabbinet.add(Memory())
println(cabbinet.getPrice())

// 600

 

cabbinet 변수는 기본적으로 Cabbinet 클래스가 Composite 클래스를 상속받기 때문에 name에 'cabbinet'을 가지고 있다.  add( ) Method를 통해 Equipment를 상속받는 FloopyDisk, HardDrive, Memory 클래스들을 추가하고 가격의 합을 출력한다.

 

1-4. 데코레이터 (Decorator)

객체를 동적으로 새로운 책임 (Responsibility)을 추가할 수 있게 하는 패턴으로 서브클래스를 생성하는 것보다 더 유연한 방법을 제공한다. 객체를 데코레이터 클래스의 객체로 감싸 런타임에 객체의 기능을 확장하거나 변경하는 데 사용된다. 

 

예시.)

 

interface CoffeeMachine {
    fun makeSmallCoffee()
    fun makeLargeCoffee()
}

class NormalCoffeeMachine : CoffeeMachine {
    override fun makeSmallCoffee() = println("Normal: Making small coffee")

    override fun makeLargeCoffee() = println("Normal: Making large coffee")
}

//Decorator
class EnhancedCoffeeMachine(val coffeeMachine: CoffeeMachine) : CoffeeMachine by coffeeMachine {

    // overriding behaviour
    override fun makeLargeCoffee() {
        println("Enhanced: Making large coffee")
        coffeeMachine.makeLargeCoffee()
    }

    // extended behaviour
    fun makeCoffeeWithMilk() {
        println("Enhanced: Making coffee with milk")
        coffeeMachine.makeSmallCoffee()
        println("Enhanced: Adding milk")
    }
}

 

CoffeeMachine Interface를 NormalCoffeeMachine이 구현하고 있다. EnhancedCoffeeMachine 클래스는 coffeeMachine이라는 클래스 변수를 파라미터로 사용한다. 이때, EnhancedCoffeeMachine 클래스는 CoffeeMachine Interface를 구현하지만 실제로는 파라미터로 받은 CoffeeMachine 클래스 변수를 위임받아 사용한다.

 

아래와 같이 사용할 수 있다.

 

val normalMachine = NormalCoffeeMachine()
val enhancedMachine = EnhancedCoffeeMachine(normalMachine)

enhancedMachine.makeSmallCoffee()        // 재정의 X

enhancedMachine.makeLargeCoffee()        // 재정의 O

enhancedMachine.makeCoffeeWithMilk()     // 확장


// Normal: Making small coffee
//
// Enhanced: Making large coffee
// Normal: Making large coffee
//
// Enhanced: Making coffee with milk
// Normal: Making small coffee
// Enhanced: Adding milk

 

NormalCoffeeMachine 타입의 normalMachine 변수를 EnhancedCoffeeMachine 타입의 enhancedMachine 변수의 파라미터로 넘긴다. 이렇게 되면, 재정의하지 않은 makeSmallCoffee( ) Method와 재정의한 makeLargeCoffee( ) Method, 확장한 makeCoffeeWithMilk( ) Method까지 사용할 수 있게 된다.

 

1-5. 퍼사드 (Facade)

서브 시스템을 더 쉽게 사용할 수 있도록 higher-level Interface를 정의하고 제공한다. 즉, 보다 복잡한 서브 시스템에 대한 단순한 Interface를 정의하는데 퍼사드 패턴을 사용할 수 있다.

퍼사드 패턴은 특정 기능에 대해 Interface의 개수가 확장되거나 시스템이 복잡해질 수 있는 상황에서 사용하기 적합하다. 또한, 비슷한 작업을 수행하는 다양한 Interface들 중 하나의 Interface를 Client에 제공해야 하는 상황에서 적용하는 것이 좋다. 

 

예시.)

 

class ComplexSystemStore(val filePath: String) {

    init {
        println("Reading data from file: $filePath")
    }

    val store = HashMap<String, String>()

    fun store(key: String, payload: String) {
        store.put(key, payload)
    }

    fun read(key: String): String = store[key] ?: ""

    fun commit() = println("Storing cached data: $store to file: $filePath")
}

data class User(val login: String)

//Facade:
class UserRepository {
    val systemPreferences = ComplexSystemStore("/data/default.prefs")

    fun save(user: User) {
        systemPreferences.store("USER_KEY", user.login)
        systemPreferences.commit()
    }

    fun findFirst(): User = User(systemPreferences.read("USER_KEY"))
}

 

ComplexSystemStore 클래스는 HashMap으로 구성된 store 변수를 가지고, store( ), read( ), commit( ) Method를 이용하여 store 변수를 조작한다.

User 클래스는 사용자 정보를 가지는 data class이며, UserRepository 클래스는 ComplexSystemStore 타입의 변수 systemPreferences를 갖는다. save( ) Method를 통해 systemPreferences 변수로 ComplexSystemStore 클래스에서 이루어졌던 Method를 사용한다. 

 

아래와 같이 사용할 수 있다.

 

val userRepository = UserRepository()
val user = User("jslee")
userRepository.save(user)
val resultUser = userRepository.findFirst()
println("Found stored user: $resultUser")

// Reading data from file: /data/default.prefs
// Storing cached data: {USER_KEY=hongbeom} to file: /data/default.prefs
// Found stored user: User(login=jslee)

 

1-6. 플라이웨이트 (Flyweight)

어떤 클래스의 인스턴스 하나를 이용하여 여러 개의 '가상 인스턴스'를 제공하고 싶을 때 사용할 수 있는 패턴이다. 

 

1-7. 프록시 (Proxy)

실제 기능을 수행하는 객체 대신 가상의 객체를 만들어 사용하는 패턴이다. 프록시의 의미 자체에서 알 수 있듯이 어떤 객체를 사용하고자 할 때, 해당 객체를 직접 참조하는 것이 아니라, 그 객체를 대신 (Proxy) 하는 객체를 통해 접근하는 방식이다. 이는, 해당 객체가 메모리상에 존재하지 않더라도 기본적인 정보를 참조하거나 설정할 수 있고, 실제 객체의 기능이 반드시 필요한 시점까지 객체 생성을 미룰 수 있는 장점이 있다. 

 

예시.)

 

interface File {
    fun read(name: String)
}

class NormalFile : File {
    override fun read(name: String) = println("Reading file: $name")
}

//Proxy:
class SecuredFile : File {
    val normalFile = NormalFile()
    var password: String = ""

    override fun read(name: String) {
        if (password == "secret") {
            println("Password is correct: $password")
            normalFile.read(name)
        } else {
            println("Incorrect password. Access denied!")
        }
    }
}

 

NormalFile과 SecuredFile 클래스는 File Interface를 구현하고 있다. NormalFile 클래스에서는 read( ) Method를 오버라이딩하여 재정의한다. SecuredFile 클래스에서는 NormalFile 객체를 생성하는 normalFile 변수를 통하여 read( ) Method를 재정의하고 있다. 이때, SecuredFile 클래스는 NormalFile의 프록시이며, 흐름을 다르게 할 뿐 반환 값을 변경하면 안 된다.

 

아래와 같이 사용할 수 있다.

 

val securedFile = SecuredFile()
securedFile.read("readme.md")

securedFile.password = "secret"
securedFile.read("readme.md")


// Incorrect password. Access denied!
// Password is correct: secret
// Reading file: readme.md

 


References

https://github.com/dbacinski/Design-Patterns-In-Kotlin#decorator

 

GitHub - dbacinski/Design-Patterns-In-Kotlin: Design Patterns implemented in Kotlin

Design Patterns implemented in Kotlin. Contribute to dbacinski/Design-Patterns-In-Kotlin development by creating an account on GitHub.

github.com

https://velog.io/@namezin/GoF-design-pattern

 

GoF 디자인 패턴

Gang of Four Design Pattern

velog.io

https://lee1535.tistory.com/106

 

[디자인패턴/Design Pattern] Flyweight Pattern / 플라이웨이트 패턴

관련 내용은 [자바 언어로 배우는 디자인 패턴 입문] , [Head First Design Pattern] 의 내용을 참고해서 정리한 글입니다. 잘못된 부분은 댓글로 피드백 부탁드립니다. 1. Flyweight 패턴이란? 어떤 클래스

lee1535.tistory.com

https://readystory.tistory.com/193

 

[구조 패턴] 퍼사드 패턴(Facade Pattern) 이해 및 예제

퍼사드 패턴(Facade Pattern)은 Flyweight 패턴, Adapter 패턴, Decorator 패턴처럼 구조 패턴 중 하나로, 클라이언트가 쉽게 시스템과 상호작용 할 수 있도록 도와주는 패턴입니다. 구조 패턴(Structural Pattern..

readystory.tistory.com