Android

안드로이드 [Kotlin] - 안드로이드에서의 싱글톤패턴(Singleton Pattern) with object & DCL

🤖 Play with Android 🤖 2022. 12. 9. 14:16
728x90


싱글톤 패턴이란?

소프트웨어 디자인 패턴에서 싱글턴 패턴(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글턴 패턴이라고 한다. 주로 공통된 객체를 여러 개 생성해서 사용하는 DBCP(DataBase Connection Pool)와 같은 상황에서 많이 사용된다. - 위키피디아

 

 

싱글톤 패턴의 이점

위의 설명과 같이 싱글톤 패턴을 사용하면 이점이 어떤 것이 있을까?

 

메모리 측면

  • 가장 먼저 떠올릴 수 있는 이점은 아무래도 "메모리 측면" 일 것이다.
  • 최초 한 번의 생성을 통해서 고정된 메모리 영역을 사용하기 때문에 추후 해당 객체에 접근할 때 메모리 낭비를 방지할 수 있다. 뿐만 아니라 이미 생성된 인스턴스를 활용하니 속도 측면에서도 이점이 있다고 볼 수 있다.

 

데이터 공유 측면

  • 또 다른 이점은 다른 클래스 간에 "데이터 공유가 쉽다"는 것이다.
  • 싱글톤 인스턴스가 전역으로 사용되는 인스턴스이기 때문에 다른 클래스의 인스턴스들이 접근하여 사용할 수 있다.

 

싱글톤 패턴의 문제점

말만 들었을 때 싱글톤 패턴을 사용하지 않을 이유가 없을 것 같다. 그렇다면 싱글톤 패턴의 단점은 무엇일까?

 

여러 개의 인스턴스 생성

잠시 자바에서 싱글톤 패턴을 구현한 대표적인 코드를 보자

public class Singleton {
    // 단 1개만 존재해야 하는 객체의 인스턴스로 static 으로 선언
    private static Singleton instance;

    // private 생성자로 외부에서 객체 생성을 막아야 한다.
    private Singleton() {
    }

    // 외부에서는 getInstance() 로 instance 를 반환
    public static Singleton getInstance() {
        // instance 가 null 일 때만 생성
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  • 멀티스레드 환경에서 인스턴스가 없을 때 "동시에" 아래의 getInstance() 메서드를 실행하는 경우 각각 새로운 인스턴스를 생성할 수 있다.
  • 즉 멀티스레드 환경에서의 동기화에 문제가 생기는 것이다.

 

 


 

코드를 통한 실습

코드를 통해 싱글톤 패턴에 대해 더 알아보자

 

자바

우선 위에서 자바로 싱글톤 패턴을 구현하기 위해 다음과 같은 코드를 작성했다.

public class Singleton {
    // 단 1개만 존재해야 하는 객체의 인스턴스로 static 으로 선언
    private static Singleton instance;

    // private 생성자로 외부에서 객체 생성을 막아야 한다.
    private Singleton() {
    }

    // 외부에서는 getInstance() 로 instance 를 반환
    public static Singleton getInstance() {
        // instance 가 null 일 때만 생성
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

 

코틀린

코틀린에서는 object 키워드로 간단하게 구현 가능하다.

object Singleton {}

 

  • 언어 차원에서 object 키워드로 생성하는 인스턴스는 초기화 시 한 번만 실행되며 "Thread-safe 하다는 것이 보장" 되기 때문에 싱글톤을 만들기 위해서는 따로 패턴을 만들어서 구현할 필요 없이 그냥 object를 사용하면 된다.
  • 위 코틀린 코드는 저 위의 자바 11줄의 코드와 완벽히 동일한 기능을 하며 오히려 "Thread-safe 하다는 것이 보장" 한다는 장점이 있다.

 

하지만 이러한 object 키워드에도 크게 두 가지 단점이 존재한다.

 

object 키워드의 단점

프로세스 시작 시 인스턴스 생성

  • 위의 자바 코드는 Singleton 클래스의 getInstance() 메서드가 호출될 때 초기화되어 메모리 상에 올라간다.
  • 하지만 코틀린의 object로 구현한 Singleton은 프로세스가 메모리 상에 올라갈 때 곧바로 생성되어 올라간다. 이는 클래스가 사용되지 않을 때도 메모리 상에 인스턴스가 올라가 있다는 것을 의미한다.

 

인스턴스를 생성할 때 파라미터를 전달할 수 없음

  • object 키워드를 사용하면 인스턴스를 생성할 때 파라미터를 전달할 수 없다는 한계가 존재한다.
  • 파라미터를 전달하기 위해서는 결국 자바와 같이 클래스를 구현하고 자바의 static을 companion object를 통해 구현해야 한다.
  • 이를 코드로 만들면 다음과 같다.
class DBHandler private constructor(context: Context) {
    companion object {
        private var instance: DBHandler? = null

        fun getInstance(context: Context) =
            instance ?: DBHandler(context).also {
                instance = it
            }
    }
}
  • 자바의 싱글톤 패턴 코드와 매우 유사한 것이 보인다.
  • private를 이용해 외부에서 생성자에 직접 접근하지 못하도록 막고 getInstance를 통해야 인스턴스를 만들 수 있게 한다.
  • 그 후 엘비스 연산자와 스코프 함수를 이용해 instance를 확인해서 값이 null이라면 새로 만들고, 그렇지 않다면 기존 값을 반환하는 구조를 가지고 있다.

 

 

파라미터를 전달하는 싱글톤 패턴의 문제점 해결

  • 만약 파라미터를 전달하는 경우가 아니라면 object는 코틀린 언어 차원에서 Thread-safe 하다는 것이 보장되기 때문에 그대로 사용하면 된다.
  • 문제는 object 키워드는 파라미터를 전달할 수 없기 때문에 파라미터를 전달해야 하는 경우라면 멀티스레드 환경에서의 동기화에 문제가 발생할 수 있다.
  • 싱글톤 생성 시 발생하는 이러한 스레드 동기화 문제를 해결하기 위해 제안된 해결책 중 Double Checked Locking(DCL) 방법이 있는데, 코틀린에서는 다음과 같이 구현할 수 있다.
class DBHandler private constructor(context: Context) {
    companion object {
        @Volatile
        private var instance: DBHandler? = null

        fun getInstance(context: Context) =
            instance ?: synchronized(DBHandler::class.java) {
                instance ?: DBHandler(context).also {
                    instance = it
                }
            }
    }
}
  • 인스턴스를 생성하기 전에 synchronized 키워드를 써서 스레드가 동시에 경합하지 않도록 막아준다.
  • 또한 synchronized를 실행하기 전에 instance의 null을 한 번 더 체크한다. 이 체크가 없으면 각 스레드에서 getInstance에 접근할 때 인스턴스가 이미 존재하더라도 synchronized에 의해 일단 lock이 걸리게 되어 성능 저하가 발생할 수 있기 때문이다.
  • 그래서 우선 엘비스 연산자를 통해 널 체크를 하고 인스턴스가 없을 때만 synchronized 이하 블록을 실행하도록 한 것이다.

이렇게 두 번에 걸쳐 인스턴스를 체크하기 때문에 Double Checked Locking이라는 이름이 붙게 되었다고 한다.

 

 

 

 

synchronized 키워드

자바에서 지원하는 synchronized 키워드는 여러개의 스레드가 한개의 자원을 사용하고자  , 현재 데이터를 사용하고 있는 해당 스레드를 제외하고 나머지 스레드들은 데이터에 접근   없도록 막는 개념

 

예시) A스레드와 B스레드가 공유데이터에 접근하는 상황
  1. 공유데이터에 대한 접근과 수정이 이루어지는 메서드에 synchronized 키워드를 리턴타입 앞에 붙여준다.
  2. A스레드가 먼저 공유데이터나 메서드에 점유하고 있는 상태인 경우 block으로 처리하기 때문에 A 이외의 스레드의 접근을 막는다.
  3. A스레드가 작업을 다 끝냈다면 .unblock으로 처리하여 A 이외의 스레드의 접근을 허락

 

synchronized는 총 세가지로 분류할 수 있다.

  • Instance methods : 인스턴스 메서드에 synchronized 선언
  • Static methods : static method에 synchronized 선언
    • java의 static 키워드의 특징상 JVM 힙에 저장된다.
  • Code blocks : 메서드 전체에 synchronized를 적용하지 않고 싶을때 사용할 수 있다.

 

@Volatile

  • 사전적 의미는 “휘발성”이라는 뜻
  • volatile 변수를 사용하지 않는 일반적인 경우는 내부적으로 성능 향상을 위해 메인 메모리로부터 읽어온 값을 CPU 캐시에 저장한다. 
  • "변수 선언 시 volatile을 지정하면 값을 메인 메모리에만 적재" 하게 된다.
  • 즉 변수에 Volatile 어노테이션을 붙여주면 스레드가 메인 메모리에서 직접 변수를 참조하게 되므로 인스턴스 인식 시차에 의해 싱글톤이 깨지는 문제를 회피할 수 있다.

 

이렇게 @Volatile을 붙이면 변수의 값이 메인 메모리에만 저장되며, 멀티 스레드 환경에서 메인 메모리의 값을 참조하므로 변수 값 불일치 문제를 해결할 수 있게 된다. 다만 CPU 캐시를 참조하는 것보다 메인 메모리를 참조하는 것이 더 느리므로, 성능은 떨어질 수밖에 없다