알기쉬운 Singleton Pattern

이번 포스팅에서는 싱글톤 패턴에 대해 알아보도록 하겠습니다.

# Singleton 이란

싱글톤(Singleton)소프트웨어 디자인패턴의 한 종류로, 프로그램 안에서 클래스의 인스턴스가 단 하나만 존재해야 할 때 사용합니다. 예를들어 데이터베이스를 변경할 수 있는 DBHandler 클래스의 인스턴스가 두개 있어서 동시에 데이터베이스에 접근한다면 문제가 생기겠죠.

자바에서는 일반적으로 다음과 같은 방식으로 싱글톤을 구현할 수 있습니다. private를 이용해 외부에서 생성자에 접근하지 못하도록 막고 getInstance를 통해야 인스턴스를 만들 수 있게 합니다. 이 때 static instance를 확인해서 인스턴스가 없으면 객체를 새로 만들고, 있다면 그대로 반환해주는 구조를 가집니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class DBHandler { 
    private static DBHandler instance;
    
    // 생성자 접근 차단
    private DBHandler(){}
    
    public static DBHandler getInstance() { 
        if(instance == null) { 
            instance = new DBHandler();
        } 
        return instance; 
    } 
}

# 코틀린에서 구현하기

그런데 코틀린에서는 object 키워드를 사용해서 다음과 같이 간단하게 싱글톤을 생성할 수 있습니다. 언어차원에서 object 키워드로 생성하는 인스턴스는 초기화시 한번만 실행되며 Thread-safe하다는 것이 보장되기 때문에 싱글톤을 만들기 위해서는 따로 패턴을 만들어서 구현할 필요없이 그냥 object를 사용하면 됩니다.

1
2
object DBHandler {...}
val dbHandler = DBHandler

# 클래스로 구현하기

다만 object를 사용하면 인스턴스를 생성할 때 파라미터를 전달할 수가 없다는 한계가 있습니다. 파라미터를 전달하기 위해서는 결국 다음과 같이 클래스를 구성해서, 자바의 static을 companion object로 구현하도록 해야 합니다.

아래 보이는 코드는 맨 처음에 보여드린 자바코드와 동일한 작동을 하는 코틀린 코드입니다. private를 이용해 외부에서 생성자에 직접 접근하지 못하도록 막고 getInstance를 통해야 인스턴스를 만들수 있게 합니다.

그 후 instance를 확인해서 값이 없으면 새로 만들고, 그렇지 않다면 기존 값을 반환하는 구조를 가지고 있습니다. 이 때 생성자는 context를 전달받을 수 있게 구성하였습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class DBHandler private constructor(context: Context) {
    companion object {
        private var instance: DBHandler? = null

        fun getInstance(context: Context) =
            instance ?: DBHandler(context).also {
                instance = it
            }
    }
}

# Double Checked Locking

앞에서 설명한 구조라면 한 개의 스레드안에서는 싱글톤이 구현됩니다. 하지만 두 개의 스레드가 동시에 인스턴스를 만들려고 접근할 경우를 생각해보겠습니다.

인스턴스를 만들기 직전에 두 스레드가 보는 instance는 모두 null이기 때문에 각 스레드는 모두 인스턴스를 만드는데 성공하게 되어 두 개의 DBHandler 인스턴스가 생성되게 됩니다.

싱글톤 생성시 발생하는 이러한 스레드 동기화 문제를 해결하기 위해 제안된 해결책 중 Double Checked Locking(DCL) 방법이 있는데요, 코틀린에서는 다음과 같이 구현할 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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
                }
            }
    }
}

