참고링크 : https://developer.android.com/training/location/geofencing#kotlin

개요

Android 에서는 Geofencing 기능을 통해 현재 위치가 특정 위치 반경에 진입/머물기/이탈 등의 이벤트를 처리 가능합니다.

단말 유저 당, 앱 당 최대 100개의 geofence 를 사용할 수 있습니다.

You can have multiple active geofences, with a limit of 100 per app, per device user.

Location Services에 geofence 를 위한 이벤트들을 요청해놓을 수 있습니다.

요청한 변경들이 감지되면 BroadcastReciever 등을 통해 처리가 가능합니다.

  • 반경 진입
  • 반경 이탈
  • 반경 진입 후 지정한 시간 동안 이탈 안함
  • geofence 만료 시간 (만료 시간 후 Location Service는 geofence를 알아서 지웁니다.)
항목 설명
최대 등록 수 100 per app, per device user
만료시간 Geofence 마다 millisecond 로 지정.
NEVER_EXPIRE 옵션 지원.
시간 지정 후 만료시 자동으로 Geofence 제거됨.
영역 지정 Circle 형태로 등록 되며, 위경도와 반경(m) 지정.
최상의 결과는 반경 100~150m 사이로 설정해야 함.
Wi-FI 사용 가능시 20~40m 까지 가능.
실내 위치 사용시는 5m 정도로 작을 수도 있음.
Geofence 내부 위치를 알수 없다면 Wi-Fi 정확도는 약 50m라고 가정함.
Wi-Fi 위치 사용할 수 없다면 수백m ~ 수Km 편차 생길수도 있음.
머물기 시간 지정 Geofence 진입 후 머물기로 체크할 시간을 millisecond로 지정.
Geofence 감지할 변경 타입 진입 (GEOFENCE_TRANSITION_ENTER)
이탈 (GEOFENCE_TRANSITION_EXIT)
머뭄 (GEOFENCE_TRANSITION_DWELL)
응답시간 지정 응답 시간이 큰 경우 전력을 크게 절약하고
응답 시간을 낮춘다고 바로 알림을 받지는 않음.
Geofence 추가시 초기 알림 설정 진입 (INITIAL_TRIGGER_ENTER)
이탈 (INITIAL_TRIGGER_EXIT)
머뭄 (INITIAL_TRIGGER_DWELL)
Geofence 사용 이유 설명 앱의 백그라운드 위치에 액세스하므로 사용자에게 권한 설명 필요.
Geofence 재등록이 필요없는 경우 Google Play 서비스 업그레이드나 리소스 제한으로 종료 후 재시작시
Location 프로세스 충돌시
Geofence 재등록이 필요한 경우 단말 재부팅시 부팅 완료 시점을 받아 다시 등록해야 함.
앱이 제거 후 재 설치시
앱 데이터 삭제시
Google Play 서비스 데이터 삭제시
앱에 GEOFENCE_NOT_AVAILABLE 알림 수신시
(일반적으로 Android's Network Location Provider 비활성화시 발생함)
Geofence 진입이 잘 동작하지 않는 경우 (GEOFENCE_TRANSITION_ENTER) Geofence 정확한 위치를 사용할 수 없거나 반경이 너무 작은 경우
Wi-Fi가 꺼져 있는 경우 (SettingsClient를 사용해 장치 설정 체크 필요)
- 참고로 Android 10에서는 WifiManager.setEnabled() 호출 불가하므로 설정 패널 사용.
- 시스템 앱이나 Device policy controller 에서는 가능.
Geofence 내 안정적인 네트워크 연결이 없을 경우
단순히 알림이 늦는 경우.
- 지속적으로 위치 쿼리하는 형식이 아니라 대기 시간이 필요함.
- 일반적인 대기 시간은 2 분 미만이며 장치가 움직일 때는 훨씬 적음.
- 백그라운드 위치 제한 적용시 대기 시간은 평균 약 2~3분 정도.
- 단말이 장시간 정지된 경우 대기 시간이 증가할 수 있음. (최대 6분)

 

Android App 샘플 만들기

1. 권한 추가

먼저 권한을 아래와 같이 추가 후 앱에서 permission 체크 및 요청 로직을 추가합니다.

ACCESS_BACKGROUND_LOCATION 권한은 Android 10 부터 필요합니다.

App 권한 요청 처리

Background 위치 접근 권한 처리

AndroidManifest.xml

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

    <!-- Required if your app targets Android 10 (API level 29) or higher -->
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
    ...
</manifest>

MainActivity.kt

    private val MY_PERMISSIONS_REQ_ACCESS_FINE_LOCATION = 100
    private val MY_PERMISSIONS_REQ_ACCESS_BACKGROUND_LOCATION = 101

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        checkPermission()
    }

    override fun onRequestPermissionsResult(requestCode: Int,
                                            permissions: Array<String>, grantResults: IntArray) {
        when (requestCode) {
            MY_PERMISSIONS_REQ_ACCESS_FINE_LOCATION,
            MY_PERMISSIONS_REQ_ACCESS_BACKGROUND_LOCATION -> {
                grantResults.apply {
                    if (this.isNotEmpty()) {
                        this.forEach {
                            if (it != PackageManager.PERMISSION_GRANTED) {
                                checkPermission()
                                return
                            }
                        }
                    } else {
                        checkPermission()
                    }
                }
            }
        }
    }

    private fun checkPermission() {
        val permissionAccessFineLocationApproved = ActivityCompat
            .checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ==
                PackageManager.PERMISSION_GRANTED

        if (permissionAccessFineLocationApproved) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                val backgroundLocationPermissionApproved = ActivityCompat
                    .checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION) ==
                        PackageManager.PERMISSION_GRANTED

                if (!backgroundLocationPermissionApproved) {
                    ActivityCompat.requestPermissions(
                        this,
                        arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
                        MY_PERMISSIONS_REQ_ACCESS_BACKGROUND_LOCATION
                    )
                }
            }
        } else {
            ActivityCompat.requestPermissions(this,
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                MY_PERMISSIONS_REQ_ACCESS_FINE_LOCATION
            )
        }
    }

