개발새발 - IT 기술블로그
article thumbnail

안녕하세요. 이번 포스팅에서는 스마트폰에 내장된 GPS 센서가 측정한 데이터를 불러오고, 상세 데이터로 변환하는 예제를 진행 해보겠습니다. 특정 기술에 대한 이해가 목적이므로 추가적인 학습이 필요한 기술의 사용은 최대한 지양하였습니다. 그럼 같이 알아보겠습니다!

 


 

개요

이번 예제에서는 Google Location Service 라이브러리의  LocationGeoCoder를 사용하여 아래의 순서로 위치 정보를 불러오는 절차를 진행해보겠습니다.

 

 

1. 현재 GPS 위치정보 불러오기

2. 불러온 위치정보를 바탕으로 추가적인 주소정보 불러오기

3. 임의의 위치와 지금 내 위치와의 거리 구하기

 

 

본론

 

라이브러리 추가

<Build.gradle>

항상 그렇듯 라이브러리는 최신버전의 태그를 사용해주시면 됩니다. 저는 21.0.1 버전을 사용하였습니다.

implementation 'com.google.android.gms:play-services-location:21.0.1'

 

 

권한추가

<AndroidManifest.xml>

위치정보를 받아오는 권한을 요청하여 줍니다.

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

<!-- 필요에 의해 사용 -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>

 

COARSE_LOCATION(이하 coarse)과 FINE_LOCATION(이하 fine)의 차이에 대해 간단하게 설명드리겠습니다.

 

coarse는 네트워크만을 사용하여 사용자의 대략적인 위치를 받아오고, fine은 GPS와 네트워크를 사용하여 좀 더 정확한 위치를 받아옵니다. finecoarse보다 정확한 위치를 받아오지만 사용자의 정보를 보호하는 부분에 있어 취약점이 있습니다. 

 

여기서 중요한 부분은 fine만 사용하고싶다고 coarse에 대한 퍼미션을 설정해주지 않으면 네트워크 사용에 대한 처리에서 에러가 납니다.  따라서 우리는 두가지 모두 허용해 주는 작업을 진행하도록 하겠습니다.

 

 

민감정보 요청

위치나 알림, 음성과 같이 민감정보에 해당하는 데이터를 받아오려면 Manifest 파일에 명시하는 것 이외에 사용자에게 직접 동의를 구해야합니다. 퍼미션(허락)을 요청하는 클래스를 생성하였습니다.

 

 

전역변수 생성

<RequestPermissionsUtil.kt>

class RequestPermissionsUtil(private val context: Context) {

    private val REQUEST_LOCATION = 1

    /** 위치 권한 SDK 버전 29 이상**/
    @RequiresApi(Build.VERSION_CODES.Q)
    private val permissionsLocationUpApi29Impl = arrayOf(
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_BACKGROUND_LOCATION
    )

