Android GPS 위치 추적 완벽 가이드 — FusedLocationProviderClient, 경로 기록, 정확도 개선과 GPX 파일 내보내기

Android 앱에서 위치 정보를 사용하는 기능은 다양합니다.

대표적으로 현재 위치 표시, 주변 시설 검색, 이동 거리 측정, 운동 기록, 지도 경로 표시 등이 있습니다.

하지만 단순히 현재 위치를 한 번 가져오는 기능과 사용자의 이동 경로를 지속적으로 기록하는 기능은 구현 방식이 다릅니다.

예를 들어 정원 안에 있는 좁고 구불구불한 산책로를 직접 걸으면서 기록한다고 가정하겠습니다.

사용자는 실제로 다음과 같은 경로를 이동했을 수 있습니다.

출발 → 직진 → 좌회전 → 지그재그 이동 → 우회전 → 도착

그런데 GPS 오차가 크면 기록된 경로가 실제 산책로와 다르게 나타날 수 있습니다.

특히 나무가 많은 지역, 건물 주변, 좁은 통로에서는 위치 오차가 증가할 수 있습니다.

이번 글에서는 Android의 FusedLocationProviderClient를 이용해 위치 정보를 수집하고, SQLite에 저장한 뒤 GPX 파일로 내보내는 방법을 설명하겠습니다.

또한 정확도가 낮은 위치 데이터를 처리하는 방법과 백그라운드 위치 추적 시 주의사항도 살펴보겠습니다.

개발 환경: Kotlin, XML ViewBinding, MVVM, Coroutine, SQLiteOpenHelper, Google Play services Location, Storage Access Framework. minSdk 24, targetSdk 35~36을 고려합니다. 라이브러리 버전은 공식 릴리스 문서에서 확인해 적용합니다.


1. Android 위치 정보의 종류

Android에서는 여러 방법으로 위치 정보를 얻을 수 있습니다.

대표적인 위치 정보 공급원은 다음과 같습니다.

위치 정보특징
GNSS(GPS 등)위성 신호를 이용한 위치 측정
Wi-Fi주변 무선 네트워크 정보를 이용
이동통신망기지국 기반 위치 추정
Fused Location여러 위치 공급원을 조합해 위치 제공

Android 앱에서는 Google Play services의 FusedLocationProviderClient를 사용하는 경우가 많습니다.

FusedLocationProviderClient는 기기에서 이용 가능한 위치 공급원을 조합해 위치 정보를 제공합니다.

단, Fused Location을 사용한다고 해서 항상 GPS보다 정확한 위치가 제공되는 것은 아닙니다.


GPS 정확도 이해하기

2. 위치 정확도란?

Android의 Location 객체에는 위치 정확도를 나타내는 값이 있습니다.

val accuracy = location.accuracy

이 값의 단위는 미터입니다.

예를 들어 다음과 같은 값이 반환될 수 있습니다.

latitude  = 37.123456
longitude = 127.123456
accuracy  = 8.5m

위 값은 설명을 위한 가상 예시입니다.

Android의 수평 정확도 값은 일반적으로 약 68% 신뢰 수준의 추정 반경을 의미합니다.

따라서 accuracy = 5m라고 해서 실제 위치 오차가 반드시 5m 이내라는 뜻은 아닙니다.


3. GPS 오차가 발생하는 이유

위치 정확도는 여러 환경 요인의 영향을 받습니다.

대표적으로 다음과 같습니다.

  • 건물에 의한 위성 신호 차단

  • 나무와 지형에 의한 신호 감쇠

  • 건물이나 지면에서 반사되는 다중 경로 신호

  • 위성 배치 상태

  • 기기 안테나 성능

  • 위치 측정 시간

  • 절전 모드

  • 사용자 위치 권한 설정

특히 좁은 산책로에서는 몇 미터의 오차만 발생해도 실제 경로와 기록된 경로가 다르게 보일 수 있습니다.


FusedLocationProviderClient 설정

4. Gradle 의존성

