본문 바로가기

코딩 이야기/안드로이드&코틀린 공부

[안드로이드&코틀린 공부] 디자인패턴 (싱글톤, 어댑터, 옵저버)

반응형

디자인 패턴이란?

디자인 패턴은 객체 지향 프로그래밍에서 코딩을 할 때 발생하는 문제들을 깔끔하게 코딩하기 위한 방안들입니다.

주로 자주 발생하는 문제들을 해결하기 위한 것들로 개발자들에게 있어서 공부하면 코드를 효율적으로 설계할 수 있는 스킬입니다.

싱글톤 패턴

싱글톤 패턴이란?

싱글톤 패턴은 아무리 많은 객체를 생성해도, 단 하나의 인스턴스만을 생성한 것과 같은 디자인 패턴입니다.

원래 같으면 여러 개의 객체를 생성하면 각각의 객체가 가진 변수는 값을 공유하지 않지만, 싱글톤 패턴은 여러 개의 객체에서 같은 필드를 공유합니다.

객체를 생성할 때 생성자가 호출되고 메모리에 올라가는 등 비용이 발생하게 됩니다.

만약 객체를 생성하는 비용이 크다고 한다면, 객체를 자주 생성하는 일은 시스템에 큰 부담입니다.

싱글톤 패턴은 이러한 문제에 대응해, 객체를 한번만 생성하고 생성된 객체를 재사용 하면서 객체의 재생성 비용을 줄이는 패턴입니다.

전 예전에 싱글톤 패턴을 C++로 배워서 당연하게도 클래스의 생성자와 포인터를 사용해서 구현할거라고 생각했는데 코틀린은 C++과 다른 방식으로 구현을 해야했습니다.

object SingletonPattern{
    fun calcPlus(a : Int, b : Int) {
		println(a + b)
    }
}

fun main(){
	SingletonPattern.calcPlus(3,4)
}

그냥 클래스에 object만 붙여도 싱글톤이 됩니다.

다른 언어에서의 싱글톤 패턴은 동시성 문제를 가집니다.

하나의 객체를 여러 쓰레드에서 동시에 사용하는 문제가 발생할 수도 있습니다.

하지만 코틀린에서는 object 키워드 만으로 해결 가능합니다.

object 키워드를 사용하는 방법 말고도 다른 여러가지 방법이 있어서 한번 알아보겠습니다.

Eager initialization

class Singleton private constructor(){
    var su = 3;
    companion object {
        private var INSTANCE: Singleton = Singleton()

        fun getInstance(): Singleton {
            return INSTANCE
        }
    }
}

fun main(){
	var a = Singleton.getInstance()
    var b = Singleton.getInstance()
    println(a.su)
    a.su = 4
    println(b.su)
}

이 방법은 싱글톤의 객체를 사용하지 않아도 생성을 하는 방식입니다. 매우 단순하고 직관적이지만 객체를 사용하지 않는 시점에서도 객체를 생성하느라 불필요한 연산이 필요하게 되는게 단점입니다.

companion object를 통해 정적 멤버에 인스턴스와 getInstance()를 만들고 반환해주면 끝입니다.

그리고 생성자를 외부에서 사용못하도록 private해주면 됩니다.

Lazy Initialization

class Singleton private constructor(){
	var su = 3
    companion object {
        private var INSTANCE: Singleton? = null
		fun getInstance() : Singleton?{
            if(INSTANCE == null){
                INSTANCE = Singleton()
            }
            return INSTANCE
        }
    }
}

Lazy Initialization은 eager Initialization의 단점을 보완합니다. 객체 생성을 늦게 함으로써 불필요한 연산을 미리 할 필요가 없습니다.

인스턴스를 nullable로 둬서 인스턴스를 처음 반환해야할 때 생성을 하는 방식입니다.

어댑터 패턴

리사이클러뷰 어댑터도 공부하기

어댑터 패턴은 서로 호환되지 않는 인터페이스를 연결하는 디자인 패턴입니다.

기존 클래스를 수정하지 않고 특정 인터페이스를 필요로 하는 코드를 사용할 수 있게 해줍니다.

서로 다른 인터페이스를 가진 클래스들이 상호작용할 수 있도록 하여 코드 재사용성을 높입니다.

개발을 하다보면 외부의 라이브러리 클래스를 사용하고 싶은데, 당장의 코드에 맞지 않는 상황이 발생하곤 합니다.

또한 여러 자식 클래스가 부모클래스와 묶여있을 때, 부모 클래스를 수정하는 대신 사용하기도 한다고 합니다.

예를 들어 외부 라이브러리 중 숭실대의 강의실 정보를 csv로 제공하는 라이브러리가 있다고 합시다.

하지만 현재 팀 내에서 스프레드 시트 파일을 JSON으로 사용하고 있었다고 하면, 이를 csv를 JSON으로 변경하는 어댑터 코드를 만들어서 해결할 수 있습니다.

class Adaptee(val data: String) {
    fun fetchData(): String {
        return data
    }
}

interface Target {
    fun request(): String
}

class Adapter(private val adaptee: Adaptee) : Target {
    override fun request(): String {
        return adaptee.fetchData()
    }
}

fun main() {
    val adaptee = Adaptee("어댑터")

    val adapter = Adapter(adaptee)
    val result = adapter.request()

    println(result)
}

위 예제 코드를 보면 어댑티 클래스와 타켓 인터페이스의 메소드 구성이 다릅니다.

하지만 어댑터 클래스에서 타겟 인터페이스를 상속받고, 파라미터로 어댑티를 받으며 타겟 인터페이스의 메소드를 오버라이딩함과 동시에 파라미터인 어댑티의 메소드를 그 안에서 사용하여 어댑터 패턴을 구현하였습니다.

물론 장점만 있는 디자인 패턴은 아닙니다.

소스코드도 늘어나면서 코드가 복잡해지기도 하고 중간에 데이터를 변환하는 어댑터를 거치면서 추가적인 처리 시간과 오버헤드가 발생할 수도 있습니다.

옵저버 패턴

옵저버 패턴은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴입니다.

즉, 어떤 객체의 상태가 변할 때 그와 연관된 객체들에게 알림을 보내는 디자인 패턴입니다.

장점

  • 실시간으로 한 객체의 변경사항을 다른 객체에 전파할 수 있다.
  • 느슨한 결합으로 시스템이 유연하고 객체간의 의존성을 제거할 수 있다. 즉 다른 클래스를 직접적으로 사용하는 클래스의 의존성을 줄일 수 있습니다.

구현 방식을 떠올릴 때, 예를 들어 클래스 A에서 클래스 B의 이벤트가 일어날 때마다 메소드를 호출하게 하고 싶다고 합시다.

그러면 보통 생각하기에 클래스 B를 인스턴스화해서 처리하면 될 것처럼 보입니다. 하지만 아쉽게도 일방적으로 호출하게 되면 지금 클래스 B에서는 자신의 인스턴스를 누가 사용하는 지 알 수 없기 때문에 이벤트가 발생했음을 알릴 수가 없습니다.

그래서 이를 해결하기 위해 인터페이스를 사용하게 됩니다. 중간 매개체 역할이 되어서 인터페이스에 이벤트 발생을 알리고 다른 클래스에서 이를 확인하는 방식입니다.

그리고 이 인터페이스가 옵저버입니다.

반응형