[Kotlin] 디자인패턴(Singleton, Adapter, Observer)

신민준·2024년 11월 8일
1

Singleton Pattern이란

The singleton pattern is one of the creational design patterns that is used to limit the instances of a class to one. That means with the Singleton pattern, only one instance of a class exists in the whole project with global access.
Singleton is a creational design pattern that lets you ensure that a class has only one instance while providing a global access point to this instance.

Singleton Pattern은 객체의 인스턴스가 오직 1개만 생성되는 패턴으로 생성자가 여러 차례 호출되어도 실제로 생성되는 객체는 하나이고 맨 처음에 생성된 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 return한다.

Kotlin에서 구현하기

public class Singleton {

    private static Singleton instance = new Singleton();
    
    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }

    public void say() {
        System.out.println("hi, there");
    }
}

Singleton Pattern의 기본적인 구현 방법으로 유의해야할 점은 다음과 같다.

  • 외부에서 추가적인 객체 생성을 막기 위해 해당 클래스 생성자가 private인지 확인해야한다.
  • 클래스 내부에 getInstance() 함수를 선언하여 외부로 객체를 반환할 때 이미 생성된 instance를 반환한다.

다른 방법으로는 companion object를 사용하는 방법이 있다.

class EventManager private constructor() {
    companion object{
        private val instance = EventManager()

        fun sharedInstance(): EventManager {
            return instance
        }
    }
}

먼저 companion object는 클래스 내부의 자바의 static과 비슷한 역할을 수행한다고 볼 수 있다.
(물론 정확하게 분류하면 companion object는 객체이며 하나의 클래스에는 오직 하나의 companion object만 존재할 수 있는 등 엄연히 다르다.)

  • private constructor()를 통해 외부에서 직접 인스턴스를 생성할 수 없다.
  • EventManager 클래스 내부에 companion object가 있기 때문에, sharedInstance() 메서드에 EventManager.sharedInstance()와 같이 직접 접근할 수 있다.
  • EventManage의 유일한 인스턴스가 companion object 내부에서 생성되어 이 변수는 private이므로 외부에서는 접근할 수 없다.

여기서 보다 더 안전하게 Singleton Pattern을 구현하고자 한다면 Singleton 인스턴스를 @Votaile로 표시하면 된다.

Volatile

@Volatile은 변수에 대한 스레드 간 일관성을 보장하기 위해 사용되는 키워드로 주로 멀티스레드 환경에서 특정 변수에 여러 스레드가 동시에 접근할 때, 각 스레드가 항상 최신의 값을 읽도록 강제하는 역할을 한다.

class Singleton private constructor() {

    companion object {

        @Volatile private var instance: Singleton? = null

        fun getInstance() =
            instance ?: synchronized(this) { 
                instance ?: Singleton().also { instance = it }
            }
    }
}

이 코드에서는 instance가 null일 경우 synchronized 블록에 진입하여 인스턴스를 생성한다. 이 블록은 한 번에 하나의 스레드만 진입할 수 있어, 여러 스레드가 동시에 인스턴스를 생성하는 것을 방지한다.

@Volotile로 표시했을 때는 다음과 같은 이점이 존재한다.

  • 인스턴스에 대한 읽기 및 쓰기가 컴파일러나 프로세서에 의해 재정렬되지 않는다.
  • 한 스레드에서 변경한 내용은 다른 스레드에서 즉시 볼 수 있다.

그렇다면 왜 Singleton Pattern을 사용할까?

1. 메모리 측면

  • 한 개의 인스턴스만을 고정 메모리 영역에 생성하고 이후 외부에서 해당 객체에 접근할 때 메모리 낭비를 방지할 수 있다. 또한 이미 생성된 인스턴스를 활용하니 속도 측면에서도 이점이 있다고 볼 수 있다.

2. 데이터 공유가 쉽다

  • Singleton 인스턴스는 전역으로 사용되는 인스턴스이기에 다른 클래스의 인스턴스들이 접근해서 사용할 수 있다.
    * 이떄 동시 접근 문제가 발생할 수 있어 @Volatile을 사용하거나 설계에 유념

Singleton Pattern의 단점

1. 확장성 및 테스트 가능성 감소

  • 글로벌 상태를 만들면 애플리케이션의 여러 구성 요소 간에 종속성과 결합을 도입하여 수정 또는 확장하기 어렵게 만들 수 있다.
    또한 싱글톤 인스턴스는 자원을 공유하고 있기 때문에 테스트가 결정적으로 격리된 환경에서 수행되려면 매번 인스턴스의 상태를 초기화시켜주어 한다.

