[Android] 데이터 λ‘œλ”© μ „λž΅

πŸ’‘μš°μ•„ν•œν…Œν¬μ½”μŠ€μ—μ„œ ν•™μŠ΅ν•œ λ‹€μ–‘ν•œ λ‘œλ”© μ „λž΅μ— λŒ€ν•˜μ—¬ νŒŒμ•…ν•˜κ³ , μž₯단점을 λΆ„μ„ν•˜μ—¬ μ •λ¦¬ν•˜μ˜€μŠ΅λ‹ˆλ‹€!

 

데이터 λ‘œλ”© μ „λž΅

  • μ•ˆλ“œλ‘œμ΄λ“œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ 데이터 λ‘œλ”© μ „λž΅μ€ μ‚¬μš©μžμ˜ κ²½ν—˜μ„ κ°œμ„ ν•˜κ³  μ„±λŠ₯을 μ΅œμ ν™”ν•˜κΈ° μœ„ν•΄ μ€‘μš”ν•˜κ²Œ ν™œμš©λ  수 μžˆμŠ΅λ‹ˆλ‹€.
  • 데이터λ₯Ό 효율적으둜 λ‘œλ“œν•˜κ³  κ΄€λ¦¬ν•˜λŠ” μ „λž΅μ€ λ„€νŠΈμ›Œν¬ μš”μ²­, λ°μ΄ν„°λ² μ΄μŠ€ μ ‘κ·Ό, UI λ Œλ”λ§κ³Ό κ΄€λ ¨λœ μž‘μ—…μ—μ„œ 큰 차이λ₯Ό λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€.

Googleλ₯Ό μœ„ν•œ νŽ˜μ΄μ§€λ‘œ λ‚˜λˆ„κΈ° ꢌμž₯사항 | Google 검색 μ„Όν„°  |  λ¬Έμ„œ  |  Google for Developers

 

Googleλ₯Ό μœ„ν•œ νŽ˜μ΄μ§€λ‘œ λ‚˜λˆ„κΈ° ꢌμž₯사항 | Google 검색 μ„Όν„°  |  λ¬Έμ„œ  |  Google for Developers

νŽ˜μ΄μ§€λ‘œ λ‚˜λˆ„κΈ°μ™€ 점진적 νŽ˜μ΄μ§€ λ‘œλ“œ μ‚¬μš© μ‹œ μ „μžμƒκ±°λž˜ μ‚¬μ΄νŠΈμ˜ 색인을 μƒμ„±ν•˜λŠ” 방법에 κ΄€ν•œ ꢌμž₯사항과 μ΄λŸ¬ν•œ μš”μ†Œκ°€ Google 검색에 μ–΄λ– ν•œ 영ν–₯을 쀄 수 μžˆλŠ”μ§€ μ•Œμ•„λ³΄μ„Έμš”.

developers.google.com

μ ν•©ν•œ UX νŒ¨ν„΄ 선택

  • λŒ€κ·œλͺ¨ λͺ©λ‘μ˜ ν•˜μœ„ 집합을 ν‘œμ‹œν•˜κΈ° μœ„ν•΄ λ‹€μ–‘ν•œ UX νŒ¨ν„΄ 쀑에 선택할 수 μžˆμŠ΅λ‹ˆλ‹€.
  • 점진적 데이터 λ‘œλ“œλ‘œ λ„€νŠΈμ›Œν¬ νŠΈλž˜ν”½ κ°μ†Œμ™€ μ΄ˆκΈ°μ— λΉ λ₯Έ λ‘œλ”© 속도λ₯Ό 이점으둜 얻을 수 μžˆμŠ΅λ‹ˆλ‹€.

