패스트터틀

Android Paging v3 라이브러리 + 맵 마커 최적화 적용기 본문

Development language/android

Android Paging v3 라이브러리 + 맵 마커 최적화 적용기

SudekY 2021. 12. 25. 15:55

요소수여기


요소수 사태가 일어나고 공공데이터 포털에 전국 주유소의 요소수 보유현황 API 가 공개되었다. 그리고 이를 사용해서 맵에 뿌리면 어떨까  생각이 들어서 아주 잠깐 시간을 내서 초간단한 '요소수 여기' 안드로이드 어플리케이션을 만들었다. 근데 사용자가 10명정도 밖에 안되는것같다. 솔직히 말하자면 길찾기 기능도 없는 단순 맵에 정보 출력용 앱이라서 사용자가 많이는 없는것같기도 하다. T맵이나 네이버지도에서 요소수 정보를 제공을 해준다고하니 내가 만든 앱을 사용할 이유가 없는것같기도 하지만 사용자가 한명이라도 있으니까 업데이트 중이다.

 

각설하고 '요소수여기' 앱에서는 주유소의 정보를 리스트 형태로 다음과 같이 출력을 한다.

요소수 여기 ListAdpater

분명 처음에는 데이터가 약 115 개 정도였다. 그런데 시간이 지날수록 200.. 400개가 되더니 현재는 약 2000개의 데이터를 받아온다.

(위에 개수는 왜 1000개냐면 내가 앱을 만들 당시 115개밖에 안되길래 1페이지에 1000개만 요청하였기 때문이다. 그걸로 충분하다고 생각했었다. 근데 지금은 그렇게 요청해서 결국 모든 데이터를 표시하지도 못한다.)

 

전국에 주유소가 12000 개 정도 있는 걸 검색해서 확인하였고 최소 1만 개는 표시할 수 있어야 된다고 생각했다. 근데 굳이 모든 데이터를 다 불러들일 필요가 있을까? Json으로 오는 값을 1만 개로 하였을 때 용량으로 치환해보니 약 4MB 정도였다. 내가 사용자인데 이놈의 앱이 소중한 데이터를 4MB씩 사용한다면 사용하지 않을 것 같았다. 그래서 이를 Paging을 적용하기로 결정했다. Paging 이란 데이터를 페이지별로 나누는 것이다. 예를 들어 데이터 10000 개를 10 페이지로 나누어서 1000 개씩 보는 것을 Paging이라고 한다. 구글에 검색하면 나오는 결과가 한 번에 표시되지 않고 페이지별로 나누어 표시되는 것과 같다.

 

그렇다면 이를 구현하려면 어떠한 것들을 수정해야 할까? 간단히 생각하면 다음과 같이 요약할 수 있을 것 같다.

Recyclerview 가 현재 Position 이 데이터의 끝에 도달하였는가를 판단하고 이때 다음 페이지를 API로 요청하고 받아 온 값을 다시 submit 하여 추가한다. 근데 와중에 Exception이라도 발생하면 이를 위한 처리를 해야 한다. 그리고 다음 페이지가 로딩되고 있다는 로딩바를 뷰에 추가하고 사용자가 이를 확인할 수 있어야 한다. 생각만 해도 머리가 아프다.

하지만 안드로이드에는 갓 구글의 Paging 라이브러리가 있다. 이를 사용하면 복잡한 Paging 구현을 매우 간단(?)하게 할 수 있다.

Paging 은 버전이 v3까지 나와있다. v2에서는 내가 요청하는 API의 형태에 따라서 구현 방법이 달랐는데 v3 에는 이 과정이 통합되었다. 더 쉬워졌다고 이해하면 된다.

 

결론적으로 앱에 페이징을 적용한 결과는 다음과 같다. 구분이 잘 안 가는데 오른쪽 스크롤바를 보면 구분이 가능하다. 데이터를 한 번에 불러오는 것이 아닌 스크롤이 끝에 도달했을 때 다시 데이터를 받아와서 List에 추가가 되는 것을 볼 수 있다.

 