    /** 위치 권한 SDK 버전 29 이하**/
    @TargetApi(Build.VERSION_CODES.P)
    private val permissionsLocationDownApi29Impl = arrayOf(
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_COARSE_LOCATION
    )
    ...

안드로이드 SDK 버전 29이상부터 백그라운드 & 포그라운드에서 위치정보를 불러올 경우 BACKGROUND_LOCATION 권한을 요청해야합니다. 이번 예제에서는 백그라운드 호출이 없지만 상황에 맞는 사용을 위해 분기하여 작성했습니다.

 

 

위치정보 권한 요청

<RequestPermissionsUtil.kt>

 

실제로 권한의 허용여부를 검사하여 SDK 버전에 맞게 권한을 요청하는 스니펫입니다. 

/** 위치정보 권한 요청**/
fun requestLocation() {
    if (Build.VERSION.SDK_INT >= 29) {
        if (ActivityCompat.checkSelfPermission(
                context,
                permissionsLocationUpApi29Impl[0]
            ) != PackageManager.PERMISSION_GRANTED
            || ActivityCompat.checkSelfPermission(
                context,
                permissionsLocationUpApi29Impl[1]
            ) != PackageManager.PERMISSION_GRANTED ||
            ActivityCompat.checkSelfPermission(
                context,
                permissionsLocationUpApi29Impl[2]
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            ActivityCompat.requestPermissions(
                context as Activity,
                permissionsLocationUpApi29Impl,
                REQUEST_LOCATION
            )
        }
    } else {
        if (ActivityCompat.checkSelfPermission(
                context,
                permissionsLocationDownApi29Impl[0]
            ) != PackageManager.PERMISSION_GRANTED
            || ActivityCompat.checkSelfPermission(
                context,
                permissionsLocationDownApi29Impl[1]
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            ActivityCompat.requestPermissions(
                context as Activity,
                permissionsLocationDownApi29Impl,
                REQUEST_LOCATION
            )
        }
    }
}

 

 

현재 GPS 위치정보 불러오기 

<MainActivity.kt>

실제 위치정보는 LocationServices 객체의 getFusedLocationProviderClient.lastLocation으로 불러옵니다.

addOnSuccessLinstener로 호출을 성공했을 경우와 addOnFailureListener로 실패했을 경우의 처리를 해주시면 됩니다.

저는 텍스트뷰와 버튼을 하나씩 생성하여 버튼을 클릭하면 텍스트뷰의 내용을 수정하는 방식으로 구현하였습니다.

@SuppressLint("MissingPermission")
    private fun getLocation(textView: TextView) {
        val fusedLocationProviderClient =
        LocationServices.getFusedLocationProviderClient(this)
        
        fusedLocationProviderClient.lastLocation
            .addOnSuccessListener { success: Location? ->
                success?.let { location ->
                    textView.text = 
                    "${location.latitude}, ${location.longitude}"
                }
            }
            .addOnFailureListener { fail ->
                textView.text = fail.localizedMessage
            }
    }

 

2023.06.08 추가 : lastLocation은 핸드폰에서 측정 된 마지막 위치정보를 불러옵니다.

그말인 즉, 가장 최신 위치가 아닐 가능성이 있다는걸 의미합니다. 현재 앱에서 새로 위치 데이터를 측정하여 갱신시키려면 .lastLocation 대신 .getCurrentLocation(PRIORITY_HIGH_ACCURACY, null) 을 사용하십시오.

 

 

override fun onStart() {
        super.onStart()
        RequestPermissionsUtil(this).requestLocation() // 위치 권한 요청
    }

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val locationText: TextView = findViewById(R.id.locationText)
    val locationButton: Button = findViewById(R.id.locationButton)
    locationButton.setOnClickListener {
        getLocation(locationText)
    }
}

Location 객체가 담고있는 정보에는 위도(latitude), 경도(longitude), 고도(altitude), 측정 정확도(accuracy), 측정 시간(time), 이동 속도(speed) 등 실시간 정보와 이전 정보와의 비교 정보들입니다.

 

 

전체 목록은 아래 링크를 통해 확인하실 수 있습니다.

 

https://developer.android.com/reference/android/location/Location.html#constants

 

 

 

그럼 실행결과를 확인해 보겠습니다.

 

처음 어플을 실행하면 위치권한을 요청할 것인지에 대해 물어보는 다이얼로그가 생성됩니다.

 

 

허용을 누른 뒤 GPS를 불러오는 버튼을 클릭하면 위도와 경도가 불러와 지는것을 보실 수 있습니다.

 

 

 

불러온 위치정보를 바탕으로 추가적인 주소정보 불러오기

다음으로 받아온 위도 경도값을 가지고 주소정보를 불러와 보겠습니다. GeoCoderAddress를 사용하였습니다.

 

아래에 작성된 getAddress 함수는 위도와 경도를 인수로 받아 주소를 출력하는 형식으로 작성하였고, 위에서 작성한 getLocation 함수의 addOnSuccessListener를 수정하였습니다.

 

getFromLocation의 3번째 인수인 1은 불러올 주소의 개수입니다. 같은 위경도에서 불러오는 주소가 여러개가 될 수 있습니다. (Address를 리스트 타입으로 생성한 이유입니다.)

 

private fun getAddress(lat: Double, lng: Double): List<Address>? {
    lateinit var address: List<Address>

    return try {
        val geocoder = Geocoder(this, Locale.KOREA)
        address = geocoder.getFromLocation(lat, lng, 1) as List<Address>
        address
    } catch (e: IOException) {
        Toast.makeText(this, "주소를 가져 올 수 없습니다", Toast.LENGTH_SHORT).show()
        null
    }
}
@SuppressLint("MissingPermission")
private fun getLocation(textView: TextView) {
    val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)
    fusedLocationProviderClient.lastLocation
        .addOnSuccessListener { success: Location? ->
            success?.let { location ->
                val address = getAddress(location.latitude, location.longitude)?.get(0)
                textView.text =
                    address?.let {
                        "${it.adminArea} ${it.locality} ${it.thoroughfare}"
                    }
            }
        }
        .addOnFailureListener { fail ->
            textView.text = fail.localizedMessage
        }
}

 

Address 클래스가 담고있는 모든 데이터는 아래 링크를 통해서 확인해주세요.

 

https://developer.android.com/reference/android/location/Address

 

 

 

실행 결과 현재의 주소가 불러와진 것을 확인하실 수 있습니다.

※ 한줄에 전체 주소를 알고싶다면 getAddressLine(index) 메서드를 사용해보세요. 오차는 존재하지만 상세주소를 출력합니다.

 

 

 

 

임의의 위치와 지금 내 위치와의 거리 구하기

마지막으로 특정 위치와 내 위치와의 거리를 구하는 코드를 작성해 보겠습니다. 방법은 간단합니다. 임의의 위도와 경도의 데이터쌍을 만들고, Location 객체의 distanceTo 메서드를 이용하시면 됩니다. 결과값은 Meter 단위로 출력됩니다.

 

@SuppressLint("MissingPermission")
private fun getLocation(textView: TextView) {
    val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)
    fusedLocationProviderClient.lastLocation
        .addOnSuccessListener { success: Location? ->
            success?.let { location ->
                val testLocal = Location("testPoint")
                testLocal.apply {
                    latitude = 35.0
                    longitude = 120.0
                }
                textView.text = "${location.distanceTo(testLocal)}M"
            }
        }
        .addOnFailureListener { fail ->
            textView.text = fail.localizedMessage
        }
}

 

 

