Android 장소 검색 완벽 가이드 — Google Places SDK, 자동완성, 장소 상세 정보와 API 비용 최적화

Android 지도 앱을 개발하다 보면 사용자가 원하는 장소를 직접 검색할 수 있도록 만들어야 하는 경우가 있습니다.

예를 들어 관광지 안내도 앱에서는 특정 관광지를 검색할 수 있어야 하고, 조류 관찰 앱에서는 공원이나 습지의 위치를 검색해 관찰 기록과 연결할 수 있어야 합니다.

단순히 SQLite에 저장된 장소를 검색하는 것이라면 LIKE나 FTS를 사용할 수 있습니다.

하지만 사용자가 입력한 장소 이름을 실제 지도상의 위치와 연결하려면 별도의 장소 검색 서비스가 필요합니다.

Google Maps Platform에서는 이러한 기능을 위해 Places SDK for Android를 제공합니다.

이번 글에서는 Places SDK를 이용해 장소 자동완성, 장소 상세 정보 조회, 지도 마커 표시를 구현하고 API 호출 비용을 관리하는 방법까지 살펴보겠습니다.

개발 환경: Kotlin, XML ViewBinding, MVVM, Coroutine, Google Maps SDK for Android, Places SDK for Android(New), SQLiteOpenHelper. 예제의 SDK 버전과 과금 조건은 공식 문서를 기준으로 확인해야 합니다.


1. Google Maps SDK와 Places SDK의 차이

두 SDK는 역할이 다릅니다.

구분Google Maps SDKPlaces SDK
주요 목적지도 표시장소 검색 및 정보 조회
지도 마커지원지도 SDK와 연동
장소 자동완성직접 제공하지 않음지원
장소 상세 정보제한적지원
GPS 경로 표시지원해당 없음
장소 검색 결과별도 서비스 필요지원

Google Maps SDK는 지도 화면을 표시하는 데 사용하고, Places SDK는 장소 데이터를 검색하는 데 사용한다고 이해하면 됩니다.

실제 앱에서는 두 SDK를 함께 사용하는 경우가 많습니다.


Places SDK 프로젝트 설정

2. Google Cloud 설정

Places SDK를 사용하려면 Google Cloud Console에서 다음 항목을 준비합니다.

  1. Google Cloud 프로젝트 선택

  2. 결제 계정 연결

  3. Places API (New) 활성화

  4. API 키 생성 또는 기존 키 검토

  5. Android 애플리케이션 제한 설정

  6. 필요한 API만 허용하도록 API 제한 설정

Places SDK와 Maps SDK의 사용량은 각각의 과금 체계에 따라 계산될 수 있습니다.

따라서 지도 표시와 장소 검색을 모두 사용하는 앱이라면 관련 API의 사용량을 함께 확인해야 합니다.


3. Gradle 의존성 추가

app/build.gradle.kts에 Places SDK를 추가합니다.

dependencies {

    implementation(
        "com.google.android.libraries.places:places:<version>"
    )

    implementation(
        "com.google.android.gms:play-services-maps:<version>"
    )
}

<version>은 공식 릴리스에서 확인한 버전으로 변경합니다.

이번 예제에서는 Places SDK의 신규 API를 사용하는 방식을 기준으로 설명합니다.


4. Places SDK 초기화

Application 클래스에서 Places SDK를 초기화할 수 있습니다.

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        if (!Places.isInitialized()) {
            Places.initializeWithNewPlacesApiEnabled(
                applicationContext,
                BuildConfig.MAPS_API_KEY
            )
        }
    }
}

BuildConfig.MAPS_API_KEY는 Gradle에서 주입한 값이라고 가정합니다.

SDK 버전에 따라 초기화 API가 달라질 수 있으므로 사용 중인 버전의 공식 문서를 확인해야 합니다.

API 키를 앱에 포함할 때는 Google Cloud에서 Android 패키지 이름과 서명 인증서 제한을 적용해야 합니다.


장소 검색 기능 설계

5. 장소 검색 흐름

장소 검색 기능은 다음과 같은 흐름으로 구성할 수 있습니다.

사용자 검색어 입력
        │
        ▼
자동완성 요청
        │
        ▼
검색 결과 목록 표시
        │
        ▼
사용자 장소 선택
        │
        ▼
Place ID 확인
        │
        ▼
장소 상세 정보 요청
        │
        ▼
