다중 인스턴스 환경에선 Flow가 자동 다운스트림 되지 않던 이슈

SSY·2024년 1월 23일
0

Flow

목록 보기
5/5
post-thumbnail

☘️시작하며

시작하기에 앞서, 싱글턴 디자인 패턴이 무엇인지 먼저 알아야 한다. 싱글턴 패턴은 특정 클래스에 해당하는 정적메모리에 올라가있는 객체이다. 이곳에서 파생된 객체는 해당 클래스로부터 만들어진 모든 객체가 공유할 수 있게된다. 이러한 생성 패턴을 싱글턴 패턴이라 한다.

🦁Room과 Flow를 사용하는 일반적인 패턴

FlowRoom을 함께 사용하면 반응형 데이터 스트림을 사용할 수 있어 매우 편리하다. 특정 비즈니스로직을 시작하기 위해 메서드 호출을 수를 대폭 줄이며, 재활용할 수 있게 되는게 가장 큰 장점이다. 아래는 FlowRoom을 사용하여 반응형 데이터 스트림 의사 코드이다.

class SampleDao {

    @Insert
    fun insertModel(model: Model)

    @Query("Select * From Table")
    fun observeModel(): Flow<List<Model>>
}
class SampleViewModel: ViewModel() {
    val sampleDao = SampleDao()
    
    fun insertModel(model: Model) = 
        sampleDao.insertModel(model = model)
        
    fun observeModel(): Flow<List<Model>> = 
        sampleDao.observeModel()
        
}

fun main() {
    val viewModel = SampleViewModel()
    CoroutineScope(Dispatchers.Main).launcher {
        viewModel.insertModel(model)
    }
    CoroutineScope(Dispatchers.Main).launcher {
        viewModel.observeModel()
            .collect { model ->
                print("result : $model")
            }
    }
}

위처럼 Room 데이터베이스를 사용하여, 첫 번째 코루틴에다 데이터를 insert하고 있다. 그러면 그 밑, 두 번째 코루틴에서 해당 데이터를 곧바로 observing받을 수 있다. 굳이 불필요한 호출코드 없이 말이다.

이제 위의 코드를 실행시키기 위해, Room DB의 객체를 아래와 같은 방식으로 만들었다. 가정하자. 이 방식은 안드로이드 공식 홈페이지에 나와있는 Nomal한 형태이기도 하다.

fun getInstance(applicationContext: Context) =
    Room
        .databaseBuilder(
            context = applicationContext, 
            kclass = SampleDaoList::class.java, 
            name = "SampleDB")
        .build()

🐔 여러개의 Room DB객체가 생성된 환경에서 Flow를 함께 사용할때의 문제

하지만 위와 같은 Room생성 코드와 함께 아래 코드를 실행시킨다면 반드시 문제가 발생한다.

class SampleDao {

    @Insert
    fun insertModel(model: Model)

    @Query("Select * From Table")
    fun observeModel(): Flow<List<Model>>
}

class SampleViewModel: ViewModel() {
    val sampleDao = SampleDB.getInstance(context).sampleDao()

    fun observeModel(): Flow<List<Model>> = 
        sampleDao.observeModel()
        
}
class AnotherSampleViewModel: ViewModel() {
    val sampleDao = SampleDB.getInstance(context).sampleDao()
    
    fun insertModel(model: Model) = 
        sampleDao.insertModel(model = model)
}

fun main() {
    val sampleViewModel = SampleViewModel()
    val anotherSampleViewModel = AnotherSampleViewModel()
    CoroutineScope(Dispatchers.Main).launcher {
        sampleViewModel.insertModel(model)
    }
    CoroutineScope(Dispatchers.Main).launcher {
        anotherSampleViewModel.observeModel()
            .collect { model ->
                print("result : $model")
            }
    }
}

위의 코드가 올바르게 동작할까? 그렇지 않다. 분명 Room DB에 데이터를 insert해줬음에도 불구하고 받지 못하는걸 알 수 있다. 문제는 무엇일까?