Pagination

  • μ‚¬μš©μžκ°€ λ‹€μŒ, 이전 νŽ˜μ΄μ§€ 번호 λ“±μ˜ 링크λ₯Ό μ‚¬μš©ν•˜μ—¬ νŽ˜μ΄μ§€ κ°„ μ΄λ™ν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.
  • κ΅¬ν˜„μ΄ μ‰½μ§€λ§Œ μ œμ–΄ 방식이 λ‹€μ†Œ λ³΅μž‘ν•  수 있으며, μ½˜ν…μΈ κ°€ μ—¬λŸ¬ νŽ˜μ΄μ§€κ°€ 걸쳐 λ‚˜λˆ μ§€κ²Œ λ©λ‹ˆλ‹€.

Load More

  • μ‚¬μš©μžκ°€ λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ μ²˜μŒμ— ν‘œμ‹œλœ 검색결과 집합을 펼칠 수 μžˆμŠ΅λ‹ˆλ‹€.
  • 단일 νŽ˜μ΄μ§€μ— λͺ¨λ“  μ½˜ν…μΈ κ°€ ν¬ν•¨λ˜μ§€λ§Œ, 검색 κ²°κ³Όκ°€ λ§Žμ€ 경우 μ²˜λ¦¬ν•˜κΈ° μ–΄λ €μšΈ 수 μžˆμŠ΅λ‹ˆλ‹€.

Infinite Scroll

  • μ‚¬μš©μžκ°€ νŽ˜μ΄μ§€μ˜ λκΉŒμ§€ μŠ€ν¬λ‘€ν•˜λ©΄ 더 λ§Žμ€ 컨텐츠가 λ‘œλ“œλ©λ‹ˆλ‹€.
  • 직관적이고 μ‚¬μš©μž μΉœν™”μ μ΄μ§€λ§Œ, κ΅¬ν˜„μ΄ μ–΄λ ΅λ‹€λŠ” 단점이 μžˆμŠ΅λ‹ˆλ‹€.

μ•ˆλ“œλ‘œμ΄λ“œ 데이터 λ‘œλ”© μ „λž΅

  • μ•ˆλ“œλ‘œμ΄λ“œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œλŠ” μ‚¬μš©μžμ˜ κ²½ν—˜μ„ κ°œμ„ ν•˜κ³  μ„±λŠ₯을 μ΅œμ ν™”ν•˜κΈ° μœ„ν•΄ 데이터 λ‘œλ”© μ „λž΅μ΄ μ€‘μš”ν•©λ‹ˆλ‹€.
  • λ„€νŠΈμ›Œν¬ μš”μ²­, λ°μ΄ν„°λ² μ΄μŠ€ μ ‘κ·Ό, UI λ Œλ”λ§κ³Ό κ΄€λ ¨λœ μž‘μ—…μ—μ„œ 큰 차이가 λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.

Lazy Loading

  • 지연 λ‘œλ”© λ°©μ‹μœΌλ‘œ ν•„μš”ν•œ μ‹œμ κΉŒμ§€ 데이터λ₯Ό λ‘œλ“œν•˜μ§€ μ•Šκ³ , ν•„μš”ν•  λ•Œ λ‘œλ“œν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.
  • ViewPagerλ‚˜ RecyclerView처럼 λ§Žμ€ 데이터λ₯Ό ν•œ λ²ˆμ— 닀루지 μ•Šκ³  μΌλΆ€λ§Œ ν‘œμ‹œν•˜λŠ” κ²½μš°μ— μœ μš©ν•©λ‹ˆλ‹€.
  • μ•±μ˜ 초기 λ‘œλ”© μ‹œκ°„μ€ λ‹¨μΆ•ν•˜κ³  λ©”λͺ¨λ¦¬ μ‚¬μš©λŸ‰μ„ μ΅œμ ν™”ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • [RecyclerViewμ—μ„œ 지연 λ‘œλ”©]
    • 초기 λ‘œλ”© μ‹œκ°„μ„ 쀄이고, 적은 λ©”λͺ¨λ¦¬ μ‚¬μš©κ³Ό λ„€νŠΈμ›Œν¬ νŠΈλž˜ν”½μ„ 쀄일 수 μžˆμŠ΅λ‹ˆλ‹€.