2. dependency 추가

Location Service 및 Geofence 사용을 위해서 location 라이브러리를 추가합니다.

app/build.gradle

dependencies {
    ...
    implementation 'com.google.android.gms:play-services-location:17.0.0'
    ...
}

3. Geofence List 추가

테스트 용으로 추가할 Geofence 데이터를 생성합니다.

Geofence 객체 생성시 여러가지 설정을 할 수 있습니다.

ex) 위치 및 반경, 어떤 이벤트를 걸건지, 만료나 머무는 체크 시간을 얼마나 할지 등등

MainActivity.kt

    val geofenceList: MutableList<Geofence> by lazy {
        mutableListOf( 
            getGeofence("현대백화점", Pair(37.5085864,127.0601149)),
            getGeofence("삼성역", Pair(37.5094518,127.063603))
        )
    }

    private fun getGeofence(reqId: String, geo: Pair<Double, Double>, radius: Float = 100f): Geofence {
        return Geofence.Builder()
            .setRequestId(reqId)    // 이벤트 발생시 BroadcastReceiver에서 구분할 id
            .setCircularRegion(geo.first, geo.second, radius)    // 위치 및 반경(m)
            .setExpirationDuration(Geofence.NEVER_EXPIRE)        // Geofence 만료 시간
            .setLoiteringDelay(10000)                            // 머물기 체크 시간
            .setTransitionTypes(
                Geofence.GEOFENCE_TRANSITION_ENTER                // 진입 감지시
                        or Geofence.GEOFENCE_TRANSITION_EXIT    // 이탈 감지시
                        or Geofence.GEOFENCE_TRANSITION_DWELL)    // 머물기 감지시
            .build()
    }

4. Geofence Client 생성

Location API 사용을 위하여 Geofencing Client 인스턴스를 생성해야 합니다.

MainActivity.kt

    private val geofencingClient: GeofencingClient by lazy {
        LocationServices.getGeofencingClient(this)
    }

5. Geofencing Request 빌드

Geofence 지정 및 관련 이벤트 트리거 방식을 설정하기 위해 GeofencingRequest 를 빌드합니다.