🐝 싱글턴으로 만들지 않은 Room DB객체

문제는 Room DB의 객체를 만들 때, 다중 인스턴스로 만들어질 수 있게한 것이 문제이다. 문제가 되었던 코드는 아래와 같다.

class SampleDB private constructor() {
    companion object {
        fun getInstance(applicationContext: Context) =
            Room.databaseBuilder(
                context = applicationContext, 
                kclass = SampleDaoList::class.java, 
                name = "SampleDB")
            .build()
    }
}

@Database(
    entities = [
        Model::class],
    version = 1
)
abstract class SampleDaoList : RoomDatabase() {
    abstract fun sampleDao() : SampleDao
}

필자가 위와 같이 작성했는데, 위의 코드를 실행시키면 메모리에 단일 Room객체가 만들어진다 착각했다. 하지만 그렇지 않다. 이는 Room객체를 생성하는 메서드를 단일한 정적 영역에 정의했을 뿐, 매번 새로운 객체를 생성한다.

따라서 위와 같이 Room객체를 각각 만들고, 하나의 ViewModel에선 데이터의 insert작업을 진행하면, 또 다른 ViewModel에선 당연히 예상했던 데이터 select작업이 되지 않는 것이다. 그럼 어떻게 해줘야 할까?

🦆 싱글턴 패턴 사용 및 Room객체를 1개로 유지

아래와 같은 코드를 작성함으로써 RoomFlow가 항상 동작할 수 있도록 했다. Room을 어떤 객체로부터 호출하든 상관 없이 말이다.

companion object {

    @Volatile
    var INSTANCE: SampleDaoList? = null
    fun getInstance(context: Context): SampleDaoList {
        synchronized(this) {
            var instance = INSTANCE
            if (instance == null) {
                instance = Room.databaseBuilder(
                    context = context.applicationContext,
                    klass = SampleDaoList::class.java,
                    name = "SampleDB")      
                    .build()
                INSTANCE = instance
            }
            return instance
        }
    }
}

기존의 fun getInstance(context: Context)의 인터페이스는 변경하지 않은채로 내부 구현만 변경했다. 이로써 기존 코드엔 전혀 영향이 가지 않을 뿐만 아니라, 어떤 객체로부터 호출하든 반응형 데이터 스트림을 자유자재로 활용할 수 있게 되었다.

또한 INSTANCE라는 프로퍼티를 정의함으로써 인스턴스를 정적 영역에 캐시할 수 있게 만들었다. 만약, 위 메서드를 처음 호출한다면 INSTANCE에 해당 인스턴스가 초기화될 것이다. 그 후, getInstance()를 재호출하면, INSTANCE가 캐시하고 있는 기존 객체를 그대로 사용할 것이다.

🐷마치며

아래 도식도는 기존의 도식도이다.

Room DB의 인스턴스가 위처럼 2개가 생성되니 Flow를 통한 데이터 자동 다운 스트림이 진행되지 않았던 것이다.

반면, 아래는 싱글턴 패턴으로 만든, Room DB객체를 공유한 도식도이다.

SampleViewModelAnotherSampleViewModel모두 동일한 Room DB객체를 공유하고 있다. 그러니 어떤 뷰모델에서든 데이터 insert작업을 하든 상관 없이, 또 다른 곳에서 데이터를 반응형으로 받을 수 있는 것이다.

RoomDB를 사용하여 반응형이 아닌, 명령형 방식으로의 CRUD작업을 수행할 의도라면, 해당 객체를 싱글턴으로 만들 필요가 없다. 하지만, Flow를 사용하고, 어떤 객체에서부터 시작하여 호출하든 상관없이 자동으로 다운스트림을 받고싶다면 반드시 싱글턴으로 생성해야만 한다.

profile
불가능보다 가능함에 몰입할 수 있는 개발자가 되기 위해 노력합니다.

0개의 댓글