app/build.gradle.kts에 Google Play services Location을 추가합니다.

dependencies {

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

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


5. AndroidManifest.xml 권한

정확한 위치를 사용하려면 다음 권한을 선언합니다.

<uses-permission
    android:name="android.permission.ACCESS_COARSE_LOCATION" />

<uses-permission
    android:name="android.permission.ACCESS_FINE_LOCATION" />

Android 12 이상에서는 사용자가 정확한 위치 대신 대략적인 위치만 허용할 수 있습니다.

따라서 ACCESS_FINE_LOCATION 권한을 선언했다고 해서 반드시 정밀한 위치를 얻을 수 있는 것은 아닙니다.

또한 Android 12 이상을 대상으로 정확한 위치를 요청할 때는 ACCESS_COARSE_LOCATION과 ACCESS_FINE_LOCATION을 함께 요청하는 방식이 권장됩니다.


런타임 권한 처리

6. 위치 권한 요청

Activity Result API를 사용해 위치 권한을 요청할 수 있습니다.

private val locationPermissionLauncher =
    registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { permissions ->

        val fineGranted =
            permissions[
                Manifest.permission.ACCESS_FINE_LOCATION
            ] == true

        val coarseGranted =
            permissions[
                Manifest.permission.ACCESS_COARSE_LOCATION
            ] == true

        when {
            fineGranted -> {
                startLocationTracking()
            }

            coarseGranted -> {
                showPreciseLocationRequiredMessage()
            }

            else -> {
                showLocationPermissionDeniedMessage()
            }
        }
    }

권한 요청은 다음과 같이 실행합니다.

locationPermissionLauncher.launch(
    arrayOf(
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION
    )
)

정밀 경로 기록 앱에서는 대략적인 위치만 허용된 상태로 경로를 수집하면 결과가 크게 부정확해질 수 있습니다.

따라서 정확한 위치 권한이 필요한 이유를 사용자에게 설명하는 것이 좋습니다.


현재 위치 가져오기

7. getCurrentLocation()

현재 위치를 한 번 가져오려면 getCurrentLocation()을 사용할 수 있습니다.

@SuppressLint("MissingPermission")
private fun getCurrentLocation() {

    val client =
        LocationServices.getFusedLocationProviderClient(
            requireActivity()
        )

    val request =
        CurrentLocationRequest.Builder()
            .setPriority(
                Priority.PRIORITY_HIGH_ACCURACY
            )
            .build()

    client.getCurrentLocation(
        request,
        CancellationTokenSource().token
    ).addOnSuccessListener { location ->

        if (location != null) {

            Log.d(
                "Location",
                "Accuracy: ${location.accuracy}"
            )
        }
    }
}

이 코드는 위치 권한이 이미 허용된 상태를 전제로 합니다.

실제 앱에서는 Fragment가 종료되거나 요청을 취소해야 할 때 CancellationTokenSource를 이용해 요청을 취소하도록 구성하는 것이 좋습니다.


지속적인 위치 추적

8. requestLocationUpdates()

사용자의 이동 경로를 기록하려면 위치 정보를 반복적으로 받아야 합니다.

이때 requestLocationUpdates()를 사용할 수 있습니다.

private lateinit var fusedLocationClient:
    FusedLocationProviderClient

private var locationCallback: LocationCallback? = null

초기화:

fusedLocationClient =
    LocationServices.getFusedLocationProviderClient(
        requireActivity()
    )

9. LocationRequest 설정

위치 요청은 다음과 같이 구성할 수 있습니다.

val locationRequest =
    LocationRequest.Builder(
        Priority.PRIORITY_HIGH_ACCURACY,
        1000L
    )
        .setMinUpdateIntervalMillis(500L)
        .setMinUpdateDistanceMeters(1f)
        .build()

설정값의 의미는 다음과 같습니다.

설정예제 값설명
PriorityHIGH_ACCURACY높은 정확도 요청
Interval1초희망 업데이트 간격
Min Update Interval0.5초허용하는 가장 빠른 간격
Min Distance1m최소 이동 거리 조건

중요한 점은 이러한 설정이 실제 GPS 측정 주기나 정확도를 보장하지 않는다는 것입니다.

기기 상태, 위치 공급원, 시스템 정책에 따라 실제 업데이트 간격이 달라질 수 있습니다.


위치 콜백 구현

10. LocationCallback

위치 정보를 수신하는 콜백을 구현합니다.

locationCallback =
    object : LocationCallback() {

        override fun onLocationResult(
            result: LocationResult
        ) {

            result.locations.forEach { location ->

                handleLocation(location)
            }
        }
    }

위치 업데이트를 시작합니다.

@SuppressLint("MissingPermission")
private fun startLocationTracking() {

    val request =
        LocationRequest.Builder(
            Priority.PRIORITY_HIGH_ACCURACY,
            1000L
        )
            .setMinUpdateIntervalMillis(500L)
            .setMinUpdateDistanceMeters(1f)
            .build()

    val callback =
        object : LocationCallback() {

            override fun onLocationResult(
                result: LocationResult
            ) {
                result.locations.forEach { location ->
                    handleLocation(location)
                }
            }
        }

    locationCallback = callback

    fusedLocationClient.requestLocationUpdates(
        request,
        callback,
        Looper.getMainLooper()
    )
}

실제 앱에서는 권한 확인과 추적 중복 시작 방지 로직을 추가해야 합니다.


위치 데이터 모델

11. GPS 기록 데이터

위치 정보는 위도와 경도만 저장하는 것보다 측정 시각과 정확도를 함께 저장하는 것이 좋습니다.

data class TrackPoint(
    val latitude: Double,
    val longitude: Double,
    val altitude: Double?,
    val accuracy: Float,
    val timestamp: Long,
    val speed: Float?
)

여기서 timestamp는 위치 측정 시각을 나타냅니다.

실제 경로 기록에서는 위치 측정 시각과 앱이 데이터를 수신한 시각을 구분하는 것이 중요합니다.


12. Location을 TrackPoint로 변환

fun Location.toTrackPoint(): TrackPoint {

    return TrackPoint(
        latitude = latitude,
        longitude = longitude,
        altitude = if (hasAltitude()) {
            altitude
        } else {
            null
        },
        accuracy = accuracy,
        timestamp = time,
        speed = if (hasSpeed()) {
            speed
        } else {
            null
        }
    )
}

고도와 속도는 기기 및 위치 공급원에 따라 제공되지 않을 수 있습니다.

따라서 해당 값이 항상 존재한다고 가정하지 않는 것이 좋습니다.


정확도 필터링

13. 정확도가 낮은 위치 제외

정밀한 경로를 기록하려면 정확도가 낮은 위치를 구분해야 합니다.

예를 들어 다음과 같은 필터를 사용할 수 있습니다.

fun isAcceptableLocation(
    location: Location
): Boolean {

    if (!location.hasAccuracy()) {
        return false
    }

    return location.accuracy <= 10f
}

이 예제에서는 정확도가 10m 이하인 위치만 허용합니다.

하지만 10m라는 값은 모든 상황에 적합한 기준이 아닙니다.

좁은 정원 산책로에서는 10m 오차도 너무 클 수 있습니다.

반대로 나무가 많은 지역에서는 기준을 지나치게 엄격하게 설정하면 대부분의 위치 데이터가 제외될 수 있습니다.

따라서 실제 환경에서 측정한 결과를 바탕으로 기준을 조정해야 합니다.


14. 정확도 필터의 한계

정확도가 낮은 위치를 제외한다고 해서 남은 위치가 모두 정확한 것은 아닙니다.

GPS가 잘못된 위치를 높은 정확도로 추정하는 경우도 발생할 수 있습니다.

따라서 다음 요소를 함께 고려하는 것이 좋습니다.

  • 위치 정확도

  • 이전 위치와의 거리

  • 경과 시간

  • 이동 속도

  • 위치 측정 시각

  • 비정상적인 위치 점프


비정상 위치 점프 제거

15. 이동 속도 기반 필터

사용자가 천천히 걷고 있는데 갑자기 수십 미터 떨어진 위치가 기록된다면 GPS 오류일 수 있습니다.

예를 들어 이전 위치와 현재 위치 사이의 거리를 계산할 수 있습니다.

fun calculateDistance(
    previous: Location,
    current: Location
): Float {

    return previous.distanceTo(current)
}

시간 차이를 이용해 평균 속도를 계산할 수도 있습니다.

fun calculateSpeed(
    previous: Location,
    current: Location
): Double? {

    val elapsedSeconds =
        (current.elapsedRealtimeNanos -
            previous.elapsedRealtimeNanos) / 1_000_000_000.0

    if (elapsedSeconds <= 0.0) {
        return null
    }

    val distance =
        previous.distanceTo(current)

    return distance / elapsedSeconds
}

이 값은 두 위치 사이의 평균 이동 속도입니다.

실제 이동 경로가 곡선이라면 두 지점 사이의 직선거리를 사용하기 때문에 실제 이동 속도보다 낮게 계산될 수 있습니다.

또한 GPS 오차가 포함되어 있으므로 이 값만으로 위치를 삭제하는 것은 주의해야 합니다.


위치 기록 간격

16. 1초마다 기록하면 정확할까?

위치 기록 간격을 짧게 설정하면 더 많은 좌표를 수집할 수 있습니다.

하지만 좌표가 많다고 해서 경로 정확도가 반드시 높아지는 것은 아닙니다.

예를 들어 실제 사용자가 움직이지 않는 상황에서도 GPS 오차로 인해 다음과 같은 좌표가 기록될 수 있습니다.

실제 위치: 동일

기록 좌표:
A → B → C → D → E

이러한 좌표를 모두 연결하면 사용자가 움직이지 않았는데도 이동한 것처럼 표시될 수 있습니다.

따라서 위치 업데이트 간격과 정확도 필터를 함께 설계해야 합니다.


17. 정밀 경로 기록을 위한 설정 예제

다음은 실험을 시작할 때 사용할 수 있는 예제 설정입니다.

val request =
    LocationRequest.Builder(
        Priority.PRIORITY_HIGH_ACCURACY,
        1000L
    )
        .setMinUpdateIntervalMillis(500L)
        .setMinUpdateDistanceMeters(0f)
        .build()

이 설정은 가능한 한 자주 위치를 수집하려는 목적의 예제입니다.

하지만 실제로 0.5초마다 위치가 제공된다는 의미는 아닙니다.

배터리 소모가 증가할 수 있으므로 장시간 기록에서는 다른 설정을 검토해야 합니다.


SQLite에 위치 기록 저장

18. 트래킹 테이블 설계

SQLiteOpenHelper를 사용하는 앱이라면 다음과 같은 테이블을 구성할 수 있습니다.

CREATE TABLE gps_tracks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    started_at INTEGER NOT NULL,
    ended_at INTEGER
);

