본문 바로가기
Android Project/지키미

[Android/Kotlin] Room DB를 사용해 대피소 검색기능 구현하기

by juwon2 2025. 6. 7.

Room이란??

Room에 대해서 자세한 개념과 설명은 아래 포스팅에 따로 작성해놨다!

[Room의 3요소와 역할은 뭘까?]

 

🤔 고민해본 사항

검색기능 구현할때 고민해봤던 사항

  1. 전체 대피시설 정보를 미리 가지고 있어야함
  2. 그래서 검색 API를 사용하고싶었지만, 내가 사용하는 API는 실시간 검색하는 기능을 제공하지 않았다.
    (심지어 얘로 검색이 된다고해도 실시간으로 결과를 출력해야되고, 매번 서버로 요청을 보내야하기 때문에 속도나 서버부하 측면에서 안좋을 수 있는문제가 있음)

    2. 그럼 어떡해??

  • 데이터를 로컬 데이터베이스(Room)에 저장한 후에 검색기능을 구현하기로 결정! 했다
    (어느방식이 더 적합한지 앱의 요구사항에 따라 결정하면 된다.)

 

💻 구현하기

1. Room을 사용해 데이터 관리

 

Entity 정의

@Entity
data class ShelterEntity(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val vtAcmdfcltyNm: String,
    val address: String?,
    val detailAddress: String?,
    val latitude: Double,
    val longitude: Double,
    val shelterType: String,
    val vtAcmdPsblNmpr: String,
    val acmdfcltyDtlCn: String,
    val mngpsTelno: String,
    val lastUpdated: Long = System.currentTimeMillis(),
)

 

  • ShelterEntity로 데이터베이스의 테이블을 생성!
  • vtAcmdfcltyNm(대피소 이름), address(주소), latitude(위도), longitude(경도), shelterType(대피소 유형) 등을 저장

 

DAO 정의

@Dao
interface ShelterDao {
    @Query("SELECT DISTINCT * FROM ShelterEntity WHERE vtAcmdfcltyNm LIKE '%' || :query || '%'")
    fun searchShelters(query: String): Flow<List<ShelterEntity>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertShelters(shelters: List<ShelterEntity>)

    @Query("DELETE FROM ShelterEntity WHERE shelterType = :shelterType")
    suspend fun deleteSheltersByType(shelterType: String)
}

 

  • searchShelters : 검색어(query)를 포함한 대피소데이터(ShelterEntity)를 실시간으로 반환
@Query("SELECT DISTINCT * FROM ShelterEntity WHERE vtAcmdfcltyNm LIKE '%' || :query || '%'")

위의 쿼리를 하나하나 뜯어보자

SELECT DISTINCT

  • ShelterEntity 테이블에서 같은 이름을 가진 대피소가 중복 저장된 경우, 한 번만 반환

FROM ShelterEntity

  • Room에서 정의된 테이블(엔티티)
  • 대피소 데이터를 저장한 테이블로, 검색 대상이됨

where 조건  vtAcmdfcltyNm LIKE '%' || :query || '%'

  • :query검색어
  • '%...%' 패턴은 특정 문자열이 필드 내 어디에 위치하든 검색되도록 함 (ex- "학교" 라는 단어가 앞, 중간, 끝에 위치해도 검색 결과에 포함됨)
  • || : SQL에서 문자열을 연결하는 연산자 (ex- '% || '학교' || %'는 '%학교%'로 변환)

전체 SQL 동작 흐름

  1. ShelterEntity 테이블에서 모든 데이터를 조회.
  2. WHERE 조건으로 대피소 이름(vtAcmdfcltyNm)에 검색어(:query)가 포함된 레코드만 필터링.
  3. SELECT DISTINCT로 중복 제거 후 결과 반환

 

  • insertShelters : 대피소데이터(ShelterEntity)를 삽입
  • deleteSheltersByType : 특정유형의 대피소데이터(ShelterEntity)를 삭제

 

Database 생성

@Database(entities = [LikeEntity::class,ShelterEntity::class,], version = 3)

abstract class AppDatabase : RoomDatabase(){
    abstract fun shelterDao() : ShelterDao
}
  • Database 클래스에서 ShelterDao를 포함한 데이터베이스 인스턴스를 생성

 

2. 검색기능 로직 및 UI구현

 

검색로직 구현

private fun searchShelters(query: String) {
    lifecycleScope.launch {
        shelterDao.searchShelters(query).collect { shelters ->
            updateSearchResults(shelters)
        }
    }
}

 

  • Flow를 사용한 비동기처리
    • searchShelters 메서드에서 DAO의 searchShelters 메서드를 호출해 데이터를 비동기로 가져옴
    • Room에서 반환된 Flow<List<ShelterEntity>>collect해서 UI를 업데이트함