MainActivity.kt

    private fun getGeofencingRequest(list: List<Geofence>): GeofencingRequest {
        return GeofencingRequest.Builder().apply {
            // Geofence 이벤트는 진입시 부터 처리할 때
            setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)                
            addGeofences(list)    // Geofence 리스트 추가
        }.build()
    }

6. Broadcast Receiver 추가

Geofencing 변경 이벤트를 받을 BroadcastReceiver 를 추가 후 등록해줍니다.

GeofenceBroadcastReceiver.kt

class GeofenceBroadcastReceiver: BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        val geofencingEvent = GeofencingEvent.fromIntent(intent)
        if (geofencingEvent.hasError()) {
            val errorMessage = GeofenceStatusCodes.getStatusCodeString(geofencingEvent.errorCode)
            Log.e("GeofenceBR", errorMessage)
            return
        }

        // Get the transition type.
        val geofenceTransition = geofencingEvent.geofenceTransition    // 발생 이벤트 타입

        // Test that the reported transition was of interest.
        if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
        geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {

            // Get the geofences that were triggered. A single event can trigger
            // multiple geofences.
            val triggeringGeofences = geofencingEvent.triggeringGeofences

            val transitionMsg = when(geofenceTransition) {
                Geofence.GEOFENCE_TRANSITION_ENTER -> "Enter"
                Geofence.GEOFENCE_TRANSITION_EXIT -> "Exit"
                else -> "-"
            }
            triggeringGeofences.forEach {
                Toast.makeText(context, "${it.requestId} - $transitionMsg", Toast.LENGTH_LONG).show()
            }

        } else {
            Toast.makeText(context, "Unknown", Toast.LENGTH_LONG).show()
        }
    }
}

AndroidManifest.xml

<application... >
    ...
    <receiver android:name=".GeofenceBroadcastReceiver"/>
</application>

MainActivity.kt

    private val geofencePendingIntent: PendingIntent by lazy {
        val intent = Intent(this, GeofenceBroadcastReceiver::class.java)
        PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
    }

7. Geofence Request 및 Callback 추가

Geofencing Client에 Geofence 정보 및 트리거 방식을 가지고 있는 Geofencing Request과 이벤트 발생시 처리할 Broadcast Receiver를 추가해줍니다.

MainActivity.kt

private fun addGeofences() {
    geofencingClient.addGeofences(getGeofencingRequest(geofenceList), geofencePendingIntent).run {
        addOnSuccessListener {
            Toast.makeText(this@MainActivity, "add Success", Toast.LENGTH_LONG).show()
        }
        addOnFailureListener {
            Toast.makeText(this@MainActivity, "add Fail", Toast.LENGTH_LONG).show()
        }
    }
}

준비는 끝났습니다. 앱을 실행해보아 동작을 확인합니다.

 

Android Developer Document에서 권장하는 가이드 내용입니다.

https://developer.android.com/jetpack/docs/guide

Android 앱에는 여러가지 구성요소가 포함되며, 이런 내용들은 Manifest 에서 선언되어 있습니다.

ex) Activity, Fragment, Service, Content Provider, Broadcast Receiver

 

사용자는 짧은 시간 내에 여러 앱과 상호작용 할 경우도 많습니다.

앱은 사용자 중심의 다양한 워크플로 및 작업에 맞게 조정될 수 있어야 합니다.

 

예를 들어 SNS 앱에서 사진 앱을 통해 사진을 공유하거나 앱 사용 중 전화나 알림으로 사용 환경이 중단될 수도 있죠.

또한 모바일 기기는 리소스가 제한되어 언제든 일부 앱 프로세스가 종료할 수 있습니다.

 

이러한 환경을 고려할 때, 앱 구성 요소는 개별적이고 비순차적 실행이 될 수 있으며 언제든 구성 요소가 제거 될 수 있습니다.

이러한 이벤트는 직접 제어가 불가하기 때문에 앱 구성 요소에는 앱 데이터나 상태를 저장하면 안됩니다.

그리고 앱 구성 요소가 서로 종속되면 안됩니다.

일반적인 아키텍처 원칙

앱 데이터와 상태를 앱 구성 요소에 저장할 수 없다면?

관심사 분리

Activity나 Fragment에 모든 코드를 작성하는 실수는 흔합니다.