각 경로의 위치 데이터는 별도 테이블에 저장합니다.

CREATE TABLE gps_track_points (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    track_id INTEGER NOT NULL,
    latitude REAL NOT NULL,
    longitude REAL NOT NULL,
    altitude REAL,
    accuracy REAL NOT NULL,
    timestamp INTEGER NOT NULL,
    speed REAL,
    FOREIGN KEY(track_id)
        REFERENCES gps_tracks(id)
);

하나의 트래킹 세션에 여러 위치 좌표를 연결하는 구조입니다.


19. 위치 데이터 저장

fun insertTrackPoint(
    db: SQLiteDatabase,
    trackId: Long,
    point: TrackPoint
) {

    val values = ContentValues().apply {

        put("track_id", trackId)
        put("latitude", point.latitude)
        put("longitude", point.longitude)
        put("altitude", point.altitude)
        put("accuracy", point.accuracy)
        put("timestamp", point.timestamp)
        put("speed", point.speed)
    }

    db.insertOrThrow(
        "gps_track_points",
        null,
        values
    )
}

SQLite 데이터베이스 접근은 메인 스레드에서 처리하지 않는 것이 좋습니다.

위치 데이터가 지속적으로 발생하므로 Coroutine과 전용 Repository를 통해 순차적으로 저장하는 구조를 권장합니다.


