본문 바로가기

[TIL][내일배움캠프]

[내일배움캠프][TIL] 24.01.24 (수) - Android 앱개발 심화 - 위치 접근 권한, retrofit

1. Android 앱개발 심화 - 접근 권한, 서버/클라이언트

 

저번 시간에는 데이터 정리에 대한 개념을 한 번 알아봤다.

이번 시간에는 접근 권한과 데이터를 받아오는 서버에 대해서 알아보자.

 

 

위치 접근 권한

 

○ 위치 접근 권한 종류

  • android.permission.ACCESS_COARSE_LOCATION: 와이파이나 모바일 데이터(또는 둘 다)를 사용해 기기의 위치에 접근하는 권한. 도시에서 1블록 정도의 오차 수준.
  • android.permission.ACCESS_FINE_LOCATION: 위성, 와이파이, 모바일 데이터 등 이용할 수 있는 위치 제공자를 사용해 최대한 정확한 위치에 접근하는 권한.
  • android.permission.ACCESS_BACKGROUND_LOCATION: 안드로이드 10(API 레벨 29) 이상에서 백그라운드 상태에서 위치에 접근하는 권한.

 

○ 위치 권한 설정 방법

1) 매니페스트에 추가

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">

    <!-- 정밀 위치 권한 요청 -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    ...
</manifest>

 

2) 권한 요청

예) MainActivity.kt

class MainActivity : AppCompatActivity() {

    companion object {
        private const val PERMISSION_REQUEST_ACCESS_FINE_LOCATION = 100
    }

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

        requestLocationPermission()
    }

    private fun requestLocationPermission() {
        if (ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            // 권한이 없을 경우, 사용자에게 요청
            ActivityCompat.requestPermissions(
                this,
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                PERMISSION_REQUEST_ACCESS_FINE_LOCATION
            )
        } else {
            // 권한이 이미 있을 경우, 위치 정보를 사용할 수 있음
            getLocation()
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        when (requestCode) {
            PERMISSION_REQUEST_ACCESS_FINE_LOCATION -> {
                if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
                    // 권한이 부여되면 위치 정보를 사용할 수 있음
                    getLocation()
                } else {
                    // 권한이 거부되면, 기능 사용 불가
                }
                return
            }
        }
    }

    private fun getLocation

 

 

○ 플랫폼 API의 위치 매니저

val manager = getSystemService(LOCATION_SERVICE) as LocationManager

 

  • 위치 제공자 지정하기
    • GPS: GPS 위성
    • Network: 이동 통신망
    • Wifi: 와이파이
    • Passive: 다른 앱에서 이용한 마지막 위치 정보
  • 현재 기기에 어떤 위치 제공자가 있는지를 알고 싶다면 LocationManager의 allProviders 프로퍼티를 이용
var result = "All Providers : "
val providers = manager.allProviders
for (provider in providers) {
		result += " $provider. "
}
Log.d("maptest", result)  // All Providers : passive, gps, network..

 

  • 지금 사용할 수 있는 위치 제공자를 알아보려면 getProviders() 함수를 이용
result = "Enabled Providers : "
val enabledProviders = manager.getProviders(true)
for (provider in enabledProviders) {
		result += " $provider. "
}
Log.d("maptest", result)  // Enabled Providers : passive, gps, network..

 

  • 위치정보얻기
    • LocationManager의 getLastKnownLocation() 함수를 이용
    • Location은 위치의 정확도, 위도, 경도, 획득 시간 등의 데이터를 포함
      • getAccuracy(): 정확도
      • getLatitude(): 위도
      • getLongitude(): 경도
      • getTime(): 획득 시간
if (ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) == PackageManager.PERMISSION_GRANTED
        ) {
            val location: Location? = manager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
            location?.let{
                val latitude = location.latitude
                val longitude = location.longitude
                val accuracy = location.accuracy
                val time = location.time
                Log.d("map_test", "$latitude, $location, $accuracy, $time")
            }
        }

 

  • 계속 위치를 가져와야 한다면 LocationListener를 이용
    • onLocationChanged(): 새로운 위치를 가져오면 호출
    • onProviderEnabled(): 위치 제공자가 이용할 수 있는 상황이면 호출
    • onProviderDisabled(): 위치 제공자가 이용할 수 없는 상황이면 호출
val listener: LocationListener = object : LocationListener {
            override fun onLocationChanged(location: Location) {
                Log.d("map_test,","${location.latitude}, ${location.longitude}, ${location.accuracy}")
            }
        }
        manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 10_000L, 10f, listener)
        // (.. 생략 ..) //
        manager.removeUpdates(listener)

 

 