private fun updateSearchResults(shelters: List<ShelterEntity>) {
    if (shelters.isEmpty()) {
        binding.noResultsTv.text = "검색 결과가 없습니다"
        binding.noResultsTv.visibility = View.VISIBLE
    } else {
        binding.noResultsTv.visibility = View.GONE
        searchAdapter.submitList(shelters)
    }
}

 

  • 검색 결과가 없을 경우 안내 메시지를 표시하고, 결과가 있으면 searchAdapter에 검색 결과를 전달해 RecyclerView를 업데이트

 

 

UI 이벤트 처리

binding.searchEt.addTextChangedListener(object : TextWatcher {
    override fun afterTextChanged(s: Editable?) {
        val query = s?.toString()?.trim() ?: ""
        if (query.isNotEmpty()) {
            searchShelters(query)
        } else {
            // 검색어가 없을 경우 초기화
            binding.noResultsTv.text = "검색어를 입력하세요"
            searchAdapter.submitList(emptyList())
        }
    }
})

 

-> 사용자가 검색창을 입력하면, addTextChangedListener  afterTextChanged 에서 입력을 감지해, searchShelters 함수를 호출

 

 

여기까지가, Room DB를 사용해 대피소 검색기능 구현하고, 중복된 데이터는 표시하지 않고 한번만 반환하도록 구현한 부분이다!

 

 

3. 검색결과 클릭시

검색결과 클릭시에도 동작을 하도록 추가적으로 구현해주었다.

 

  • 검색 결과 클릭시, 지도에 해당위치를 표시하고 마커를 추가 + 지도로 이동하도록 구현
    • 선택한 대피소의 위도와 경도를 사용해 Maker를 지도에 추가함
    • 지도 카메라를 해당 위치로 이동함
private fun onShelterSearchItemClick(shelter: ShelterEntity) {
    hideSearchUI()
    moveCameraToLocation(shelter.latitude, shelter.longitude, shelter.vtAcmdfcltyNm, shelter.shelterType)

    val markerPosition = LatLng(shelter.latitude, shelter.longitude)
    val marker = Marker().apply {
        position = markerPosition
        map = naverMap
        icon = OverlayImage.fromResource(
            if (shelter.shelterType == "임시주거시설") R.drawable.marker_blue else R.drawable.marker_red
        )
        captionText = shelter.vtAcmdfcltyNm
    }
}

 

 

4.캐싱 및 데이터최적화

 

캐싱

  • 마지막 업데이트 시간과 데이터 개수를 저장해, 불필요한 API호출을 방지
  • 데이터가 최신상태가 아니거나 충분하지 않으면 API를 호출해 데이터갱신
val needsUpdate = currentTime - lastUpdate > UPDATE_INTERVAL || dbCount < totalCachedCount * 0.9
if (!needsUpdate && dbCount > 0) {
    return convertDbToApiFormat(shelterDao.getSheltersByType("야외대피장소").first())
}

 

 

RoomDB 업데이트

  • 데이터 재호출시, 기존데이터를 삭제하고 새로운 데이터를 삽입
shelterDao.deleteSheltersByType("야외대피장소")
val entities = shelters.mapNotNull { shelter -> ... }
shelterDao.insertShelters(entities)

 

 

⭐️ 결과

 

 

💻 트러블 슈팅

→ 검색 기능구현할때 RoomDB데이터가 바껴서 앱을 삭제하고 다시 실행했음

→ 근데 이렇게 앱자체를 삭제하면, 기존 데이터가 모두 삭제되는 문제가 발생함

⇒ 실제 앱 배포해서 사용자가 있는경우라면, 기존에 앱에 저장되어있는 데이터가 모두 삭제될 수 있는 아주아주 큰 문제가 발생한다.

 실제 앱 배포시에는 절대 앱자체를 삭제하지 말고, Migration으로 구현해서 앱 데이터를 안전하게 관리해야한다!!!

(RoomDB 데이터가 변경될 경우 버전을 올리고, Migration객체를 생성한후, Room빌더에 Migration을 추가하는 방식으로 구현해야한다!!)

 

💡마치며..

RoomDB를 사용해, 검색기능은 처음 구현해본것같은데 재밌었다.

그리고 확실히 고민해보면서 내앱에 맞는 기술을 사용하는것이 중요함을 느꼈다

 

📚 참고자료

[참고자료]