MVVM 구조 적용

20. GPS 추적 아키텍처

TrackingFragment
       │
       ▼
TrackingViewModel
       │
       ▼
TrackingRepository
       │
       ├── FusedLocationProviderClient
       │
       └── SQLiteOpenHelper
                │
                ▼
          GPS Track DB

다만 백그라운드에서도 위치를 기록해야 한다면 위치 수집 책임을 Fragment나 ViewModel에만 두어서는 안 됩니다.

화면이 종료되어도 추적을 유지해야 하는 경우에는 Foreground Service를 검토해야 합니다.


실시간 경로 표시

21. 지도에 위치 표시하기

GPS 좌표를 지도에 표시하려면 지도 SDK를 사용할 수 있습니다.

예를 들어 Google Maps SDK를 사용하는 경우 Polyline을 이용해 경로를 표시할 수 있습니다.

val polylineOptions =
    PolylineOptions()

trackPoints.forEach { point ->

    polylineOptions.add(
        LatLng(
            point.latitude,
            point.longitude
        )
    )
}

googleMap.addPolyline(
    polylineOptions
)

실제 프로젝트에서는 위치 데이터가 추가될 때마다 전체 Polyline을 새로 생성하기보다 기존 Polyline의 좌표 목록을 갱신하는 방식을 고려할 수 있습니다.


