💡 Intro
이전 안드로이드 위치정보 가져오기 글에서는 LocationManager에 대한 내용만 있다. 사실 LocationManager 보다는 더 정확한 위치 정보를 제공해주는 FusedLocationProviderClient를 사용하고, 이전 글에 같이 정리했었지만 계속 내용이 추가되면서 글이 자꾸 길어져서 읽기 편하게(?), 혹은 분량을 보고 뒤로가기를 클릭하지 않도록 하기 위해 분리를 했다.
LocationManager가 궁금하다면 아래 링크를 따라 이동해도 된다.
안드로이드 위치정보 가져오기 - LocationManager
💡 Intro 현재 진행하고 있는 프로젝트는 위치를 기반으로 하는 프로젝트다. 때문에 필수로 위치 정보를 가져와야 한다. 네이버맵 혹은 카카오맵에서 반환되는 값으로부터 간편하게 좌표를 받아
krrong.tistory.com
❓ FusedLocationProviderClient
FusedLocationProviderClient는 LocationManager보다 더 정확하고 효율적으로 위치 정보를 제공한다. FusedLocationProviderClient는 Google Play 서비스의 위치 API중 하나다. 기본 위치 기술을 관리하고 간단한 API를 제공하기 때문에 높은 정확도나 전력과 같은 높은 수준의 요구사항을 만족할 수 있으며 배터리 전력 사용 역시 최적화할 수 있다.
FusedLocationProviderClient는 이름에서도 알 수 있듯이 가속도계, 자이로스코프, 자기계 및 기타 센서는 물론 GPS, Wi-Fi 등의 신호를 결합한 정보다.
FusedLocationProviderClient를 사용하기 위해서는 프로젝트에 Google Play 서비스가 포함되어 있어야 한다. 추가하기 위해 여기를 참고하고 다음의 코드를 알맞는 파일에 추가하자.
// manifest
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
// gradle
dependencies {
implementation 'com.google.android.gms:play-services-location:21.0.1'
}
❗️ Create Location Service Client
액티비티의 onCreate() 메서드에서 다음 코드와 같이 FusedLocationProviderClient 인스턴스를 생성한다.
private lateinit var fusedLocationClient: FusedLocationProviderClient
override fun onCreate(savedInstanceState: Bundle?) {
// ...
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
}
❗️ Get the last known location
마지막으로 알려진 위치를 요청하기 위해서는 다음과 같은 방법을 사용할 수 있다.
fusedLocationClient.lastLocation
.addOnSuccessListener { location : Location? ->
// Got last known location. In some rare situations this can be null.
if (location != null) {
val latitude = location.latitude
val longitude = location.longitude
Log.d("Test", "GPS Location Latitude: $latitude, Longitude: $longitude")
}
}
getLastLocation() 메서드는 위경도 좌표가 포함된 Location 객체를 반환하는데 Location이 null이 될 수 있는 상황은 다음과 같다.
- 디바이스의 GPS 사용이 중지되는 경우 캐시도 지워지기 때문에 이전에 마지막 위치를 검색했더라도 반환 값이 null이 될 수 있다.
- 디바이스가 위치 정보를 얻은 적이 없는 경우 null이 반환될 수 있다.
- Google Play 서비스가 재실행되었을 때, 저장된 위치 정보가 없기 때문에 null이 반환될 수 있다.
❗️ requestLocationUpdates
FusedLocationClient도 주기적으로 위치정보를 반환받을 수 있다.
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000).apply {
setMinUpdateDistanceMeters(0F)
setWaitForAccurateLocation(true)
}.build()
찾아본 바로는 대부분 LocationRequest.create() 메서드를 사용하라고 하는데 이 메서드가 deprecated 되어 쓸 수 없더라. 이 메서드 대신 Builder를 이용해야 하고 그 코드는 위와 같다. Builder를 통해서 LocationRequest를 생성할 수 있다.
- setMinUpdateDistanceMeters : 위치 업데이트 사이의 최소 거리를 설정한다. 기본 값은 0으로 최소 업데이트 거리가 없음을 의미한다.
- setWaitForAccurateLocation(true) : true로 설정하고 이 요청이 Priority.PRIORITY_HIGH_ACCURACY인 경우, 정확도가 높은 위치를 대신 전달할 수 있을 때까지 초기 정확도가 낮은 위치의 전달이 잠시 지연된다.
이 외에도 여기를 참고하면 LocationRequest에 값을 설정해줄 수 있는 것들을 확인할 수 있다.
val locationCallback = object : LocationCallback() {
override fun onLocationAvailability(p0: LocationAvailability) {
super.onLocationAvailability(p0)
}
override fun onLocationResult(p0: LocationResult) {
super.onLocationResult(p0)
Log.d("krrong", p0.toString())
}
}
fusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
주기적으로 위치 정보를 얻고 싶은 경우 LocationCallback을 위처럼 선언하고 requestLocationUpdates() 메서드 안에 LocationRequest와 함께 넣어주면 된다.
하지만 마지막으로 알려진 위치를 조회하는 것보다 상대적으로 요청 응답시간이 길고, 디바이스 주변 환경에 따라 다르기도 하다. stopLocationUpdates() 를 호출해주지 않으면 리소스 낭비를 야기할 수 있다.
현재 LocationCallback에서 결과가 반환되었을 경우 Location 정보를 로그를 찍도록했다. 그러면 아래처럼 주기적으로 값이 반환되어 로그가 찍히는 것을 확인할 수 있다.
💡 비교
에뮬레이터의 좌표를 (37.4059, 126.6830)으로 설정해두었을 때 FusedLocationClient를 이용하여 얻은 값과 LocationManager를 통해 얻은 값을 비교해보면 다음과 같다.
어라..? FusedLocationClient를 사용하여 얻은 결과가 더 정확할 것이라고 예상했지만 같은 결과가 나왔고, 소숫점 이하 자리는 심지어 LocationManager가 더 정확하게 나왔다.
이러한 결과가 나온 이유를 예상해보면
1. 에뮬레이터를 사용했다는 점
2. 에뮬레이터의 위치를 고정해두었다는 점
3. LocationManager에서도 Provider가 fused가 반환되었다는 점
정도가 있을 것 같다.
이후에 실기기를 연결해서 확인해보면 더 정확한 이유와 결과를 얻을 수 있을 것 같다. 얼른 실기기로 연결해서 확인해봐야겠다.
💡 실기기 연결
갤럭시 노트 8(API level 28)실제 기기를 실내에서 연결하고 LocationManager와 FusedLocationClient를 비교한 결과다.
LocationManager를 사용했을 때 에뮬레이터에서 확인했던 것과 달리 provider는 gps를 반환했다. 그리고 gps provider를 사용하여 반환받은 Location의 위경도는 (37.4097664, 126.6823695)이다.
FusedLocationClient를 사용했을 때 반환받은 lastLocation의 위경도는 (37.409767, 126.6823707)이다. LocationManager를 사용한 것과 큰 차이가 없다.
흠.. 이것도 내가 예상한 결과는 아닌데 뭘까..? 정확히 모르겠다.
💡 추가(08.29)
레벨4 시작에 있는 레벨 인터뷰를 위해 레벨 3의 레벨로그를 작성하다 위치 관련하여 더 파보면 좋겠다는 생각이 들어 조금 더 고민하는 시간을 가졌다.
❗️ FusedLocationProviderClient.getCurrentLocation()
더 정확한 현재 위치를 가져오기 위해 FusedLocationProviderClient의 getCurrentLocation 메서드를 사용할 수 있다. 공식문서를 참고하면 첫 번째 파라미터가 다른 getCurrentLocation 메서드가 있는 것을 볼 수 있다. 하나는 단순히 우선순위를 받고 다른 하나는 CurrentLocationRequest를 받는다.
CurrentLocationRequest을 사용하면 현재 위치 요청에 대한 세부 정보나 옵션을 전달할 수 있다. 그런데 이 친구는 'com.google.android.gms:play-services-location:$version' $version이 21이상인 경우에만 사용할 수 있더라..
나는 CurrentLocationRequest 사용하지 못해서 자세히 찾아보지는 않았다.(왜 사용하지 못했는지는 다음에 나온다.) 각각의 인자들이 어떤 역할을 하는지 궁금하면 여기를 참고하면 좋을 것 같다.
그리고 두 메서드의 공통인자인 CancellationToken에 대해 궁금하다면 여기를 참고하자.
🤔 웃긴 네이버 지도
현재 우리 프로젝트에서는 네이버 지도를 사용하고 있다. 공식 문서를 보면 네이버 지도를 사용하기 위해서는 play-services-location 21.0.1 이상 버전에 대한 의존성을 추가하라고 하는데… 21버전 의존성을 추가하면 네이버 맵이 정상동작이 되지 않는다. (너무한거 아니냐고…ㅠ)
어쩔 수 없이 네이버 지도와 호환되는 버전을 찾아 play-services-location 19.0.1버전을 사용했다.. 그래서 CurrentLocationRequest를 사용할 수 없었고, Priority를 인자로 받는 getCurrentLocation 메서드를 사용해야만 했다.
또 재밌는건 Priority를 넘겨주기 위해 원래는 Priority.PRIORITY_HIGH_ACCURACY 또는 Priority.PRIORITY_BALANCED_POWER_ACCURACY 와 같이 Priority 클래스 안에 선언되어 있는 상수들을 가져와 사용할 수 있다. 그런데 이것도 19버전을 사용하면 Priority 클래스 자체가 import되지 않는다 ^^.. 그래서 21버전을 연동한 뒤 선언된 Priority의 int 값이 몇인지 확인한 뒤에 19버전으로 변경하여 getCurrentLocation 메서드에 상수를 넘겨주는 방식을 사용했다..
Priority 클래스는 다음과 같으니 참고하면 좋겠다.
계속 찾아보다가 발견한 것인데 공식문서에 따르면 프레임 워크 API를 사용하는 경우 Google Play 서비스로 이전하는 것이 더 좋다고 권장하고 있다. 그래서 레벨 4에서 리팩터링 하려고 한다.
👍 재비교
어떻게 사용하는지 코드를 보도록 하자.
private fun setCoordinate() {
if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
val locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
// 1. getLastKnownLocation()
val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (location != null) {
Log.d("krrong", "getLastKnownLocation - latitude : ${location.latitude}")
Log.d("krrong", "getLastKnownLocation - longitude : ${location.longitude}")
}
// 2. getCurrentLocation()
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
fusedLocationClient.getCurrentLocation(100, createCancellationToken())
.addOnSuccessListener { location ->
if (location != null) {
Log.d("krrong", "getCurrentLocation - latitude : ${location.latitude}")
Log.d("krrong", "getCurrentLocation - longitude : ${location.longitude}")
}
}
.addOnFailureListener {
Log.d("krrong", "실패 : ${it.message}")
}
}
}
private fun createCancellationToken(): CancellationToken {
return object : CancellationToken() {
override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken {
return CancellationTokenSource().token
}
override fun isCancellationRequested(): Boolean {
return false
}
}
}
진행하는 프로젝트에는 setCoordinate 메서드에 다른 작업도 존재했지만 모두 지운 뒤, 위치 권한이 있는지 확인하고 있는 경우 getLastKnownLocation 메서드와 getCurrentLocation 메서드의 결과를 로그로 찍어 확인하는 작업만 넣어둔 상태다.
getCurrentLocation 메서드에 첫 번째 인자 100은 Priority.PRIORITY_HIGH_ACCURACY와 동일한 값이다.
위 코드를 실행했을 때 결과를 예상해보면 getLastKnownLocation 메서드는 처음 위치인 (37.4059, 126.6830)을, getCurrentLocation 메서드는 이동한 후의 위치인 (37.5153, 127.1030)을 반환할 것 같다. 로그를 확인해보도록 하자.
예상과 딱 맞는다. 좋다. 일단 현재 위치를 갱신하여 반환하는 기능을 성공적으로 구현한 것 같다.
그런데 getCurrentLocation 메서드는 현재 위치가 바로 반환되지 않고, 약간의 시간이 필요하다. getLastKnownLocation 메서드는 저장되어 있는 정보를 가져오기 때문에 시간이 필요하지 않지만 아마 getCurrentLocation 메서드는 새로 현재 위치를 받아와야 하기 때문으로 생각된다.
약간의 문제 그리고 해결
현재 위치 좌표를 잘 받아와서 기분이 좋아서 짧은 시간내에 연속적으로 로그를찍어 확인해봤다. 그런데 에뮬레이터의 GPS 좌표를 변경한 뒤 바로 getCurrentLocation() 메서드를 실행했을 때 다시 현재 위치를 받아오는 것이 아니라 이전에 받아왔던 위치를 출력하고 있었다.
그래서 왜 그럴까? 고민하면서 계속 메서드를 재실행 해보다가 발견한 사실인데, 위치를 변경한 뒤 1분 이내에 getCurrentLocation() 메서드를 재호출하면 같은 위치를, 1분 정도 지난 후에 재호출하면 새로운 위치를 잘 받아온다.
무엇을 간과했을까. 다시 찾아봤다. 공식문서를 꼼꼼히 읽어보니 이렇게 나와있더라. “This may return a cached location if a recent enough location fix exists, or may compute a fresh location.”
충분히 최근에 수정된 위치가 있는 경우 캐시된 위치를 반환하거나 새로운 위치를 계산한다.
여러번의 테스트를 통한 경험적인 관점으로 바라보았을 때 여기서 의미하는 ‘최근’은 1분 내를 뜻하는 것 같다.
이것을 어떻게 해결해야 할까? 우리는 이 기능을 특정 장소를 등록하는데 사용한다. 장소를 등록하는 것이기 때문에 위치정보가 중요한데 “최근”이라는 정의가 1분 이내라고 한다면 위치정보가 꽤나 큰 차이를 보이지 않을까? 1분은 생각보다 많은 거리를 이동할 수 있는 시간이다. 그래서 어떻게 해야할까 많이 고민했다.
레아에게 여쭤보고 아차 싶었던 것은 내가 에뮬레이터로만 측정했다는 점이다. 아이폰을 사용하는 가짜 안드로이드 개발자인 나는 왜 그런 것인가 이유만 찾고 있었을 뿐 실기기에서도 테스트를 해봐야겠다는 생각을 전혀 하지 못하고 있었던 것 같다. 그래서 실기기를 바로 가져와 테스트를 해보았다.
로그를 찍어보며 테스트해본 결과 15초 내의 재요청은 같은 위치를 반환하고, 15초가 지난 후의 요청은 새로운 위치를 받아 반환해준다.
참고
FusedLocationProviderClient 공식문서
LocationRequest google-service 문서