지도 중심 이동
        │
        ▼
마커 및 장소 정보 표시

여기서 중요한 개념이 Place ID입니다.

Place ID는 Google Places에서 장소를 식별하는 데 사용하는 값입니다.

장소의 이름이나 주소는 변경될 수 있으므로 장소 선택 결과를 관리할 때 Place ID를 활용하는 것이 편리합니다.

다만 Place ID도 영구적으로 변경되지 않는다고 가정해서는 안 됩니다.


자동완성 UI

6. XML 검색 화면 구성

검색어를 입력할 EditText와 결과 목록을 표시할 RecyclerView를 준비합니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <EditText
        android:id="@+id/editSearch"
        android:layout_width="match_parent"
        android:layout_height="56dp"
        android:hint="장소 이름을 입력하세요"
        android:inputType="text"
        android:imeOptions="actionSearch"
        android:singleLine="true" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerSuggestions"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

검색 결과를 표시하는 Adapter는 일반적인 RecyclerView + ListAdapter 구조로 구현할 수 있습니다.


자동완성 세션 토큰

7. AutocompleteSessionToken이란?

Places 자동완성에서는 사용자가 검색어를 입력하고 장소를 선택하는 과정을 하나의 세션으로 관리할 수 있습니다.

이때 사용하는 객체가 AutocompleteSessionToken입니다.

private var sessionToken =
    AutocompleteSessionToken.newInstance()

예를 들어 사용자가 다음과 같이 입력했다고 가정하겠습니다.

경
경복
경복궁

각 입력마다 자동완성 요청이 발생할 수 있습니다.

이러한 요청을 동일한 검색 세션으로 연결할 때 세션 토큰을 사용합니다.

세션 토큰은 과금 방식과 관련이 있으므로 요청마다 무조건 새로 생성해서는 안 됩니다.

반대로 서로 독립적인 검색 세션에서 같은 토큰을 계속 재사용하는 것도 적절하지 않습니다.


장소 자동완성 요청

8. FindAutocompletePredictionsRequest

Places SDK에서는 자동완성 요청을 생성할 수 있습니다.

val request =
    FindAutocompletePredictionsRequest.builder()
        .setQuery(query)
        .setSessionToken(sessionToken)
        .build()

query는 사용자가 입력한 검색어입니다.

검색 대상 국가나 위치 범위를 제한하는 옵션도 검토할 수 있습니다.


9. 자동완성 결과 조회

fun searchPlaces(
    placesClient: PlacesClient,
    query: String,
    token: AutocompleteSessionToken,
    onSuccess: (List<AutocompletePrediction>) -> Unit,
    onError: (Exception) -> Unit
) {

    val request =
        FindAutocompletePredictionsRequest.builder()
            .setQuery(query)
            .setSessionToken(token)
            .build()

    placesClient.findAutocompletePredictions(request)
        .addOnSuccessListener { response ->

            onSuccess(
                response.autocompletePredictions
            )
        }
        .addOnFailureListener { exception ->

            onError(exception)
        }
}

이 예제는 Places SDK의 Task 기반 API를 사용합니다.

실제 프로젝트에서는 검색 요청의 생명주기와 이전 요청의 응답 순서를 함께 관리해야 합니다.


자동완성 요청 최적화

10. 입력할 때마다 요청하면?

사용자가 한 글자를 입력할 때마다 바로 API를 호출하면 불필요한 요청이 증가할 수 있습니다.

예를 들어 다음과 같은 입력이 발생한다고 가정하겠습니다.

제
제주
제주도
제주도 돌
제주도 돌문
제주도 돌문화공원

검색어가 변경될 때마다 요청을 전송하면 응답이 도착하는 순서가 뒤바뀌는 문제도 발생할 수 있습니다.

이를 줄이기 위해 Debounce를 적용할 수 있습니다.


11. StateFlow + Debounce

ViewModel에서 검색어를 관리합니다.

class PlaceSearchViewModel(
    private val repository: PlaceSearchRepository
) : ViewModel() {

    private val query =
        MutableStateFlow("")

    fun updateQuery(value: String) {
        query.value = value
    }

    init {

        query
            .debounce(350)
            .map { it.trim() }
            .distinctUntilChanged()
            .filter { it.length >= 2 }
            .onEach { keyword ->

                repository.search(keyword)
            }
            .launchIn(viewModelScope)
    }
}