GPX 파일이란?

22. GPX 형식 이해하기

GPX는 GPS 경로와 위치 정보를 저장하는 XML 기반 파일 형식입니다.

다양한 지도 프로그램과 GPS 장치에서 활용할 수 있습니다.

대표적인 GPX 구조는 다음과 같습니다.

<?xml version="1.0" encoding="UTF-8"?>
<gpx
    version="1.1"
    creator="MyGPSApp"
    xmlns="http://www.topografix.com/GPX/1/1">

    <trk>
        <name>Garden Walk</name>

        <trkseg>
            <trkpt lat="37.123456" lon="127.123456">
                <ele>45.2</ele>
                <time>2026-10-31T01:00:00Z</time>
            </trkpt>
        </trkseg>
    </trk>
</gpx>

trkpt는 하나의 경로 좌표를 의미합니다.


GPX 파일 생성

23. Kotlin으로 GPX 만들기

GPX 파일을 생성할 때는 XML 문자열을 직접 연결하는 것보다 XML 생성 API를 사용하는 것이 안전합니다.

다음은 Android의 XmlSerializer를 이용한 예제입니다.

fun writeGpx(
    output: OutputStream,
    points: List<TrackPoint>
) {

    val serializer = Xml.newSerializer()

    serializer.setOutput(output, "UTF-8")

    serializer.startDocument("UTF-8", true)

    serializer.startTag(
        "http://www.topografix.com/GPX/1/1",
        "gpx"
    )

    serializer.attribute(null, "version", "1.1")
    serializer.attribute(null, "creator", "MyGPSApp")

    serializer.startTag(null, "trk")
    serializer.startTag(null, "trkseg")

    val formatter =
        SimpleDateFormat(
            "yyyy-MM-dd'T'HH:mm:ss'Z'",
            Locale.US
        ).apply {
            timeZone = TimeZone.getTimeZone("UTC")
        }

    points.forEach { point ->

        serializer.startTag(null, "trkpt")

        serializer.attribute(
            null,
            "lat",
            point.latitude.toString()
        )

        serializer.attribute(
            null,
            "lon",
            point.longitude.toString()
        )

        point.altitude?.let { altitude ->

            serializer.startTag(null, "ele")
            serializer.text(altitude.toString())
            serializer.endTag(null, "ele")
        }

        serializer.startTag(null, "time")

        serializer.text(
            formatter.format(Date(point.timestamp))
        )

        serializer.endTag(null, "time")
        serializer.endTag(null, "trkpt")
    }

    serializer.endTag(null, "trkseg")
    serializer.endTag(null, "trk")

    serializer.endTag(
        "http://www.topografix.com/GPX/1/1",
        "gpx"
    )

    serializer.endDocument()
    serializer.flush()
}