2.복잡성 및 위험 증가

  • 동시성 문제, private 생성자로 인한 자식 클래스 생성의 어려움 등 Singleton 클래스의 설계 및 구현에 주의가 필요하다. 제대로 수행하지 않으면 버그, 장기생존으로 인한 메모리 누수 또는 성능 병목 현상의 원인이 될 수 있다.

Adapter Pattern이란

Adapter Pattern은 기존 클래스의 인터페이스를 사용하고자 하는 다른 인터페이스로 변환해주어 호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있도록 어댑터로서 사용되는 구조 패턴이다.

Adapter Pattern의 구성요소는

  • Target : Client가 사용하고자 하는 인터페이스
  • Adaptee : Client가 갖고 있는 인터페이스
  • Adapter : Target 인터페이스를 구현하는 클래스
  • Client : Target 인터페이스를 사용하고자 하는 주체

// A 인터페이스
interface AInterface {
    fun methodA()
}

// B 인터페이스
interface BInterface {
    fun methodB()
}

// A 인터페이스 구현 클래스
class AClass : AInterface {
    override fun methodA() {
        println("A 클래스의 methodA 호출")
    }
}

// 어댑터 클래스: B 인터페이스를 구현하면서 내부적으로 A 클래스를 사용
class AtoBAdapter(private val aClass: AClass) : BInterface {
    override fun methodB() {
        println("어댑터가 B 인터페이스의 methodB를 호출함.")
        aClass.methodA()  // A 클래스의 methodA를 호출하여 B 인터페이스에 맞게 동작
    }
}

// 클라이언트 코드: B 인터페이스를 기대
fun clientCode(bInterface: BInterface) {
    bInterface.methodB()
}

// 사용 예시
fun main() {
    val aClass = AClass()               // A 인터페이스를 구현하는 클래스
    val adapter = AtoBAdapter(aClass)    // AtoBAdapter로 AClass를 B 인터페이스에 맞게 변환

    clientCode(adapter)  // 클라이언트 코드는 B 인터페이스를 사용하는 것처럼 작동
}

AClass 객체를 AtoBAdapter로 감싸서 clientCode()에 전달함으로써 AClassBInterface처럼 사용할 수 있다.

Adapter Pattern은 호환 작업 방식에 따라 두 종류로 나뉜다.

  • 객체 어댑터(Object Adatper)
  • 클래스 어댑터(Class Adatper)

객체 어댑터(Object Adapter)

객체 어댑터는 Composition(합성)을 통해 구현한다.

객체 어댑터 방식은 기존 클래스(Adaptee)의 인스턴스를 Adapter 클래스 내부에 포함시켜 필요한 인터페이스(Target)를 변환해주는 방식으로 Adatper 클래스가 원래 클래스의 인스턴스를 포함하고, 해당 인스턴스의 메서드를 호출하여 인터페이스 간의 호환성을 제공하는 방식이다.

코드로 보면 이와 같은 형태이다.

interface TargetInterface {
    fun request() // 클라이언트가 호출하는 메서드
}

class AdapteeClass {
    fun specificRequest() {
        println("기존 클래스의 specificRequest 메서드 호출")
    }
}

class ObjectAdapter(private val adaptee: AdapteeClass) : TargetInterface {
    override fun request() {
        println("어댑터에서 request를 호출하여 specificRequest로 변환")
        adaptee.specificRequest() 
    }
}

fun clientCode(target: TargetInterface) {
    target.request()
}

fun main() {
    val adaptee = AdapteeClass()                
    val adapter = ObjectAdapter(adaptee)        

    clientCode(adapter) 
}

Client는 TargetInterfacerequest()를 호출하지만 ObjectAdapter를 통해 AdapteeClassspeicifcRequest()가 호출된다.

객체 어댑터(Object Adapter)의 장단점

장점

  • 객체 어댑터는 합성을 통해 구현되는데 합성은 두 객체 사이의 의존성은 런타임에 해결하므로 런타임 중에 Adaptee가 결정되어 유연하다.

단점

  • 어댑터마다 추가적인 객체가 생성되기 때문에, 빈번한 어댑터 사용이 필요한 경우 성능이 저하될 수 있다.

클래스 어댑터(Class Adapter)

클래스 어댑터는 상속(Inheritance)를 통해 구현된다.

Adapter 클래스가 두 인터페이스를 모두 구현하고 기존 클래스의 기능을 상속하여 원하는 인터페이스로 변환하는 방식의 Adapter이다.