○ 구글 Play 서비스의 위치 라이브러리

  • 구글에서는 최적의 알고리즘으로 위치 제공자를 지정할 수 있도록 Fused Location Provider라는 라이브러리를 제공
implementation 'com.google.android.gms:play-services:12.0.1'

 

  • Fused Location Provider에서 핵심 클래스
    • FusedLocationProviderClient: 위치 정보를 얻음
    • GoogleApiClient: 위치 제공자 준비 등 다양한 콜백을 제공
    • GoogleApi Client에서는 GoogleApiClient.ConnectionCallbacks와 GoogleApiClient.OnConnection FailedListener 인터페이스를 구현한 객체를 지정
val connectionCallback = object: GoogleApiClient.ConnectionCallbacks{
            override fun onConnected(p0: Bundle?) {
                // 위치 제공자를 사용할 수 있을 때
                // 위치 획득
            }

            override fun onConnectionSuspended(p0: Int) {
                // 위치 제공자를 사용할 수 없을 때
            }
        }
        val onConnectionFailCallback = object : GoogleApiClient.OnConnectionFailedListener{
            override fun onConnectionFailed(p0: ConnectionResult) {
                // 사용할 수 있는 위치 제공자가 없을 때
            }
        }
        val apiClient = GoogleApiClient.Builder(this)
            .addApi(LocationServices.API)
            .addConnectionCallbacks(connectionCallback)
            .addOnConnectionFailedListener(onConnectionFailCallback)
            .build()

 

FusedLocationProviderClient 초기화

val providerClient = LocationServices.getFusedLocationProviderClient(this)

 

GoogleApiClient 객체에 위치 제공자를 요청

apiClient.connect()

 

onConnected() 함수에서 FusedLocationProviderClient의 getLastLocation() 함수 호출

// 위치 제공자를 사용할 수 있는 상황일 때
    override fun onConnected(p0: Bundle?) {
        if(ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) === PackageManager.PERMISSION_GRANTED){
            providerClient.lastLocation.addOnSuccessListener(
                this@MainActivity,
                object: OnSuccessListener<Location> {
                    override fun onSuccess(p0: Location?) {
                        p0?.let {
                            val latitude = p0.latitude
                            val longitude = p0.longitude
                            Log.d("map_test", "$latitude, $longitude")
                        }
                    }
                }
            )
            apiClient.disconnect()
        }
    }

 

 

retrofit

 

○ 서버 / 클라이언트

 

  • 서버(Server) : 데이터나 리소스를 제공하는 시스템. 사용자의 요청을 기다리고, 요청이 들어오면 그에 맞는 응답을 전송
  • 클라이언트(Client) : 사용자를 대표하여 서버에 정보나 서비스를 요청하는 시스템. 웹 브라우저, 모바일 앱, 데스크톱 앱 등 다양한 형태로 존재

즉, 클라이언트는 서비스를 요청하고, 서버는 서비스를 제공한다.

 

※ 3-Tier 아키텍쳐

 

 

○ 프론트엔드 / 백엔드

  • 프론트엔드 개발자 : 클라이언트처럼 사용자가 눈으로 보고 UI클릭, 터치 하는 것 과 같은 상호작용을 할 수 있는 앱을 주로 개발하는 개발자
  • 백엔드 개발자 : 사용자 눈에 보이지는 않지만 상품정보를 API로 노출, 로그인/로그아웃, 권한같은 사용자 인증을 다루는 개발자

 

○ 프로토콜(Protocol)

프로토콜은 통신 규약, 즉 약속이다.

서버가 해독할 수 없는 언어로 보내거나 정해진 규칙대로 보내지 않으면 서버에서 받아들일 수 없기 때문에 프로토콜은 필요하다.

 

  • 웹 / 어플리케이션 프로토콜 : HTTP
    • 클라이언트와 서버가 서로 HTTP 라는 프로토콜을 이용해서 서로 대화를 나눈다.
    • HTTP를 이용해 주고받는 메시지는 "HTTP 메시지" 라고 부른다.
     

 

○ API (Application Programming Interface)

리소스를 잘 활용할 수 있도록 서버가 클라이언트에게 제공하는 인터페이스.

 