이 예제는 GPX 1.1의 기본 트랙 구조를 작성합니다.

실제 프로젝트에서는 네임스페이스 처리, 경로 이름, 세그먼트 구분, 오류 처리 등을 추가해야 합니다.

특히 GPS 신호가 장시간 끊긴 구간은 별도의 트랙 세그먼트로 구분하는 것이 좋습니다.


SAF로 GPX 저장

24. 사용자가 저장 위치 선택

Storage Access Framework를 이용하면 사용자가 GPX 파일의 저장 위치를 선택할 수 있습니다.

private val createGpx =
    registerForActivityResult(
        ActivityResultContracts.CreateDocument(
            "application/gpx+xml"
        )
    ) { uri ->

        if (uri != null) {
            exportGpx(uri)
        }
    }

저장 버튼:

binding.buttonExportGpx.setOnClickListener {

    createGpx.launch(
        "garden_track.gpx"
    )
}

25. GPX 내보내기

private fun exportGpx(uri: Uri) {

    viewLifecycleOwner.lifecycleScope.launch {

        try {

            val points =
                viewModel.getTrackPoints()

            withContext(Dispatchers.IO) {

                requireContext().contentResolver
                    .openOutputStream(uri)
                    ?.use { output ->

                        writeGpx(
                            output,
                            points
                        )

                    } ?: error("파일을 열 수 없습니다")
            }

            Toast.makeText(
                requireContext(),
                "GPX 저장 완료",
                Toast.LENGTH_SHORT
            ).show()

        } catch (e: Exception) {

            Toast.makeText(
                requireContext(),
                "GPX 저장 실패",
                Toast.LENGTH_SHORT
            ).show()
        }
    }
}

이 방식에서는 사용자가 선택한 문서 URI에 기록하므로 일반적인 저장소 전체 권한이 필요하지 않습니다.


백그라운드 위치 추적

26. 화면이 꺼져도 GPS를 기록하려면?

사용자가 앱 화면을 끄거나 다른 앱으로 이동해도 경로를 기록하려면 백그라운드 실행 정책을 고려해야 합니다.

Android에서는 위치 추적을 위해 Foreground Service를 사용할 수 있습니다.

다만 Android 버전별로 Foreground Service 실행 조건과 권한 요구사항이 다릅니다.

특히 Android 14 이상에서는 위치 유형의 Foreground Service 권한과 실행 조건을 검토해야 합니다.


27. Foreground Service 권한

다음은 위치 추적용 Foreground Service를 위한 Manifest 예제입니다.

<uses-permission
    android:name="android.permission.FOREGROUND_SERVICE" />

<uses-permission
    android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />

서비스에는 위치 유형을 선언합니다.

<service
    android:name=".service.LocationTrackingService"
    android:exported="false"
    android:foregroundServiceType="location" />

위치 권한도 별도로 필요합니다.

또한 Android 13 이상에서는 알림 권한 정책도 고려해야 합니다.


28. Foreground Service 시작

사용자가 앱을 사용 중이고 필요한 권한을 허용한 상태에서 추적을 시작하도록 구성하는 것이 좋습니다.

val intent = Intent(
    requireContext(),
    LocationTrackingService::class.java
)

ContextCompat.startForegroundService(
    requireContext(),
    intent
)

서비스에서는 정해진 시간 안에 startForeground()를 호출하고 지속적인 알림을 표시해야 합니다.