객체 어댑터와 다른 점은 Adaptee(Service)를 상속했기 때문에 따로 객체 구현없이 바로 코드 재사용이 가능하다는 것이다.
즉 클래스 어댑터는 클라이언트와 서비스 양쪽에서 행동들을 상속받기에 객체를 래핑할 필요가 없다.
그러나 클래스 어댑터는 다중 상속을 통해 구현되므로 다중 상속을 지원하지 않는 언어에서는 구현하기 용이하지 않다.

코드로 보면 이와같은 형태이다

interface BInterface {
    fun methodB()
}

open class AClass {
    fun methodA() {
        println("A 클래스의 methodA 호출")
    }
}

class ClassAdapter : AClass(), BInterface {
    override fun methodB() {
        println("어댑터에서 B 인터페이스의 methodB를 호출함")
        methodA()  
    }
}

fun clientCode(bInterface: BInterface) {
    bInterface.methodB()
}

fun main() {
    val adapter = ClassAdapter()  
    clientCode(adapter)           
}

ClientClassAdpaterBInterface를 구현하고, AClass를 상속하여 methodB()를 override하여 상속받은 methodA()를 호출한다.

클래스 어댑터(Class Adapter)의 장단점

장점

  • 어댑터가 Adaptee를 상속받기 때문에, 추가 객체 생성 없이 Adaptee의 메서드를 바로 호출할 수 있다.

단점

  • Adaptee의 서브클래스이기 때문에 Adaptee의 서브클래스로서의 제한을 받는다.

Adapter Pattern의 장점

1.호환성 제공

  • 기존 코드를 수정하지 않고도 클라이언트가 원하는 인터페이스로 변환하여 호환성을 제공한다.

2.재사용성 증가

  • 외부 라이브러리나 기존 클래스 등 수정이 불가능한 코드도 클라이언트 요구에 맞게 변환하여 재사용할 수 잇다.

3.유연성 제공

  • 어댑터를 이용해 여러 인터페이스를 서로 호환할 수 있으므로 코드의 유연성이 높아진다.

어댑터 패턴의 단점

복잡도 증가

  • 간단한 경우에도 새로운 클래스를 추가하게 되어 코드의 복잡도가 증가할 수 있고 이에 따라 코드가 다소 난잡해질 경우 유지보수가 어려워진다.

Observer Pattern이란

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

  • 위키피디아

즉 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 알림을 보내는 방식으로 상태 변화가 여러 객체에 전파되어야 하는 경우에 다른 객체의 상태 변화를 별도의 함수 호출 없이 즉각적으로 알 수 있어 이벤트에 대한 처리를 자주해야 하는 프로그램에서 매우 유용하다.

Observer Pattern의 구성

Observer Pattern의 구성요소는 패턴의 이름대로 객체를 Observe하는 Observer와 Observe되는 Subject로 이루어져있다.
(SubjectPublisher, Observable라고도 하고 ObserverSubscriber라고도 한다.)

Subject

  • Observe 되는 객체

Subject는 기본적으로 3개의 기능을 갖는다. 각각 attach, detach, notify

  • Observe하는 객체들을 등록하는 attach
  • Observe하는 객체를 제거하는 detach
  • Observe하는 객체들에게 상태변화를 알려주는 notify

의 기능을 수행한다.

코드로 보면

class Subject {
    private val observers = mutableListOf<Observer>()
    
    fun move() {
    	println("Subject 이동")
       	notify("Subject가 이동함")
    }
    
    fun stop() {
    	println("Subject 정지")
        notify("Subject가 멈춤")
    }

    fun attach(observer: Observer) {
        observers.add(observer)
    }

    fun detach(observer: Observer) {
        observers.remove(observer)
    }

    fun notify(message: String) {
        observers.forEach { it.update(message) }
    }
}

중요한 점은 Subject의 상태가 변하면 notify()를 호출하여 Observerupdate해준다.

Observer

  • Observe 하는 객체

ObserverSubjectnotify한 내용을 통해 update하는 기능이 필요하다.

코드로 보면

interface Observer {
    fun update(message: String)
}

class ConcreteObserver(private val name: String) : Observer {
    override fun update(message: String) {
        println("$name received message: $message")
    }
}

Subject로 부터 notify된 내용을 토대로 update 메소드를 통해 상태 변화를 자동으로 수행할 수 있다.

참고로 Java에서는 Observer 클래스를 지원했었으나 멀티스레딩 환경에서의 비효율성과 유연성 문제로 Java 9부터 Deprecated 되었다.

발생할 수 있는 문제

그러나 여기서도 동시성에 대한 문제가 발생할 수 있다.
예를 들어 notify가 실행되는 동안 다른 스레드에서 새로운 Observerattach를 통해 등록되면, 이 새로운 Observer는 현재 진행 중인 알림 과정에 포함되지 않는 문제가 발생할 수 있다.