○ REST API

  • REST는 Representational State Transfer를 뜻한다.
  • 웹의 장점을 최대한 활용할 수 있는 아키텍쳐 스타일
  • 예) WWW (World Wide Web)

  • REST의 핵심 원칙
    1. 자원(리소스)의 식별: 각 리소스는 고유한 URI로 식별
    2. 메시지의 상태를 통한 표현: 리소스는 JSON, XML 등의 형식으로 표현
    3. 상태가 없는(stateless) 통신: 각 요청은 서버에서 필요한 모든 정보를 포함하고 있어야 하고, 이를 통해 서버는 각 요청을 개별적으로 처리할 수 있음
    4. 클라이언트-서버 구조: 사용자 인터페이스와 데이터 저장소의 관심사가 분리되어 각각의 독립성 상승
    5. 캐시 처리 가능: 응답 데이터에 캐싱이 가능한지 여부를 명시하여 성능을 향상
    6. 계층화된 시스템: 서버와 클라이언트 사이에 다양한 계층(보안, 로드 밸런싱 등)이 존재

 

○ JSON (JavaScript Object Notation)

  • 데이터를 저장하거나 전송할 때 많이 사용되는 경량의 DATA 교환 형식
  • NAME, VALUE로 이루어져 있다.
    • 데이터이름(NAME) :  String
    • VALUE : 숫자, 문자열(String), 불리언(boolean), 객체, 배열(array), NULL값  

 

○ GSON

Google에서 제공하는 오픈소스 라이브러리.

  • GSON의 장점
    • 코드의 간결성 : GSON을 사용하면 JSON을 객체로, 또는 객체를 JSON으로 변환하는 작업을 한 줄의 코드만으로 수행 가능
    • 성능 효율성 :내부적으로 최적화된 알고리즘을 사용하여 직렬화 및 역직렬화 작업을 빠르게 수행하기 때문에 대규모의 데이터나 복잡한 구조의 객체도 효과적으로 처리
    • 광범위한 커뮤니티 지원 및 잘 정리된 문서

 

○ Retrofit

  • Square Inc.에서 개발한, 안드로이드 및 자바를 위한 타입-세이프한 HTTP 클라이언트 라이브러리
  • REST API의 HTTP 요청을 자바 인터페이스로 변환하는 것을 주 목적으로 함
  • Retrofit의 장점
    1. 코드의 간결성
      • 복잡한 HTTP API 요청을 쉽고 간결하게 제작
      • 간단한 어노테이션을 통해 요청 메서드와 URL을 정의할 수 있움
    2. 안정성과 확장성
      • 내부적으로 OkHttp 라이브러리를 사용하여 통신, 이를 통해 안정적인 통신이 가능
      • 인터셉터를 사용하여 요청/응답 프로세스를 확장하거나 수정
    3. 다양한 플러그인과 컨버터 지원
      • 다양한 데이터 형식(JSON, XML 등)에 대해 데이터 변환 컨버터를 제공
      • RxJava, Coroutines와 같은 비동기 프로그래밍 라이브러리와 연동 가능

○ Retrofit 실행하기

1. Gradle에 Retrofit 라이브러리 추가하기

// build.gradle (Module: app)
dependencies {
    implementation 'com.squareup.retrofit2:retrofit:2.x.x'
    implementation 'com.squareup.retrofit2:converter-gson:2.x.x' // Gson 컨버터 추가
}

 

2. API 인터페이스 정의하기

interface ApiService {
    @GET("users/{id}")
    fun getUser(@Path("id") id: Int): Call<User>
}

 

3. Retrofit 인스턴스 생성하기

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .build()

val apiService = retrofit.create(ApiService::class.java)

 

4. 응답 요청

  • 동기식 요청 : 현재 스레드에서 실행되며, 응답이 올 때까지 다음 코드의 실행이 중단
    val response: Response<User> = apiService.getUser(id).execute()​
  • 비동기식 요청 : 콜백을 사용하여 백그라운드에서 실행되며, 응답이 오면 해당 콜백이 호출
    apiService.getUser(id).enqueue(object: Callback<User> {
        override fun onResponse(call: Call<User>, response: Response<User>) {
            // 처리
        }
    
        override fun onFailure(call: Call<User>, t: Throwable) {
            // 오류 처리
        }
    }

 

5. 응답 객체 사용하기

if (response.isSuccessful) {
    val user: User? = response.body()
} else {
    // 오류 메시지 처리
    val error: String = response.errorBody()?.string() ?: "Unknown error"
}

 

6. 오류 처리하기

override fun onFailure(call: Call<User>, t: Throwable) {
    // 오류 메시지 표시
    Log.e("API_ERROR", t.message ?: "Unknown error")
}

 

 

 

내일은 지금까지 배운 것들을 실제로 적용시켜보는 연습을 해볼 예정이다.