이 코드는 검색 요청 흐름을 설명하기 위한 기본 예제입니다.

실제 구현에서는 mapLatest 또는 flatMapLatest 등을 이용해 이전 검색 결과가 최신 결과를 덮어쓰지 않도록 설계하는 것이 좋습니다.

또한 검색어가 빈 문자열로 변경되었을 때 기존 결과를 제거하는 로직도 필요합니다.


검색 결과 데이터 모델

12. Prediction 모델 변환

자동완성 결과를 앱 내부 데이터 모델로 변환하면 UI와 SDK 의존성을 분리하기 쉽습니다.

data class PlaceSuggestion(
    val placeId: String,
    val primaryText: String,
    val secondaryText: String
)

예를 들어 다음과 같이 변환할 수 있습니다.

fun AutocompletePrediction.toSuggestion():
    PlaceSuggestion {

    return PlaceSuggestion(
        placeId = placeId,
        primaryText = getPrimaryText(null).toString(),
        secondaryText = getSecondaryText(null).toString()
    )
}

검색 결과 화면에는 장소 이름과 보조 주소 정보를 함께 표시하면 사용자가 원하는 장소를 구분하기 쉽습니다.


장소 선택과 상세 정보 조회

13. FetchPlaceRequest

사용자가 자동완성 결과 중 하나를 선택하면 Place ID를 이용해 장소 상세 정보를 조회할 수 있습니다.

val fields = listOf(
    Place.Field.ID,
    Place.Field.DISPLAY_NAME,
    Place.Field.LOCATION,
    Place.Field.FORMATTED_ADDRESS
)

val request =
    FetchPlaceRequest.builder(
        placeId,
        fields
    )
        .setSessionToken(sessionToken)
        .build()

신규 Places API를 사용하는 경우 DISPLAY_NAME, LOCATION, FORMATTED_ADDRESS 같은 필드를 사용합니다.

필드 이름과 지원 여부는 SDK 버전에 따라 확인해야 합니다.


14. 장소 상세 정보 요청

fun fetchPlaceDetail(
    placesClient: PlacesClient,
    placeId: String,
    token: AutocompleteSessionToken,
    onSuccess: (Place) -> Unit,
    onError: (Exception) -> Unit
) {

    val fields = listOf(
        Place.Field.ID,
        Place.Field.DISPLAY_NAME,
        Place.Field.LOCATION,
        Place.Field.FORMATTED_ADDRESS
    )

    val request =
        FetchPlaceRequest.builder(
            placeId,
            fields
        )
            .setSessionToken(token)
            .build()

    placesClient.fetchPlace(request)
        .addOnSuccessListener { response ->

            onSuccess(response.place)
        }
        .addOnFailureListener { exception ->

            onError(exception)
        }
}

자동완성 세션을 완료한 후에는 다음 검색을 위해 새로운 세션 토큰을 생성해야 합니다.


FieldMask와 과금

15. 필요한 필드만 요청하기

Places API에서 중요한 개념 중 하나가 FieldMask입니다.

장소 상세 정보에서 어떤 필드를 요청하는지에 따라 적용되는 과금 SKU가 달라질 수 있습니다.

예를 들어 지도에 장소 이름과 좌표만 표시하는 앱이라면 전화번호, 웹사이트, 영업시간 등의 정보를 요청할 필요가 없습니다.

따라서 다음과 같이 최소한의 필드부터 요청하는 것이 좋습니다.

val fields = listOf(
    Place.Field.ID,
    Place.Field.DISPLAY_NAME,
    Place.Field.LOCATION
)

다만 DISPLAY_NAME처럼 일반적으로 보이는 필드도 더 높은 과금 등급에 해당할 수 있습니다.

필드를 적게 요청하는 것과 가장 저렴한 SKU가 적용되는 것은 동일한 의미가 아닙니다.

실제 적용 전에는 각 필드가 어느 SKU에 해당하는지 공식 가격표에서 확인해야 합니다.


16. 장소 검색 비용을 줄이는 방법

Places SDK 비용을 관리하려면 다음 사항을 고려해야 합니다.

  • 불필요한 자동완성 요청 줄이기

  • 검색어 Debounce 적용

  • 자동완성 세션 토큰 올바르게 관리

  • 장소 상세 정보 요청 필드 최소화

  • 사용하지 않는 API 비활성화

  • Google Cloud 예산 알림 설정

  • Places API 사용량 모니터링

  • 무료 사용 범위와 과금 SKU 확인