class EndlessScrollListener(
    val loadMore: () -> Unit
) : RecyclerView.OnScrollListener() {

    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)

        val layoutManager = recyclerView.layoutManager as LinearLayoutManager
        val totalItemCount = layoutManager.itemCount
        val lastVisibleItem = layoutManager.findLastVisibleItemPosition()

        // 슀크둀이 끝에 λ„λ‹¬ν–ˆμ„ λ•Œ 데이터λ₯Ό 더 λ‘œλ“œ
        if (lastVisibleItem + VISIBLE_THRESHOLD >= totalItemCount) {
            loadMore()
        }
    }

    companion object {
        const val VISIBLE_THRESHOLD = 5
    }
}

Pagination

  • Pagination은 데이터λ₯Ό μ—¬λŸ¬ νŽ˜μ΄μ§€λ‘œ λ‚˜λˆ„μ–΄ ν•œ λ²ˆμ— μΌλΆ€λ§Œ λ‘œλ“œν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.
  • λ„€νŠΈμ›Œν¬ μš”μ²­μ΄λ‚˜ λ°μ΄ν„°λ² μ΄μŠ€ 쿼리λ₯Ό μ΅œμ ν™”ν•  λ•Œ μœ μš©ν•˜λ©°, λŒ€λŸ‰μ˜ 데이터λ₯Ό λ‹€λ£° λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€.
  • [Retrofit + Pagination]
    • 적은 λ©”λͺ¨λ¦¬λ‘œ 큰 데이터λ₯Ό λ‚˜λˆ„μ–΄μ„œ λ‘œλ“œν•  수 μžˆμŠ΅λ‹ˆλ‹€.
interface ApiService {
    @GET("/items")
    suspend fun getItems(
        @Query("page") page: Int,
        @Query("limit") limit: Int
    ): List<Item>
}

class Repository(private val apiService: ApiService) {
    suspend fun loadItems(page: Int, limit: Int): List<Item> {
        return apiService.getItems(page, limit)
    }
}

Caching

  • 캐싱은 이전에 λ‘œλ“œλœ 데이터λ₯Ό μ €μž₯ν•˜μ—¬ μž¬μ‚¬μš©ν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.
  • λ„€νŠΈμ›Œν¬ μš”μ²­μ„ 쀄이고 μ˜€ν”„λΌμΈμ—μ„œλ„ 데이터λ₯Ό μ‚¬μš©ν•  수 μžˆλ„λ‘ ν•˜κΈ° μœ„ν•΄ μ‚¬μš©λ©λ‹ˆλ‹€.
  • [Room + Caching]
    • μ˜€ν”„λΌμΈ μƒνƒœμ—μ„œ 데이터λ₯Ό μ‚¬μš©ν•  수 μžˆμ§€λ§Œ, λ°μ΄ν„°μ˜ μ΅œμ‹  μƒνƒœλ₯Ό 보μž₯ν•˜μ§€ μ•Šμ„ 수 μžˆμŠ΅λ‹ˆλ‹€.
Dao
interface ItemDao {
    @Query("SELECT * FROM items")
    fun getAllItems(): LiveData<List<Item>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertItems(items: List<Item>)
}

class Repository(
    private val apiService: ApiService,
    private val itemDao: ItemDao
) {
    // λ„€νŠΈμ›Œν¬μ—μ„œ 데이터 λ‘œλ“œ ν›„ DB에 μ €μž₯
    suspend fun fetchItems() {
        val items = apiService.getItems()
        itemDao.insertItems(items)
    }

    // μΊμ‹œλœ 데이터 μ‚¬μš©
    fun getCachedItems(): LiveData<List<Item>> {
        return itemDao.getAllItems()
    }
}