UI 기반 클래스는 UI 및 OS 상호작용 등의 로직만 포함해야 합니다.

이러한 클래스를 최대한 가볍게 유지하면 생명 주기(LifeCycle) 관련 문제들을 피할 수 있습니다.

 

Activity 및 Fragment 구현은 소유의 대상이 아니며 Android OS 와 앱 사이의 계약을 나타내는 클래스일 뿐입니다.

언제든지 OS가 클래스를 제거할 수 있으니 이러한 클래스에 대한 의존성을 최소화 하는 것이 좋습니다.

Model에서 UI 구동

또 하나의 중요한 원칙은 Model에서 UI를 구동해야 한다는 것입니다.

가급적 지속성 있는 Model을 권장합니다.

 

Model은 앱의 데이터 처리를 담당하며, 앱의 View 객체 및 앱 구성요소로부터 독립되어 있습니다.

그래서 앱의 생명 주기 관련 문제의 영향을 받지 않습니다.

지속성 있는 Model이 이상적인 이유입니다.

  • Android OS에서 리소스를 확보하기 위해 앱을 제거해도 사용자 데이터는 삭제되지 않음.
  • 네트워크 연결이 취약하거나 연결되어 있지 않아도 앱이 계속 작동함.

데이터 관리 책임이 잘 정의된 Model 클래스를 기반으로 앱을 만들면 쉽게 테스트하고 일관성을 유지할 수 있습니다.

권장하는 앱 아키텍처

이 섹션에서는 Android Architecture Component 를 사용하여 앱을 구성하는 방법을 보여줍니다.

모든 시나리오에 최적화된 하나의 앱 작성 방법은 없지만 이 아키텍처가 유용한 출발점이 될 것입니다.

일반적인 아키텍처 원칙을 따르는 Android 앱 작성법을 사용하고 있으면 변경할 필요가 없습니다.

사용자 프로필 UI를 가정해보면, 비공개 백엔드 및 REST API를 사용하여 지정된 프로필 데이터를 가져옵니다.

개요

앱을 설계한 후 모듈들이 서로 어떻게 상호작용해야 하는지 다이어그램이 보여줍니다.

각 구성요소가 한 수준 아래의 구성요소에만 종속됨을 볼 수 있습니다.

Activity 및 Fragment는 ViewModel에만 종속됩니다.

 

Repository는 여러 개의 다른 클래스에 종속되는 유일한 클래스입니다.

Repository는 지속성 있는 데이터 모델과 원격 백엔드 데이터 소스에 종속됩니다.

이 설계는 일관되고 쾌적한 사용자 환경을 제공합니다.

 

사용자가 앱을 닫은 후 재사용시 앱이 로컬에 보존하는 사용자 정보가 바로 표시됩니다.

이 데이터가 오래된 경우 앱의 Repository 모듈이 백그라운드에서 데이터 업데이트를 시작합니다.

UI 빌드

UI 에 필요한 데이터는 ViewModel 아키텍처 구성요소에 기반한 ViewModel에서 정보를 유지합니다.

ViewModel 객체는 UI 구성요소에 관한 데이터를 제공하고 모델과 커뮤니케이션하기 위한 데이터 처리 비즈니스 로직을 포함합니다. ViewModel 은 UI 구성요소에 관해 알지 못하므로 구성 변경(세로 -> 가로모드)의 영향을 받지 않습니다.

ViewModel에서는 데이터 객체에 대한 변경 등을 View에 알려야 합니다.

여기에서 LiveData 가 사용됩니다.

 

LiveData는 객체 변경사항을 모니터링 할 수 있으며, Activity/Fragment/Service 와 같은 앱 구성요소의 생명 주기 상태를 고려한 로직을 포함합니다. (LiveData 필드는 생명 주기로 인해 더 이상 필요하지 않은 참조를 자동으로 정리합니다.)

RxJava와 같은 라이브러리를 사용 중이라면 그대로 사용해도 됩니다. 단 앱 생명 주기를 올바르게 처리해야 합니다.

Activity나 Fragment 에서 LiveData 객체를 observe 후 변경시에 대한 콜백 onChanged() 를 사용해 UI를 새로고침할 수 있습니다.

 

