[Kotlin]디자인 패턴 정리

tkppp·2022년 6월 21일
0

어댑터 패턴

어댑터 패턴은 기존 인터페이스와 호환되지 않는 클래스를 기존 코드를 변경하지 않고 기존 인터페이스를 구현하는 중간 어댑터 클래스를 통해 작동하도록 하는 패턴이다

예시

// 기존 코드
interface JdbcDriver {
    fun findAll()
}

class MysqlDriver : JdbcDriver {
    private val name = "MySQL"

    override fun findAll() {
        println("$name - Find all")
    }
}

class JdbcApi(private val driver: JdbcDriver) {
    fun findAll() {
        driver.findAll()
    }
}

fun main() {
    var api = JdbcApi(MysqlDriver())
    api.findAll()
}

findAll() 메소드를 가지는 JdbcDriver 인터페이스를 구현하는 클래스를 JdbcAdapter 클래스에 주입해 사용하고 있는 상태이다.

여기서 PostgresqlDriver 라는 외부 라이브러리를 사용해야하는 상황이라고 가정하자. 이 클래스는 외부 라이브러리이기 때문에 JdbcDriver 를 구현하지 않아 JdbcAdapter 에 의존성을 주입할 수 없다. 이럴 경우 어댑터 패턴을 사용할 수 있다

// 외부 라이브러리 코드
class PostgresqlDriver {
    fun findAll() {
        println("PostgreSQL - Find all")
    }
}

// 어댑터 패턴으로 구현한 어댑터 클래스
class PostgresqlAdapter : JdbcDriver {
    val driver = PostgresqlDriver()

    override fun findAll() {
        driver.findAll()
    }
}

fun main() {
    var api = JdbcApi(MysqlDriver())
    api.findAll()

    api = JdbcApi(PostgresqlAdapter())
    api.findAll()
}

/* 출력
MySQL - Find all
PostgreSQL - Find all
*/

PostgresqlAdapter 클래스에서 외부 라이브러리 의존성을 주입하고 JdbcDriver 를 구현해 어댑터 패턴으로 구현하였다.

프록시 패턴

Proxy는 대리자, 대변인의 의미를 갖는 단어로 누군가를 대신해 그 역할을 수행하는 존재를 뜻한다. 프록시 패턴은 어떤 클래스의 동작을 대신 수행하는 프록시 클래스를 통해 동작한다.

위의 어댑터 패턴에 사용된 JdbcApi 클래스도 프록시 패턴을 사용한 클래스이다

쓰임

  • 인터페이스를 구현한 클래스들의 동작에 실행 제어가 필요한 경우(예: 로깅, 권한에 따른 실행 제어 등)이 필요한 경우 사용할 수 있다
  • 리소스가 많이 드는 객체를 항상 생성하지 않는다면 객체의 로딩을 연기하여 리소스의 낭비를 줄일 수 있다(예: JPA의 Lazy Loding)

예시

interface FooService {
    fun runSomething()
}

class ServiceA : FooService {
    override fun runSomething() {
        println("ServiceA running")
    }
}

class ServiceB : FooService {
    override fun runSomething() {
        println("ServiceB running")
    }
}

class Proxy() : FooService {
    lateinit var service: FooService

    override fun runSomething() {
        println("Proxy Running...")
        service.runSomething()
    }
}

fun main() {
    // no proxy
    var service: FooService = ServiceA()
    service.runSomething()
    service = ServiceB()
    service.runSomething()

    // with proxy
    val proxy = Proxy()
    proxy.service = ServiceA()
    proxy.runSomething()
    proxy.service = ServiceB()
    proxy.runSomething()
}

/* 출력
ServiceA running
ServiceB running
Proxy Running...
ServiceA running
Proxy Running...
ServiceB running
*/

데코레이터 패턴

데코레이터 패턴은 프록시 패턴과 유사하지만 프록시 패턴과 다르게 메소드 호출의 결과에 변화를 주기 위해 사용하는 패턴이다.

예시

interface FooService {
    fun runSomething(): String
}

class ServiceA : FooService {
    override fun runSomething(): String = "Service A"
}