Preloading

  • PreloadingλŠ” 미리 데이터λ₯Ό λ‘œλ“œν•΄λ‘λŠ” λ°©μ‹μœΌλ‘œ, μ‚¬μš©μžκ°€ 데이터λ₯Ό μš”μ²­ν•˜κΈ° 전에 미리 μ€€λΉ„ν•©λ‹ˆλ‹€.
  • μ‚¬μš©μžκ°€ νŠΉμ • 화면에 μ ‘κ·Όν•  것을 μ˜ˆμƒν•˜κ³  데이터λ₯Ό λ‘œλ“œν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.
  • [ViewModel + Preloading]
    • μ‚¬μš©μž κ²½ν—˜μ„ κ°œμ„ ν•  수 μžˆμ§€λ§Œ μ‚¬μš©μžκ°€ 데이터λ₯Ό μš”μ²­ν•˜μ§€ μ•Šμ„ μˆ˜λ„ μžˆλŠ” μƒν™©μ—μ„œλŠ” λΆˆν•„μš”ν•œ λ¦¬μ†ŒμŠ€κ°€ λ©λ‹ˆλ‹€.
class MyViewModel(private val repository: Repository) : ViewModel() {
    val items: LiveData<List<Item>> = liveData {
        val data = repository.loadItems()
        emit(data)
    }

    fun preloadData() {
        viewModelScope.launch {
            repository.loadItems()
        }
    }
}

Offline-First

  • μ˜€ν”„λΌμΈ μƒνƒœμ—μ„œλ„ 데이터λ₯Ό μ‚¬μš©ν•  수 μžˆλ„λ‘ 둜컬 λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό λ¨Όμ € μ°Έμ‘°ν•œ ν›„, ν•„μš” μ‹œ λ„€νŠΈμ›Œν¬ μš”μ²­μ„ 톡해 데이터λ₯Ό μ—…λ°μ΄νŠΈν•˜λŠ” μ „λž΅μž…λ‹ˆλ‹€.
  • [Room + Retrofit]
    • μ˜€ν”„λΌμΈ μƒνƒœμ—μ„œλ„ 데이터λ₯Ό μ œκ³΅ν•˜μ§€λ§Œ, μΊμ‹œλœ 데이터가 였래되면 μ΅œμ‹  μƒνƒœλ₯Ό 보μž₯ν•˜μ§€ λͺ»ν•©λ‹ˆλ‹€.
class Repository(
    private val apiService: ApiService,
    private val itemDao: ItemDao
) {
    // μ˜€ν”„λΌμΈ 데이터λ₯Ό μš°μ„  μ œκ³΅ν•˜κ³ , 이후 λ„€νŠΈμ›Œν¬μ—μ„œ μ—…λ°μ΄νŠΈ
    fun getItems(): LiveData<List<Item>> {
        val data = MediatorLiveData<List<Item>>()

        val cachedItems = itemDao.getAllItems()
        data.addSource(cachedItems) { data.value = it }

        viewModelScope.launch {
            val remoteItems = apiService.getItems()
            itemDao.insertItems(remoteItems)
            data.postValue(remoteItems)
        }

        return data
    }
}

정리

  • μ•ˆλ“œλ‘œμ΄λ“œμ—μ„œ 데이터 λ‘œλ”© μ „λž΅μ€ μ‚¬μš©μžμ˜ κ²½ν—˜κ³Ό μ„±λŠ₯ μ΅œμ ν™”μ— 맀우 μ€‘μš”ν•œ μš”μ†Œμž…λ‹ˆλ‹€.
  • 주어진 상황에 맞게 λ‹€μ–‘ν•œ μ „λž΅μ„ μ‚¬μš©ν•  수 있으며, 데이터 λ‘œλ”© 지연을 μ΅œμ†Œν™”ν•˜κ³  λ©”λͺ¨λ¦¬μ™€ 배터리 μ‚¬μš©λŸ‰μ„ μ΅œμ ν™”ν•  수 μžˆμŠ΅λ‹ˆλ‹€.