LiveData는 생명 주기를 인식하기 떄문에 Activity 나 Fragment의 onStop(), onDestroy() 메서드 등에 대한 처리하지 않아도 됩니다.

Data 가져오기

데이터를 가져오는 방식은 내부 데이터나 외부 API를 통한 데이터를 가져오는 방식들이 있습니다.

예제에서는 Retrofit 라이브러리를 통해 백엔드 REST API를 사용합니다.

 

ViewModel에서 직접 해당 라이브러리를 이용하여 데이터를 LiveData 객체에 할당하는 방법이 있습니다.

이 경우 ViewModel 클래스에 너무 많은 책임이 부여되어 관심사 분리 원칙을 위반하게 됩니다.

또한 ViewModel의 범위는 Activity나 Fragment 생명 주기에 연결되어 있으므로 데이터가 손실될 수도 있습니다.

그래서 Repository 모듈을 사용하여 데이터 작업을 처리합니다.

 

Repository는 지속적인 모델, 웹 서비스, 캐시 등 다양한 데이터 소스 간 중재자로 볼 수 있습니다.

이제 ViewModel이 데이터를 가져오는 방법을 직접 알지 못하기 때문에 Repository에서 서로 다른 데이터 가져오기 구현을 통한 데이터를 ViewModel에 제공할 수 있습니다.

구성요소 간 종속성 관리

Repository 클래스에서 WebService를 통해 데이터를 가져올 경우 WebService 클래스의 종속성을 알아야 합니다.

이러한 상황에서 다른 클래스에서도 WebService가 필요한 경우 코드를 복제해야 합니다.

이런 문제는 아래 설계 패턴을 사용하여 해결할 수 있습니다.

  • 종속성 주입(DI) : 클래스가 자신의 종속성을 구성할 필요 없이 종속성을 정의할 수 있습니다.

     

    런타임시 다른 클래스가 이 종속성을 제공해야 합니다.
    Android 앱에서는 Dagger 2 라이브러리 사용이 좋습니다.
    Dagger 2 는 종속성 트리를 따라 객체를 자동으로 구성하고 종속성을 컴파일 시간에 보장합니다.
  • 서비스 로케이터 : 클래스가 자신의 종속성을 구성하는 대신 종속성을 가져올 수 있는 레지스트리를 제공합니다.

DI 사용보다 서비스 레지스트리 구현이 쉬우므로 DI가 익숙치 않다면 서비스 로케이터 패턴을 사용해도 됩니다.

이 패턴은 코드를 복제하거나 복잡성을 추가하지 않아도 종속성을 관리하기 위한 명확한 패턴을 제공하므로 코드를 확장할 수 있습니다.

이러한 패턴을 사용하면 테스트 및 프로덕션 데이터 가져오기 구현 등을 신속하게 전환할 수 있습니다.

ViewModel 과 Respository 연결

ViewModel에서 DI (Dagger 2) 를 사용하여 Repository를 주입시켜 봅니다.

Repository가 WebService 객체 호출을 하지만 하나의 데이터 소스에만 의존하면 유연성이 떨어집니다.

따라서 매번 WebService를 호출하는 방식은 네트워크 낭비 및 새 쿼리 완료시까지 사용자가 기다려야 하는 문제가 있습니다.

이런한 문제를 해결하기 위해 Repository에 데이터를 캐시하는 소스를 추가해 봅니다.

지속되는 데이터

위에까지 구현 방식은 앱이 나갔다 들어오거나 하는 경우 Repository가 메모리 내 캐시에서 데이터를 가져옵니다.

하지만 장시간 프로세스를 종료 후 들어온다면 다시 네트워크에서 데이터를 가져와야 합니다.

이런 경우 Room 같은 라이브러리가 필요합니다.

이미 SQLite 같은 ORM을 사용 중이라면 꼭 Room으로 대체할 필요는 없습니다.

Room에서는 Annotation으로 로컬 스키마 정의가 가능합니다. (@Entity, @PrimaryKey)

RoomDatabase를 구현하여 추상 클래스를 만들면 Room에서는 자동으로 구현을 제공합니다.

 

데이터를 데이터베이스에 액세스하기 위해 데이터 액세스 객체 (DAO) 를 만듭니다.

