안드로이드 Room에서 Database 클래스의 Singleton 구현에 조금 의아해 보이는 코드가 있었다. Database class 코드를 다시보면 아래와 같은데,
@Database(entities = [Car::class, Ticket::class], version = 1)
abstract class ParkingDatabase : RoomDatabase(){
abstract fun carDao(): CarDao
abstract fun ticketDao(): TicketDao
companion object {
// For Singleton instantiation
@Volatile private var instance: ParkingDatabase? = null
fun getInstance(context: Context): ParkingDatabase {
return instance ?: synchronized(this) {
instance ?: buildDatabase(context).also { instance = it }
}
}
private fun buildDatabase(context: Context): ParkingDatabase{
return Room.databaseBuilder(context.applicationContext, ParkingDatabase::class.java, "Parking").build()
}
}
}
singleton 구현부분에서 getInstance() 함수만 떼어서 보자.
...
fun getInstance(context: Context): ParkingDatabase {
return instance ?: synchronized(this) {
instance ?: buildDatabase(context).also { instance = it }
}
}
...
코드를 보면, instance의 null 여부를 두 번 체크하고 있다. 생각없이 보면 이상할 수 있는데, 멀티 쓰레드 동기화시 꼭 필요한 부분이며 Double-checked locking 이라고 부르는 디자인 패턴이다. 링크의 위키페이지를 따라가면 관련 설명이 있다.
이렇게 두번 체크하는 이유를 살펴보자. 첫번째 instance의 null체크는 synchronized로 lock을 거는 부하를 줄이기 위함이다. 값이 존재하면 lock 오브젝트를 생성할 필요가 없으니까.
체크를 했는데, synchronized 블럭에 진입해서 한번 더 instance의 null을 체크하는 이유는 다음과 같다. synchronized 블럭에 진입전에 lock이 걸려있으면 풀릴때까지 대기를 하게되고, 그사이 lock을 잡고있는 다른 쓰레드에서 instance를 생성했을 가능성이 있기 때문이다.(위 코드에선 가능성이 아니라 생성했겠지.) 대기가 풀리고 synchronized 블럭에 진입했을 때, 이와같은 상황에서 중복생성을 막기 위함이다.
아주 간단하지만, 보통 구현시 첫번째 체크없이 바로 synchronized 블럭만 돌리는 경우도 많고, 멀티쓰레드 상황을 의식하지 않으면 더블체크가 이상하게 보일 수도 있다. 하지만, 알고나면 이게 최적의 코드라는 것. 그러니까 디자인 패턴이겠지.