Youtube data api 에는 하루에 요청할 수 있는 횟수의 제한이 있습니다.
한 api key를 기준으로, 하루 할당량 10000이며,
각 요청 종류별로 요구되는 비용이 다릅니다.
YouTube Data API (v3) - 할당량 계산기 | Google for Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English YouTube Data API (v3) - 할당량 계산기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 아래 표
developers.google.com
기존 앱과의 차별점은 음원을 다운로드할 필요가 없다는 것이므로,
감상하고 싶은 영상을 검색할 수 있는 기능이 굉장히 중요합니다.
문제는 한 번 영상을 검색하여 데이터를 받아오는 요청의 비용이 100이라는 것입니다.
이는 100명의 이용자 분들이 각각 한 번 검색을 할 경우, 하루 할당량이 소진된다는 것을 의미합니다.
이용자가 많아짐에 따라 평점이 점점 내려가며, 불평을 호소하는 메일을 많이 받았습니다.
이에 검색한 키워드에 대한 동영상 데이터를 캐싱하는 기능을 구현하게 되었습니다.
데이터 저장 방식
검색 한 번에 50개의 데이터를 가져올 수 있습니다.
목록의 바닥까지 스크롤 시, 추가 50개의 데이터를 가져오는 형식입니다.
다음은 검색 요청을 할 때 받을 수 있는 데이터 형태 예시입니다.
{
"kind": "youtube#searchListResponse",
"etag": "2Akh3aikOgrlQrnzHaVs2J3Zyks",
"nextPageToken": "CDIQAA",
"regionCode": "KR",
"pageInfo": {
"totalResults": 1000000,
"resultsPerPage": 50
},
"items": [
{
"kind": "youtube#searchResult",
"etag": "mkO2liikCE_yWeNargEVqDkbSOc",
"id": {
"kind": "youtube#video",
"videoId": "sVTy_wmn5SU"
},
"snippet": {
"publishedAt": "2023-01-03T11:30:01Z",
"channelId": "UC3IZKseVpdzPSBaWxBxundA",
"title": "NewJeans (뉴진스) 'OMG' Official MV (Performance ver.1)",
"description": "NewJeans (뉴진스) 'OMG' Official MV (Performance ver.1) Producer: MIN HEE JIN Music Video Director: Wooseok Shin ...",
},
.../
이다음의 데이터를 요청하고 싶을 경우, 주어진 nextPageToken을 매개변수로 입력하여 요청을 해야 합니다.
따라서 데이터를 50개 단위로 분할하여 키워드와 nextPageToken과 함께 저장을 하면,
다음 데이터를 요청하기 쉬울 것이라 예상을 했습니다.
Room 라이브러리를 활용하여 데이터베이스 스키마를 다음과 같이 생성했습니다.
// 키워드 테이블
@Entity
data class CashedKeyword(
@PrimaryKey val searchKeyword: String,
@ColumnInfo val savedTime: Long
)
// 검색 결과 데이터 테이블
@Entity(
foreignKeys = [
ForeignKey(
entity = CashedKeyword::class,
parentColumns = arrayOf("searchKeyword"),
childColumns = arrayOf("keyWord"),
onDelete = ForeignKey.CASCADE
)
]
)
data class YoutubeCashedData(
@PrimaryKey(autoGenerate = true) val dataId: Int,
val searchVideoData: VideoData,
val keyWord: String
)
// token 테이블
@Entity(
foreignKeys = [
ForeignKey(
entity = CashedKeyword::class,
parentColumns = arrayOf("searchKeyword"),
childColumns = arrayOf("keyWord"),
onDelete = ForeignKey.CASCADE
)
]
)
data class PageToken(
@PrimaryKey(autoGenerate = true) val tokenId: Int,
val nextPageToken: String,
val keyWord: String
)
검색 데이터를 저장하는 YoutubeCashedData 테이블과 token을 저장하는 PageToken 테이블은 모두
CashedKeyword 테이블을 참조하는 외래키를 갖습니다.
검색을 할 때, 검색 키워드가 CashedKeyword에 저장이 되며,
이 검색 키워드에 해당하는 정보들이 각 테이블에 저장됩니다.
혹시 몰라 데이터베이스 로직 함수들도 올립니다.
@Dao
interface YoutubeCashedDataDao{
@Query("SELECT * FROM YoutubeCashedData WHERE keyWord = (:searchKeyword)")
fun getAllCashedDataBySearchKeyword(searchKeyword: String): List<YoutubeCashedData>?
@Query("SELECT * FROM CashedKeyword WHERE searchKeyword = (:searchKeyword)")
fun getCashedKeywordDataBySearchKeyword(searchKeyword: String): CashedKeyword?
@Query("SELECT * FROM PageToken WHERE keyWord = (:searchKeyword)")
fun getPageTokenBySearchKeyword(searchKeyword: String): PageToken?
@Insert (onConflict = OnConflictStrategy.IGNORE)
fun insertCashedData(vararg youtubeCashedData: YoutubeCashedData)
@Insert (onConflict = OnConflictStrategy.IGNORE)
fun insertKeyword(vararg cashedKeyword: CashedKeyword)
@Insert (onConflict = OnConflictStrategy.IGNORE)
fun insertPageToken(vararg pageToken: PageToken)
@Transaction
fun insertData(searchKeyword: String, cashedKeyword: CashedKeyword, youtubeCashedDataList: List<YoutubeCashedData>, pageToken: PageToken) {
deleteAllBySearchKeyword(searchKeyword)
insertKeyword(cashedKeyword)
youtubeCashedDataList.forEach { youtubeCashedData ->
insertCashedData(youtubeCashedData)
}
insertPageToken(pageToken)
}
@Query("DELETE FROM CashedKeyword WHERE searchKeyword = (:searchKeyword)")
fun deleteAllBySearchKeyword(searchKeyword: String)
}
데이터 요청 방식
검색 프레그먼트가 실행될 때, 다음 메서드가 onCreate에서 호출됩니다.
private var nextPageToken: String? = null
private fun getData() {
CoroutineScope(Dispatchers.IO + CoroutineExceptionObject.coroutineExceptionHandler).launch {
val cashedKeyword = cashedDataDao.getCashedKeywordDataBySearchKeyword(searchWord)
if (cashedKeyword == null){
getSearchVideoData(nextPageToken)
}
else{
getDataFromDb()
}
}
}
만약 키워드가 데이터베이스 내에 있으면 정보를 불러오고, 없으면 검색 결과를 요청하는 내용입니다.
각 매소드에서는, 저 nextPageToken이라는 지역 변수에 다음 토큰 값을 넣어줍니다.
바닥으로 스크롤 시 새 데이터를 받아와야 합니다.
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener(){
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastVisibleItemPosition =
(recyclerView.layoutManager as LinearLayoutManager?)!!.findLastCompletelyVisibleItemPosition()
val itemTotalCount = recyclerView.adapter!!.itemCount-1
// 스크롤이 끝에 도달했는지 확인
if (!binding.recyclerView.canScrollVertically(1) && lastVisibleItemPosition == itemTotalCount) {
CoroutineScope(Dispatchers.IO + CoroutineExceptionObject.coroutineExceptionHandler).launch {
getSearchVideoData(nextPageToken)
}
}
}
})
설정된 nextPageToken 값을 통해 새 데이터 50개를 불러옵니다.
데이터 갱신 방식
/**
* 원래 디비의 저장된 데이터에서 갱신이 되었을 경우,
* 디비 내 데이터를 지우고 다시 넣어줌
*/
private fun saveDataToDb(){
CoroutineScope(Dispatchers.IO).launch {
val list = cashedDataDao.getAllCashedDataBySearchKeyword(searchWord)!!
if (list.size < videoDataList.size - 1){
val cashedKeyword = CashedKeyword(searchWord, System.currentTimeMillis())
videoDataList.removeLast()
val youtubeDataList = videoDataList.map{YoutubeCashedData(0, it, searchWord)}
val pageToken = PageToken(0, nextPageToken!!, searchWord)
cashedDataDao.insertData(searchWord,cashedKeyword, youtubeDataList, pageToken)
}
}
}
위 매소드는 검색창 프레그먼트의 onDestroy 매소드에 넣어주었습니다.
데이터베이스 내의 데이터 수보다, 현재 가지고 있는 데이터의 수가 더 클 경우
데이터베이스를 지워준 후, 다시 Insert 해줍니다.
결과
'안드로이드 프로젝트 > 유튜브 음정 조절 어플리케이션' 카테고리의 다른 글
[Android] # 문제 해결 - 5 앱 이슈 해결 (0) | 2023.07.01 |
---|---|
[Android] #12 mediaSession 공부 및 코드 리팩토링 - 1 (0) | 2023.06.29 |
[Android] # 문제 해결 - 4 앱 이슈 해결 (0) | 2023.06.11 |
[Android] # 문제 해결 -3 앱 이슈 해결 (0) | 2023.06.06 |
[Android] #문제 해결 -2 앱이 자주 팅기는 이슈 (0) | 2023.05.26 |
댓글