단, 세션 토큰을 사용한다고 모든 자동완성 요청이 무료가 되는 것은 아닙니다.

세션 종료 방식과 요청한 API에 따라 과금이 달라질 수 있습니다.


검색 결과를 지도에 표시

17. Google Maps SDK 연동

장소 상세 정보를 조회했다면 해당 좌표로 지도를 이동할 수 있습니다.

fun showSelectedPlace(
    map: GoogleMap,
    place: Place
) {

    val location = place.location
        ?: return

    map.animateCamera(
        CameraUpdateFactory.newLatLngZoom(
            location,
            16f
        )
    )

    map.addMarker(
        MarkerOptions()
            .position(location)
            .title(place.displayName)
    )
}

실제 프로젝트에서는 기존 선택 마커를 제거하거나 재사용해 마커가 계속 누적되지 않도록 관리하는 것이 좋습니다.


지도 검색과 SQLite 검색의 차이

18. 어떤 검색을 사용해야 할까?

앱에 이미 저장된 장소를 검색하는 경우에는 Google Places SDK가 필요하지 않을 수 있습니다.

예를 들어 관광지 안내도 앱에서 특정 정원에 등록된 장소만 검색한다면 SQLite 검색이 적합합니다.

검색 대상추천 방식
앱 내부에 저장된 관광지 장소SQLite LIKE 또는 FTS
전국의 공원·시설 검색Places SDK
사용자가 등록한 관찰 지점SQLite
지도에서 새로운 장소 찾기Places SDK
오프라인 장소 검색로컬 SQLite 데이터

따라서 모든 검색을 Places SDK로 처리하기보다 데이터 출처에 따라 검색 방식을 구분하는 것이 좋습니다.


Places 검색과 MVVM

19. 아키텍처 구성

PlaceSearchFragment
        │
        ▼
PlaceSearchViewModel
        │
        ▼
PlaceSearchRepository
        │
        ├── PlacesClient
        │
        └── SQLiteOpenHelper

Fragment는 검색어 입력과 결과 목록을 표시합니다.

ViewModel은 검색 상태와 선택된 장소를 관리합니다.

Repository는 Places SDK 요청과 로컬 데이터 조회를 담당합니다.


20. 검색 UI 상태 모델

data class PlaceSearchUiState(
    val query: String = "",
    val suggestions: List<PlaceSuggestion> = emptyList(),
    val isLoading: Boolean = false,
    val selectedPlace: Place? = null,
    val errorMessage: String? = null
)

이 구조를 사용하면 검색 중 로딩 표시, 결과 없음, 오류 메시지, 선택된 장소를 일관되게 관리할 수 있습니다.


검색 오류 처리

21. 자주 발생하는 문제

Places SDK를 사용하는 앱에서는 다음과 같은 오류가 발생할 수 있습니다.

문제확인할 내용
자동완성 결과 없음검색어, 국가 제한, 위치 범위
API 요청 실패API 활성화, 결제 계정
권한 오류API 키 제한, 패키지 이름, SHA-1
지도는 표시되지만 검색 실패Places API 활성화 여부
검색 결과가 늦게 표시됨네트워크, 요청 빈도
이전 검색 결과가 나타남비동기 요청 순서
비용 증가요청 횟수, 세션 처리, 필드
장소 좌표 없음요청 필드 및 응답 확인

API 오류를 처리할 때는 사용자에게 단순히 '오류 발생'만 표시하기보다 재시도 가능 여부와 검색 실패 원인을 구분하는 것이 좋습니다.


개인정보와 검색 기록

22. 장소 검색 데이터 관리

사용자가 검색한 장소 정보는 이용자의 행동이나 이동 목적을 추정할 수 있는 데이터가 될 수 있습니다.

따라서 다음 사항을 검토해야 합니다.

  • 검색 기록 저장 여부

  • 검색 기록 삭제 기능

  • 검색어 로그 수집 여부

  • 위치 권한과 장소 검색의 분리

  • Google Play Data Safety 신고

  • 개인정보처리방침

  • Google Maps Platform 데이터 이용 약관

특히 사용자가 직접 입력한 장소 검색어와 GPS 위치 정보는 서로 다른 데이터입니다.