Android 14 이상에서는 위치 권한이 현재 유효한 상태인지, 서비스 시작이 허용되는 상황인지도 확인해야 합니다.

백그라운드에서 위치 서비스를 새로 시작하는 동작에는 추가 제약이 있습니다.


ACCESS_BACKGROUND_LOCATION

29. 백그라운드 위치 권한이 항상 필요할까?

Foreground Service를 사용한다고 해서 모든 경우에 ACCESS_BACKGROUND_LOCATION이 반드시 필요한 것은 아닙니다.

사용자가 앱을 사용 중일 때 위치 Foreground Service를 시작하고 이후 화면을 벗어나는 경우와, 앱이 이미 백그라운드에 있는 상태에서 위치 접근을 시작하는 경우는 다르게 취급됩니다.

따라서 앱의 실제 동작 방식에 맞춰 권한을 설계해야 합니다.

ACCESS_BACKGROUND_LOCATION은 필요성이 명확한 경우에만 요청해야 하며, Google Play의 백그라운드 위치 정책도 검토해야 합니다.


배터리 최적화

30. GPS 추적과 배터리 소모

정확도가 높은 위치를 자주 요청하면 배터리 사용량이 증가할 수 있습니다.

특히 다음 설정은 전력 소모에 영향을 줄 수 있습니다.

  • 높은 정확도 우선순위

  • 짧은 위치 요청 간격

  • 장시간 백그라운드 추적

  • 지속적인 화면 표시

  • 지도 렌더링

  • 빈번한 SQLite 기록

따라서 추적 목적에 따라 설정을 다르게 사용하는 것이 좋습니다.


31. 추적 모드별 설정 예제

다음은 초기 테스트를 위한 설정 예제입니다.

모드희망 간격목적
정밀 기록1초좁은 경로 상세 기록
일반 산책3초일반적인 이동 경로
장시간 이동5~10초배터리 절약 우선

이 값들은 고정된 권장 기준이 아닙니다.

실제 위치 업데이트 간격과 배터리 소모는 기기와 환경에 따라 달라집니다.


정밀 GPS의 한계

32. 스마트폰 GPS만으로 좁은 산책로를 정확하게 기록할 수 있을까?

스마트폰의 위치 정보는 환경에 따라 수 미터 이상의 오차가 발생할 수 있습니다.

따라서 폭이 1~2m인 산책로나 정원의 지그재그 경로를 정확하게 기록해야 한다면 스마트폰 GPS만으로는 한계가 있을 수 있습니다.

특히 다음 상황에서 오차가 커질 수 있습니다.

  • 나무가 빽빽한 지역

  • 건물과 담장 사이

  • 좁은 계곡

  • 위성 신호가 반사되는 지역

정밀한 지도 제작이 목적이라면 외부 GNSS 수신기나 RTK 장비를 검토할 수 있습니다.


33. 외부 GNSS와 RTK

RTK는 기준국 보정 정보를 이용해 위치 정확도를 높이는 GNSS 측위 방식입니다.

적절한 장비와 보정 환경에서는 센티미터 수준의 위치 정확도를 목표로 할 수 있습니다.

하지만 실제 정확도는 위성 수신 상태, 보정 신호, 장비, 설치 방법 등에 따라 달라집니다.

정원 안내도처럼 좁은 경로의 형상을 정확하게 기록해야 한다면 일반 스마트폰 GPS와 RTK 기반 측위를 비교해 보는 것이 좋습니다.


실제 앱 적용 사례

34. 정원 산책로 기록 앱

정원 내부 경로를 수집하는 앱은 다음과 같이 구성할 수 있습니다.

경로 기록 시작
      │
      ▼
위치 권한 확인
      │
      ▼
GPS 위치 수집
      │
      ▼
정확도 확인
      │
      ▼
SQLite 저장
      │
      ▼
지도 Polyline 표시
      │
      ▼
기록 종료
      │
      ▼
GPX 파일 내보내기