결론

 

이렇게 기기의 GPS 센서 데이터를 불러오는 과정에 대해 알아보았습니다.

이번 예제 이외에 WorkManagerGeoFencing을 통해 백그라운드에서 지속적으로 위치정보를 호출하는 것 또한 가능하니 참고하시면 좋을 것 같습니다. 그리고  구글의 라이브러리이다보니 구글맵과도 연동이 가능한 부분이 있어서 이 또한 참고하시면 될 것 같습니다. 감사합니다.

 

 

 

참고

https://developer.android.com/reference/android/location/Address

 

Address  |  Android Developers

 

developer.android.com

https://developer.android.com/reference/android/location/Location.html#constants

 

Location  |  Android Developers

 

developer.android.com

https://developer.android.com/training/location/permissions?hl=ko 

 

위치 정보 액세스 권한 요청  |  Android 개발자  |  Android Developers

위치 정보 액세스 권한 요청 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 사용자 개인 정보를 보호하려면 위치 서비스를 사용하는 앱에서 위치 정보 액세

developer.android.com

 

 

예제코드

https://github.com/tekken5953/MobileGpsExam/tree/master

 

GitHub - tekken5953/MobileGpsExam

Contribute to tekken5953/MobileGpsExam development by creating an account on GitHub.

github.com