class Decorator : FooService {
    lateinit var service: FooService

    override fun runSomething(): String
        = "With decorator " + service.runSomething()
}

fun main() {
    val service: FooService = ServiceA()
    val decorator = Decorator()
    decorator.service = service

    println(service.runSomething())
    println(decorator.runSomething())
}

/* 출력
Service A
With decorator Service A
*/

싱글톤 패턴

싱글톤 패턴은 인스턴스를 하나만 만들어 사용하기 위한 패턴으로 다수의 객체가 불필요한 커넥션 풀, 스레드 풀과 같은 경우 인스턴스를 하나만 만들고 이를 재사용하여 불필요한 리소스의 낭비를 줄인다.

예시

object Foo {
    var name = "foo"
}

class Bar private constructor(var name: String) {
    companion object {
        private var instance: Bar? = null
        fun getInstance(): Bar = instance ?: Bar("bar").also { instance = it }
    }
}

fun main(){
    val s1 = Foo
    val s2 = Bar.getInstance()

    s1.name = "new Foo"
    s2.name = "new Bar"

    println(Foo.name)
    println(Bar.getInstance().name)
}

/* 출력
new Foo
new Bar
*/

자바에서는 생성자를 private으로 만들고 정적 메소드를 통해 객체를 반환하게 하여 싱글톤 패턴을 구현한다. 하지만 코틀린은 언어 레벨에서 object 키워드로 하여금 싱글톤을 지원한다.

companion object 를 통해 자바와 같이 싱글톤을 구현할수도 있다

스트레티지 패턴

스트레티지 패턴은 객체의 행위를 전략이라 부르는 캡슐화한 알고리즘에 의존하는 패턴이다. 캡슐화된 알고리즘은 인터페이스를 구현한 객체로 다형성을 활용한다. 필요한 전략을 그때 그때 선택하여 객체의 행위를 결정한다.

예시

interface PaymentStrategy {
    fun pay(price: Int)
}

class KakaoPay: PaymentStrategy {
    override fun pay(price: Int) {
        println("$price Pay completed with KakaoPay")
    }
}

class NaverPay: PaymentStrategy {
    override fun pay(price: Int) {
        println("$price Pay completed with NaverPay")
    }
}

class ShoppingCart(
    val items: MutableList<Int> = mutableListOf()
) {

    fun getTotalPrice(): Int = items.fold(0) { total, price -> total + price }

    fun pay(paymentMethod: PaymentStrategy) {
        paymentMethod.pay(getTotalPrice())
    }
}

fun main() {
    val cart = ShoppingCart()
    cart.items.addAll(listOf(100, 500, 700))

    cart.pay(KakaoPay())
    cart.pay(NaverPay())
}

/* 출력
1300 Pay completed with KakaoPay
1300 Pay completed with NaverPay
*/

옵저버 패턴

옵저버 패턴은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다. 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다.

예시

interface Leader {
    fun subscribe(follower: Follower)
    fun unsubscribe(follower: Follower)
    fun notifyMember(msg: String)
}

interface Follower {
    fun update(msg: String)
}

class Member(val name: String, private val followers: MutableList<Follower> = mutableListOf()) : Leader, Follower {
    override fun subscribe(follower: Follower) {
        followers.add(follower)
    }

    override fun unsubscribe(follower: Follower) {
        followers.remove(follower)
    }

    override fun notifyMember(msg: String) {
        followers.forEach { it.update(msg) }
    }

    override fun update(msg: String) {
        println("$name - $msg")
    }

    fun writeTwit(twit: String){
        notifyMember("$name twit: $twit")
    }
}

fun main() {
    val m1 = Member("Harry")
    val m2 = Member("Potter")
    val m3 = Member("Ron")
    val m4 = Member("Wisely")

    m1.subscribe(m2)
    m1.subscribe(m3)
    m1.subscribe(m4)

    m1.writeTwit("twit!")
}

/* 출력
Potter - Harry twit: twit!
Ron - Harry twit: twit!
Wisely - Harry twit: twit!
*/

0개의 댓글