Room 메서드에서 LiveData를 사용하면 데이터 변경시 자동으로 모든 Observer들에게 알리게 됩니다.

DAO를 정의했으면 데이터베이스 클래스에서 DAO를 참조 후 Repository 소스를 수정하면 됩니다.

단일 소스 Repository

다양한 REST API 엔드포인트는 흔히 동일한 데이터를 반환합니다.

일관선 확인 없이 Repository가 WebService 요청으로부터 응답을 있는 그대로 반환한다면 Repository 의 데이터 버전과 형식이 가장 최근에 호출된 엔드포인트에 종속되므로 UI에서 혼란스러운 정보를 표시할 수 있습니다.

 

그렇기에 Repository 구현에서는 데이터베이스에 WebService 응답을 저장합니다.

그러면 데이터베이스 변경시 LiveData 객체에 콜백이 트리거 됩니다.

 

이 모델을 사용하면 데이터베이스가 단일 소스 Repository 역할을 하며 앱의 다른 부분은 Repository를 사용하여 데이터베이스에 엑세스 합니다. 디스크 캐시 사용 여부와 상관없이 Repository에서 데이터 소스를 나머지 앱의 단일 소스 Repository 로 지정하는 것이 좋습니다.

진행 중인 작업 표시

SwipeRefresh 같은 사용 사례에서는 네트워크 작업시 UI 표시가 중요합니다.

다양한 이유로 데이터가 업데이트 될 수 있으므로 실제 데이터와 UI 작업을 분리하는 것이 좋습니다.

  • LiveData 유형의 객체를 반환하도록 변경하고, 이 객체에는 네트워크 작업 상태가 포함됩니다.
  • Repository 클래스에 데이터 새로고침 상태를 반환할 수 있는 다른 함수를 제공합니다.

각 구성요소 테스트

관심사 분리의 원칙을 따랐을 때 한 가지 중요한 이점은 테스트 가능성입니다.

  • UI 및 인터렉션 : Android UI Instrumentation Test 를 사용합니다. Espresso 라이브러리를 사용하는 것이 가장 좋으며, Mock으로 ViewModel을 제공할 수 있습니다. Activity나 Fragment는 ViewModel 하고만 통신하므로 이 클래스만으로도 충분히 UI 를 테스트 할 수 있습니다.

  • ViewModel : JUnit Test 를 사용하여 ViewModel 클래스를 테스트할 수 있습니다. Repository만 Mock 객체로 테스트하면 됩니다.

  • Repository : 역시 JUnit Test로 테스트할 수 있습니다. WebService난 DAO를 Mock 객체로 테스트하면 됩니다.

    • Repository가 올바른 WebService를 호출하는지
    • Repository가 데이터베이스에 결과를 저장하는지
    • 데이터가 캐시되고 최신인 경우 Respository가 불필요한 요청은 안만드는지
  • WebService와 DAO 는 모두 인터페이스이므로 더 복잡한 테스트를 위해 Mock이나 fake 구현을 만들 수 있습니다.

  • DAO : Instrumentation Test를 사용하여 테스트합니다. UI 구성요소가 필요하지 않으므로 빠르게 실행되며, 각 테스트에서 메모리 내에 데이터베이스를 만들어 테스트 부작용이 없게 합니다. (ex. 디스크의 DB 파일 변경)

    • Room에서 데이터베이스 구현을 지정하도록 허용하므로 SupportSQLiteOpenHelper 의 JUnit 구현을 제공하여 DAO를 테스트할 수 있습니다. 하지만 기기에서 실행 중인 SQLite 버전이 개발 PC의 SQLite 버전과 다를 수 있으므로 이 방법은 권장되지 않습니다.

  • WebService : 테스트에서 백엔드로 네트워크 호출하지 않도록 합니다. 웹 기반 테스트를 포함한 모든 테스트가 외부로부터 독립적이어야 합니다. MockWebServer 를 포함하여 여러 라이브러리를 활용하여 가짜 로컬 서버를 만들 수 있습니다.

  • 아티팩트 테스트 : 아키텍처 구성요소는 백그라운드 스레드를 제어할 수 있는 maven 아티팩트를 제공합니다.

    `androidx.arch.core:core-testing` 아티팩트에는 다음과 같은 JUnit 규칙이 있습니다.
    • InstantTaskExecutorRule : 호출 스레드에서 백그라운드 작업을 즉시 실행합니다.
    • CountingTaskExecutorRule : 아키텍처 구성요소의 백그라운드 작업을 기다립니다. Espresso와 연결할 수 있습니다.