Paging 라이브러리 적용 전
Paging 라이브러리 적용 후

 

Paging v3


페이징 라이브러리는 다음과 같이 구성되어있다.

PagingSource는 데이터를 받아오는 곳에 직접 연결된 곳이다. RemoteMediator는 캐싱이라고 생각하면 된다. Pager는 어디서 받아올 것이고 어떻게 받아올 것인지에 대한 객체이다. PagingData는 받아온 데이터를 담은 객체이다. PagingDataAdapater는 Paging을 지원해주는 Adapter이다. 추가적으로 LoadStateAdapter라고 있는데 이는 현재 로딩 중인지를 표시하는 것을 관찰하는 표시할 수 있는 adpater이다. 자세한 사항은 공식문서를 참고!!!

 

필자는 다음과 같이 구성하였다.

PagingSource -> repositoryImpl -> mapper -> repository -> usecase -> viewModel -> Fragment -> PagingDataAdapter(with LoadStateAdapter)

 

그리고 rxjava 또한 지원해주기 때문에 implementation을 할 때는 다음과 같이 하면 된다.

implementation "androidx.paging:paging-rxjava2-ktx:3.1.0"
implementation 'androidx.paging:paging-runtime-ktx:3.1.0'

 

구현 시에는 구글 공식 샘플paging-rxjava로 적용한 Paging 소스를 참고하면 좋다.

 

 

 

요소수 여기는 오픈소스로 전부 공개되어있다. 위 패치를 적용한 V1.0.0.5 태그이다. 소스코드를 다운로드하면 된다. 

 

Release V1.0.0.5 · sdk0213/yososuwhere

요소수 리스트 보기 Paging v3 적용 요소수 맵 Paging 하여 API 요청 맵 마커 객체 최대 20 개로 지정

github.com

 

페이징 하여 API 요청하기


위 페이징 라이브러리 말고 데이터를 요청 시 한 번에 10000개씩 가져오는 것보다 나누어서 가져오는 것이 좋아 보여서 이것 또한 적용을 하였다. 400개씩 요청하는 것이다. 이때는 Rxjava와 concatMap을 사용하였다. 아래 코드는 스택오버플로우를 참고하였다.

    override fun getGasStationListHasYososuByPagingConcat(page: Int): Flowable<YososuAndLoadPage> {
        val perPage = 400
        return apiService.getYososuStationList(page, perPage)
            .toFlowable()
            .concatMap { response ->
                if (response.isSuccessful) {
                    val totalPage = (response.body()!!.totalCount / perPage) + 1
                    val nowPage = response.body()!!.page
                    // 마지막 페이지
                    if (totalPage == nowPage) {
                        Flowable.just( 데이터 )
                    }
                    // 다음 페이지
                    else {
                        Flowable.just( 데이터 )
                          .concatWith(getGasStationListHasYososuByPagingConcat(page = nowPage + 1))
                    }
                } else {
                    Flowable.create({ emitter ->
                        emitter.onError(Exception(response.errorBody().toString()))
                    }, BackpressureStrategy.MISSING)

                }
            }
    }

Recursive function을 사용한다. 데이터의 마지막 페이지까지 붙여서 차례대로 값을 가져온다. concatMap 은 내가 요청한 순서대로 받는 것을 보장해주기에 가능하다. 위 사항을 적용하고 나서는 사용자에게 현재 얼마나 많은 페이지를 로드하였는지 프로그래스 바로 표시할 수 있었다.

 

네이버 맵 마커수 줄이기


요소수 주유소가 100개일 때는 마커가 100개여도 상관없었는데 1000개를 넘어가니까 엄청난 메모리 사용량으로 ANR 직전까지 갔다. 네이버 맵은 비슷한 위치에 마커가 여러 개 있으면 최상의 마커만 표시해주는 기능(HideCollidedMarkers)이 있었지만 그래도 버벅거림이 심했다. 그래서 페이징을 적용한 김에 최적화를 시행했다. 페이징이 필요한 데이터만큼만 보여주듯이 네이버맵의 마커도 필요한 곳만 적용하기로 하였다. 