기록 중에는 다음 정보를 화면에 표시하면 유용합니다.

  • 현재 GPS 정확도

  • 기록된 좌표 수

  • 기록 시간

  • 이동 거리

  • 마지막 위치 측정 시각

  • GPS 신호 상태


35. 조류 관찰 앱

조류 관찰 앱에서는 GPS 트래킹을 다음과 같이 활용할 수 있습니다.

  • 탐조 이동 경로 기록

  • 관찰 위치 저장

  • 촬영 사진과 GPS 좌표 연결

  • 관찰 장소별 기록 조회

  • GPX 경로 내보내기

다만 야생동물의 민감한 서식지 위치가 외부에 공개되지 않도록 주의해야 합니다.

특히 희귀종이나 보호종의 정확한 위치 정보는 공개 범위를 신중하게 결정하는 것이 좋습니다.


GPS 추적 체크리스트

36. 출시 전 점검

  • Google Play services Location 추가

  • 위치 권한 선언

  • 정확한 위치 권한 요청

  • 대략적인 위치 허용 상황 처리

  • 위치 권한 거부 처리

  • LocationRequest 설정

  • LocationCallback 구현

  • 위치 추적 중복 시작 방지

  • 위치 업데이트 중지 처리

  • 위치 정확도 확인

  • 위치 측정 시각 확인

  • 비정상 위치 점프 처리

  • SQLite 저장

  • 메인 스레드 DB 작업 방지

  • 지도 Polyline 표시

  • 경로 기록 시작·종료 처리

  • GPX 파일 생성

  • SAF 파일 저장

  • 백그라운드 추적 정책 검토

  • Foreground Service 알림 구현

  • Android 14~16 서비스 제약 확인

  • 배터리 소모 테스트

  • GPS 오차 테스트

  • 위치 정보 개인정보처리방침 검토

  • Google Play 위치 권한 정책 확인

  • 실제 기기 테스트


37. 마무리

Android에서 GPS 경로를 기록하는 기능은 단순히 위도와 경도를 반복적으로 가져오는 것보다 고려해야 할 사항이 많습니다.

위치 정확도, 측정 시각, 이동 속도, 데이터 저장, 배터리 소모, 백그라운드 실행 정책을 함께 설계해야 합니다.

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

  1. FusedLocationProviderClient로 위치 정보를 수집할 수 있습니다.

  2. 위치 요청 간격이 실제 측정 주기를 보장하지는 않습니다.

  3. Location.accuracy는 위치 오차를 판단하는 중요한 지표입니다.

  4. 정확도가 낮은 좌표를 무조건 저장하는 것은 피하는 것이 좋습니다.

  5. GPS 오차로 발생하는 비정상적인 위치 점프를 검토해야 합니다.

  6. 위치 데이터는 SQLite에 세션별로 저장할 수 있습니다.

  7. Google Maps Polyline으로 이동 경로를 표시할 수 있습니다.

  8. GPX 파일을 생성해 다른 지도 프로그램에서 활용할 수 있습니다.

  9. 백그라운드 추적에는 Foreground Service 정책을 고려해야 합니다.

  10. 정밀한 지도 제작에는 외부 GNSS 또는 RTK 장비가 필요할 수 있습니다.

일반적인 산책 기록 앱이라면 스마트폰의 위치 기능만으로도 유용한 경로 데이터를 얻을 수 있습니다.

하지만 정원의 좁은 산책로나 복잡한 지그재그 경로를 정확하게 수집해야 한다면 GPS 오차를 먼저 검증하는 것이 중요합니다.

GPS 경로 기록의 핵심은 좌표를 많이 수집하는 것이 아니라, 실제 이동 경로를 얼마나 신뢰할 수 있게 기록하느냐에 있습니다.


참고 자료

태그: Android, Kotlin, GPS, FusedLocationProviderClient, LocationRequest, LocationCallback, 위치 추적, GPX, SQLite, Google Maps, Polyline, Foreground Service, GNSS, RTK, Android 16, Android 앱 개발