권장사항

다음 권장사항은 필수는 아니지만 경험에 의한 권장사항을 따르는 경우 장기적으로 더 강력하고 테스트 및 유지관리가 쉬운 코드 베이스를 만들 수 있습니다.

  • Activity, Service, Broadcast Receiver와 같은 앱의 진입점을 데이터 소스로 지정하지 마세요.
  • 앱의 다양한 모듈 간 책임이 잘 정의된 경계를 만듭니다.
  • 각 모듈에서 가능하면 적게 노출합니다.
  • 각 모듈을 독립적으로 테스트하는 방법을 고려합니다.
  • 다른 앱과 차별되도록 앱의 고유한 핵심에 초점을 맞춥니다.
  • 가능한 한 관련성이 높은 최신 데이터를 보존합니다.
  • 하나의 데이터 소스를 단일 소스 저장소로 지정합니다.

추가: 네트워크 상태에 따른 노출 처리 로직

SonarQube 와 Jenkins 서버를 가지고 있다는 전제 하에 시작되는 이야기입니다.

개인이 아닌 팀 규모의 개발팀에서는 보통 CI 관련하여 Jenkins 등을 많이 사용하고 있습니다.

JenkinsSonarQube 를 활용하여 사용하는 이야기 입니다.

 

먼저 시나리오를 써봅니다. 어떤 시나리오냐?

Jenkins 와 SonarQube 를 언제 어떤 용도로 사용하느냐에 대한 시나리오 입니다.

 

보통은 소스 관리를 하기 위해 GitHub, GitLab 등을 사용을 합니다.

그러면 보통은 아래와 같은 구성들이 나오겠죠

 

  • GitHub - Jenkins - SonarQube
  • GitLab - Jenkins - SonarQube

제가 사용하던 동작 흐름은 아래와 같습니다.

 

  • 소스 작업 commit 후 push
  • GitLab 에서 변화 감지후 Jenkins로 WebHook 발생
  • Jenkins에서 WebHook 인지 후 소스 pull 후 build
  • build 완료 후 deploy

이 중에 build 전후로 SonarQube 를 추가해보기로 합니다.

그러기 위해서 Jenkins에서 SonarQube 를 연동하는 법을 정리해봅니다.

제가 알아본 방법은 크게 2가지가 있었습니다.

 

  1. Jenkins Plugin 으로 사용하는 방법
  2. gradle Task 추가 후 사용하는 방법

1. Jenkins Plugin 사용하는 방법

먼저 Jenkins 에 SonarQube Scanner 플러그인을 설치합니다.

 

SonarQube Scanner for Jenkins

Jenkins 관리 - 플러그인 관리 - SonarQube Scanner for Jenkins 설치

(https://plugins.jenkins.io/sonar)

 

그리고 적용하려는 프로젝트 구성에 SonarQube 연동하는 스텝을 추가합니다.

Jenkins - 프로젝트 - 구성 - Add build step - Execute SonarQube Scanner 추가

Analysis properties 작성

 

아래 이미지와 같이 작성하며, 아래 요소들을 주의하여 작성합니다.

  • sonar.host_url : SonarQube 서버 url 및 port
  • sonar.login : SonarQube ID 혹은 token
  • sonar.password : SonarQube Password (ID에서 token 사용시는 공백)
  • sonar.projectKey : SonarQube 프로젝트 값 참조
  • sonar.projectName : SonarQube 프로젝트 값 참조
  • sonar.java.binaries : Jenkins workspace 내에서 컴파일 후 생성된 class binary 경로
  • sonar.profile : SonarQube 프로젝트 값 참조
  • sonar.coverage.jacoco.xmlReportPaths : Jenkins workspace 내에서 JaCoCo Report 생성 경로 (JaCoCo 미사용시 skip)

위 Step을 추가 후 빌드를 돌려봅니다.

빌드가 잘 되었으면 콘솔 로그도 한번 구경해봅니다.

+) Coverage Report를 작성해주는 JaCoCo 플러그인도 있습니다.

 