8번 라인에서 인스턴스를 생성하기 전에 7번 라인에서 synchronized를 써서 스레드가 동시에 경합하지 않도록 막아줍니다. 7번 라인에서는 synchronized를 실행하기 전에 instance의 null을 한 번 더 체크합니다. 이 체크가 없으면 각 스레드에서 getInstance에 접근할 때 인스턴스가 이미 존재하더라도 synchronized에 의해 일단 lock이 걸리게 되어 성능저하가 발생할 수 있습니다. 그래서 우선 널체크를 하고 인스턴스가 없을때만 synchronized 이하 블록을 실행하도록 한 것입니다. 이렇게 두번에 걸쳐 인스턴스를 체크하기 때문에 Double Checked Locking 이라는 이름이 붙었습니다.

이때 instance 변수에는 Volatile 어노테이션을 붙였습니다. 스레드는 메인 메모리와 독립된 스택 메모리 공간을 할당받습니다. 그래서 스레드가 객체를 참조할 때는 메인 메모리를 바로 보는 것이 아니라, 메인 메모리에서 읽어온 내용을 스레드의 스택에 저장한 후 이 스택을 보게 되어 있습니다. 다시말해 인스턴스를 인식하는데 시차가 발생하게 됩니다.

그렇게 되면 메인 메모리에서 null인 instance 객체를 1번 스레드에서 확인하고 인스턴스화 하더라도, 2번 스레드에서 확인한 instance는 아직 null로 보여서 인스턴스를 만드는 일이 발생할 수 있습니다. 이 때 instance에 Volatile 어노테이션을 붙여주면 스레드가 메인메모리에서 직접 instance를 참조하게 되므로 인스턴스 인식 시차에 의해 싱글톤이 깨지는 문제를 회피할 수 있습니다.

JDK 1.4 이하에서는 이 Volatile 기능이 제대로 호환되지 않는 문제가 있었지만 현재 JDK 1.8을 사용하고 있는 안드로이드에서는 사용하는데 문제가 없습니다.

DCL에 대해서는 여러가지 논란이 있습니다만 그래도 이 구조는 현재 구글의 Room 라이브러리나, 코틀린의 lazy 함수에서도 사용되는 코드이기 때문에 안심하고 사용하셔도 될 것 같습니다.

# Bill Pugh Solution

하지만 메인 메모리를 직접 사용하면서 스레드를 잠그는 방식은 처리성능에 영향을 줄 수 있는데요, 자바에는 Volatile + synchronized 조합을 사용하지 않아도 Thread-safe한 싱글톤을 만드는 Bill Pugh Solution이라는 수법이 있습니다.

여기서는 Inner static helper class를 활용합니다. 예를 들어 두 개의 스레드가 순서대로 getInstance를 실행했다고 하겠습니다. 우선 1번 스레드에 의해 실행된 getInstance는 holder 클래스를 통해 싱글톤을 생성합니다. 이 작업중에 2번 스레드가 getInstance를 실행해도 인스턴스는 private static final로 되어있기 때문에 JVM은 1번 스레드의 작업완료를 기다리게 됩니다.

따라서 성능저하를 불러올 수 있는 synchronized를 사용하지 않고도 Thread-safe하게 싱글톤을 생성할 수 있게 됩니다. 또한 getInstance를 실행하기 전까지는 싱글톤이 생성되지 않으므로 자원을 낭비하지 않을 수도 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class DBHandler {

    private DBHandler() {}
    
    private static class Holder{
        private static final DBHandler INSTANCE = new DBHandler();
    }
    
    public static DBHandler getInstance() {
        return Holder.INSTANCE;
    }
}

위 코드는 굳이 코틀린으로 변환한다면 다음과 같은 코드가 될 것입니다. 다만 내부 클래스가 object가 되어 생성자를 사용할 수 없으므로 이럴거면 그냥 처음부터 DBHandlerobject로 인스턴스화 하는것이 나을것 같네요.

1
2
3
4
5
6
7
8
9
class DBHandler private constructor() {
    companion object {
        val instance: DBHandler by lazy { Holder.INSTANCE }
    }

    private object Holder {
        val instance = DBHandler()
    }
}

이렇게 해서 싱글톤 패턴을 구현하는 방법에 대해 알아보았습니다.

Built with Hugo
Theme Stack designed by Jimmy