아래 링크의 보이는 곳의 코드를 참고하여서 마커만 표시하도록 변경하였다.

 

NAVER Maps API v3

NAVER Maps API v3로 여러분의 지도를 만들어 보세요. 유용한 기술문서와 다양한 예제 코드를 제공합니다.

navermaps.github.io

하지만 위 사항을 적용해도 버벅거림에는 문제가 있었다. 마커도 뷰 객체이기 때문에 이를 전부 관리하는 것에서 메모리 사용량이 많은 것으로 추측되었다. 그래서 내가 보고 있는 화면에 마커 수를 제한하기로 했다. 딱 20개만 마커를 생성하고 해당 객체를 돌려가면서 사용하기로 하였다.

private val markers = mutableListOf<Marker>()
...
..

// 20 개 생성
(1..20).forEach { _ ->
	markers.add(
    	Marker().apply {
        	position = LatLng(0.0, 0.0)
            this.map = mMap!!
            }
		)
	}
        
        
// 카메라가 멈췄을때
mMap!!.addOnCameraIdleListener {
		// 마커를 새로고침
	    refreshMarkers()
}

...
..


private fun refreshMarkers() {
	...
    ..
	var makeMarkerCount = 0
 	for (yososuStation in filteredYososuStationList) {
    	// 내가 보고있는 맵에 위치한 주유소라면
        if (mMap!!.contentBounds.contains(LatLng(yososuStation.lat, yososuStation.lon))) {
            markers[makeMarkerCount].apply {
                기존 마커 값 변경
            }
			if (++makeMarkerCount == 20) { // 생성가능한 마커의 개수(20개) 만큼만 변경
				break
			}
		}
	}
}

 

해당 사항을 적용하고 나서는 버벅거림이 아예 사라졌다. 아주 느린 구형폰에서도 버벅임이 없어졌다. 아래는 결과이다. 테스트는 안드로이드 10 갤럭시 기기에서 진행했다. GIF 라서 눈에 잘 안보일수있지만 상당히 버벅임이 심했다.

마커 최적화 적용 전
마커 최적화 적용 후

 

프로파일러로 확인하면은 얼마나 메모리 사용량이 2배가량 차이 나는 것을 확인할 수 있다. 

마커 최적화 적용 전 (약 700 MB 소비)
마커 최적화 적용 후 (약 350 MB 소비) 

 

모든 결과는 플레이스토어 '요소수 여기'를 검색하여 설치하면 확인 가능하다. 그리고 '요소수 여기'의 소스코드는 github에 공개되어있으니 참고하면 될 것 같다.

 

GitHub - sdk0213/yososuwhere: 요소수 있는곳 검색

요소수 있는곳 검색. Contribute to sdk0213/yososuwhere development by creating an account on GitHub.

github.com

 

앱 패치 후기


데이터를 필요한 만큼만 요청하는 것과 표시하는 것은 비용상 매우 중요한 문제인 것을 몸소 느꼈다. 근데 만약에 데이터가 1만 개가 아닌 100만 개라면 어떻게 해야 할까? 애플리케이션에서 API를 요청하였을 때 백엔드에서 정갈한 데이터를 주는 것은 얼마나 어려울 것이며 사용자가 딱 원하는 만큼의 데이터만 표시해주는 프런트엔드의 시너지가 얼마나 중요한 것인지 고작 2000개의 데이터를 다루며 아주 쪼끔이지만 느낀 것 같다. 생각을 조금 더 해보자면 카카오톡과 네이버 앱에서 맵에다가 예약 가능한 백신 개수를 뿌리고 예약 버튼을 눌렀을 때 뒤쪽에서 처리하는 백신예약 시스템을 개발하려고 했을 때 얼마나 많은 개발자들이 고생했을지 눈에 들어온다. 그리고 내가 만드는 시스템이 누군가에게 생사가 걸린 문제이고 정부가 이를 지켜보고 있고 5천만 국민들이 전부 사용한다는 그 부담감은 어땠을지 온몸에 소름이 돋으며 존경스럽다는 생각도 들었다.

 

Comments