JaCoCo plugin

Jenkins 관리 - 플러그인 관리 - JaCoCo plugin 설치

(https://plugins.jenkins.io/jacoco)

 

적용하려는 프로젝트 구성에 Coverage Report를 작성하는 스텝을 추가합니다.

Jenkins - 프로젝트 - 구성 - Add build step - Record JaCoCo coverage report 추가

아래의 항목들을 추가 작성합니다.

 

  • Inclusions (e.g.: **/*.class) : **/*.class

  • Exclusions (e.g.: **/*Test*.class) : **/R.class, **/R$*.class, **/BuildConfig.*, **/Manifest*.*, com/android/**/*.class

변경한 구성을 저장 후 빌드를 진행해 봅니다.

빌드가 성공하였다면 역시나 콘솔 로그를 확인해봅니다.

2. gradle Task 추가 후 사용하는 방법

먼저 Android 앱 소스에 gradle Task 추가하는 방법은 아래 링크를 참고하시면 됩니다.

그리고 Jenkins 프로젝트 구성에서 아래와 같이 gradlew 명령을 직접 써주시면 됩니다.

Jenkins - 프로젝트 - 구성 - Add build step - Execute shell 추가

 

chmod +x gradlew

# JaCoCo 커버리지 리포트 작성
./gradlew coverageReport

# SonarQube 분석
./gradlew sonarqube

빌드 후 확인을 해봅니다.

3. 2가지 방법의 차이?

먼저 플러그인을 사용하면 프로젝트 소스 레벨에서 추가할 부분이 없습니다.

그리고 Jenkins 프로젝트 상태 화면에 관련 UI도 이쁘게 생긴답니다.

하지만 JaCoCo플러그인 은 빌드 후 조치 구성에서만 추가가 가능하였고, Build 구성에는 추가가 불가하였습니다. SonarQube 플러그인은 Build 구성에서만 추가가 가능하였고요.

(Jenkins 프로젝트가 Maven 프로젝트였다면 다 가능한 걸로 보였습니다.)

 

gradle Task 방식으로 진행하면 Excute shell 항목에서 SonarQube, JaCoCo 사용이 자유롭게 가능합니다.

 

하지만 프로젝트 소스 레벨에서 gradle Task를 추가해야 했으며, Jenkins 프로젝트 상태 화면도 다시 평범해졌습니다.

두 방식을 사용시 장단점들이 있는 상태였습니다.

그 후...

저 이 후로 GitLab MergeRequest 시 Jenkins가 위 동작들을 처리 후 SonarQube 결과를 GitLab에 MR 메세지로 남길 수 있지 않을까 했습니다만...

특히나 검색해보니 GitHub 플러그인 사용시 아래와 같이도 사용이 가능해 보였습니다.

하지만 제가 써야 하는 건 GitLab 이었고, GitLab 플러그인은 지원이 중단되었습니다...

정확히는 기존에 있던 개인 개발자가 만들던 플러그인이 SonarQube 7.7 버전부터 중단되었습니다.

https://github.com/gabrie-allaigre/sonar-gitlab-plugin/issues/222

위 링크의 코멘트나 아래 링크를 보면 GitLab과 콜라보레이션을 위해 SonarQube 에서 플러그인 지원을 중단한 것으로 보입니다...

https://www.sonarqube.org/sonarqube-8-1/

아쉬운 대로 Jenkins에서 빌드 후 조치에 GitLab 관련된 플러그인 기능을 사용해 보았지만.... 이렇네요..

네 그래서 일단은 접었습니다.

추후에 여유가 생긴다면 다른 방법들을 생각해보던지 해야겠습니다.

'IT > ETC' 카테고리의 다른 글

Eclipse Copilot  (0) 2024.10.17
Copilot 리서칭  (1) 2024.10.17
[SonarQube] SonarQube 도입기?  (0) 2020.02.07
[Docker] Docker란?  (0) 2018.03.23
Expert Beginner란...  (0) 2018.03.12

+ Recent posts