이것을 해결하는 다양한 방법들 중 하나는 lock을 이용하는 것이다.

Lock은 스레드가 공유 자원에 접근할 때, 하나의 스레드만 접근하도록 잠금(lock)을 걸어 데이터 일관성을 보장하는 도구

어느 한 메소드가 실행중이라면 다른 메소드를 실행하지 못하도록 lock을 걸어주는 것이다.

import java.util.concurrent.locks.ReentrantLock

class Subject {
    private val observers = mutableListOf<Observer>()
    private val lock = ReentrantLock()

    fun attach(observer: Observer) {
        lock.lock()
        try {
            observers.add(observer)
        } finally {
            lock.unlock()
        }
    }

    fun detach(observer: Observer) {
        lock.lock()
        try {
            observers.remove(observer)
        } finally {
            lock.unlock()
        }
    }

    fun notify(message: String) {
        val snapshot = lock.withLock { observers.toList() }
        for (observer in snapshot) {
            observer.update(message)
        }
    }
}

interface Observer {
    fun update(message: String)
}

class ConcreteObserver(private val name: String) : Observer {
    override fun update(message: String) {
        println("$name received message: $message")
    }
}

fun main() {
    val subject = Subject()
    val observer1 = ConcreteObserver("Observer 1")
    val observer2 = ConcreteObserver("Observer 2")

    subject.attach(observer1)
    subject.attach(observer2)
    subject.notify("New Update!")
}

위 코드에서는 notify, attach, detach 메소드에서 lock을 사용하여 동시에 Observer가 추가되거나 제거되는 상황을 방지하고 notify 메소드에서는 Observer 목록을 스냅샷으로 복사하여, 알림을 보내는 도중에 목록이 변경되는 문제를 방지한다.

그 외에는 @Synchronized 블록을 사용하는 방법도 있다.

class Subject {
    private val observers = mutableListOf<Observer>()

    @Synchronized
    fun attach(observer: Observer) {
        observers.add(observer)
    }

    @Synchronized
    fun detach(observer: Observer) {
        observers.remove(observer)
    }

    @Synchronized
    fun notify(message: String) {
        // Snapshot을 만들어 동시성 문제 방지
        val snapshot = observers.toList()
        for (observer in snapshot) {
            observer.update(message)
        }
    }
}

Observer Pattern의 장점

1.느슨한 결합

  • Subject와 Observer 간의 의존성을 줄여준다. Subject가 변경되더라도 Observer를 직접 수정할 필요가 없다.

2.확장성

  • 새로운 Observer를 쉽게 추가할 수 있다. 즉, 발행자의 코드를 변경하지 않고도 새 구독자 클래스를 도입할 수 있어 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계할 수 있는 개방 폐쇄 원칙을 준수한다.

Observer Pattern의 단점

1.메모리 누수 가능성

  • Observer 등록 해제(Detach)를 제대로 관리하지 않으면 메모리 누수가 발생할 수 있다.

2.알림 순서 보장 어려움

  • 다수의 Observeer가 있을 경우 알림의 순서를 보장하기 어려워 이에 따른 추가 구현이 필요하다.

참고자료

Singleton

https://medium.com/@ZahraHeydari/singleton-pattern-in-kotlin-b09380c53b14
https://velog.io/@seongwon97/%EC%8B%B1%EA%B8%80%ED%86%A4Singleton-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80
https://tecoble.techcourse.co.kr/post/2020-11-07-singleton/
https://onlyfor-me-blog.tistory.com/441

Adatper

https://jusungpark.tistory.com/22
https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%96%B4%EB%8C%91%ED%84%B0Adaptor-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90
https://refactoring.guru/ko/design-patterns/adapter
https://velog.io/@haero_kim/%EC%9A%B0%EB%A6%AC%EB%8A%94-%EC%9D%B4%EB%AF%B8-%EC%96%B4%EB%8C%91%ED%84%B0-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%95%8C%EA%B3%A0-%EC%9E%88%EB%8B%A4

Observer

https://pjh3749.tistory.com/266
https://ko.wikipedia.org/wiki/%EC%98%B5%EC%84%9C%EB%B2%84_%ED%8C%A8%ED%84%B4
https://velog.io/@hanna2100/%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4-2.-%EC%98%B5%EC%A0%80%EB%B2%84-%ED%8C%A8%ED%84%B4-%EA%B0%9C%EB%85%90%EA%B3%BC-%EC%98%88%EC%A0%9C-observer-pattern
https://refactoring.guru/design-patterns/observer

profile
안드로이드 외길

0개의 댓글