단순한 장소 이름 검색만을 위해 위치 권한을 요청할 필요는 없습니다.


앱 적용 사례

23. 조류 관찰 앱

조류 관찰 앱에서는 다음과 같은 기능을 구현할 수 있습니다.

  1. 사용자가 관찰 장소 검색

  2. Places 자동완성 결과 표시

  3. 공원 또는 습지 선택

  4. 선택한 장소의 좌표 확인

  5. 관찰 기록과 장소 연결

  6. SQLite에 관찰 기록 저장

  7. 지도에서 관찰 위치 표시

다만 Google Places에서 제공받은 데이터의 저장과 재사용에는 별도 이용 조건이 적용될 수 있습니다.

Place ID와 직접 생성한 사용자 관찰 기록을 구분해서 관리하고, 제공받은 장소 정보의 캐싱·보관 가능 범위를 확인해야 합니다.


24. 관광지 안내도 앱

관광지 안내도 앱에서는 두 가지 검색 기능을 분리할 수 있습니다.

첫 번째는 외부 장소 검색입니다.

사용자가 전국의 관광지나 공원을 검색할 때 Places SDK를 사용합니다.

두 번째는 관광지 내부 장소 검색입니다.

특정 관광지의 화장실, 전망대, 산책로, 주차장 등을 검색할 때 SQLite를 사용합니다.

이렇게 구성하면 외부 장소 검색 기능을 제공하면서도 불필요한 API 호출을 줄일 수 있습니다.


Places SDK 테스트

25. 테스트 시나리오

출시 전에는 다음 항목을 확인하는 것이 좋습니다.

  • Places SDK 초기화

  • API 키 제한 설정

  • 디버그 인증서 등록

  • Google Play 앱 서명 인증서 등록

  • 자동완성 결과 조회

  • 검색어 Debounce

  • 검색어 삭제 시 결과 초기화

  • 오래된 요청 응답 처리

  • 세션 토큰 생성

  • 장소 선택 후 세션 종료

  • 새 검색 시 토큰 재생성

  • Place ID 조회

  • 필요한 상세 필드만 요청

  • 지도 중심 이동

  • 선택 마커 표시

  • 검색 결과 없음 처리

  • 네트워크 오류 처리

  • API 사용량 확인

  • 과금 SKU 확인

  • 개인정보처리방침 검토

  • 실제 기기 테스트


26. 마무리

Google Places SDK를 활용하면 Android 지도 앱에 장소 자동완성과 장소 상세 정보 기능을 추가할 수 있습니다.

하지만 단순히 검색 결과를 표시하는 것만으로 구현이 끝나는 것은 아닙니다.

자동완성 요청 빈도, 세션 토큰, FieldMask, API 키 제한, 사용량과 비용까지 함께 관리해야 합니다.

이번 글의 핵심을 정리하면 다음과 같습니다.

  1. Google Maps SDK와 Places SDK는 역할이 다릅니다.

  2. Places SDK로 장소 자동완성 기능을 구현할 수 있습니다.

  3. 검색 세션에는 AutocompleteSessionToken을 활용합니다.

  4. Debounce로 불필요한 요청을 줄일 수 있습니다.

  5. Place ID를 이용해 장소 상세 정보를 조회할 수 있습니다.

  6. 필요한 필드만 요청해 API 사용량과 과금 구조를 관리해야 합니다.

  7. 검색 결과의 좌표를 Google Maps SDK에 표시할 수 있습니다.

  8. 앱 내부 장소 검색에는 SQLite가 더 적합할 수 있습니다.

  9. API 키에는 Android 앱 및 API 제한을 적용해야 합니다.

  10. 출시 전에는 Google Maps Platform의 가격과 데이터 이용 약관을 확인해야 합니다.

조류 관찰 앱이나 관광지 안내도 앱에서는 Places SDK와 SQLite 검색을 적절히 조합하면 효율적인 장소 검색 기능을 구현할 수 있습니다.

장소 검색 기능의 핵심은 검색 결과를 많이 가져오는 것이 아니라, 필요한 장소 정보를 정확하고 효율적으로 제공하는 것입니다.


참고 자료

태그: Android, Kotlin, Places SDK, Google Places API, Google Maps, 장소 검색, 자동완성, AutocompleteSessionToken, Place ID, FieldMask, MVVM, StateFlow, SQLite, API 비용 최적화, Android 지도 앱