Ⅰ. 추천받기 ViewModel
3단계를 거치며 수집된 데이터들을 저장하는 뷰모델이다. 이 뷰모델 덕분에 마지막에 ResultFragment에서 한 번에 서버로 요청 보낼 수 있다.
class RecommendationViewModel : ViewModel() {
var place: String = ""
var decibel: Float = 0.0f
var goal: String = ""
// 서버 전송용 영문명
var englishPlace: String = ""
var englishGoal: String = ""
// 데이터 확인용 로그
fun checkData() {
Log.d("RecResultFragment", "현재 저장된 데이터 -> 장소: $place / 데시벨: $decibel / 목표: $goal")
}
// 서버에서 최종적으로 받은 플레이리스트를 담는 변수
val currentPlaylist = MutableLiveData<RecommendationResponse>()
}
Ⅱ. 움직이는 구슬 구현

디자인팀에서 결과를 기다리는 화면에서 구슬에 움직이는 애니메이션이 있으면 좋을 것 같다고 하였다. 그래서 처음에 그냥 gif로 추출하여 넣으면 될 것 같다고 생각했는데 gif 파일 용량이 너무 크고 끊김 현상이 발생했다.
그래서 최종으로 WebP 형식을 선택했다. Glide 라이브러리를 사용하면 WedP 애니매이션을 로드할 수 있다.
// 구슬 움직이는 animation
com.bumptech.glide.Glide.with(this)
.load(R.drawable.orb_animation) // webp 파일
.into(binding.centerButton)
Ⅲ. Polling 방식✨
이번 프로젝트의 가장 큰 도전 과제 중 하나였다. 백엔드 AI Agent가 음악을 추천하는 로직은 평균 6초 이상 소요되는 비동기 작업이다.
일반적인 API 요청처럼 마냥 기다리기엔 연결이 끊길 위험이 있어, "작업 시작 요청 -> taskId 획득 -> 주기적으로 상태 확인" 순서로 진행되는 폴링 방식을 채택했다.
1) 비동기 작업 시작 및 taskId 추출
뷰모델에서 데이터를 꺼내 request 데이터를 만들어 서버로 보낸다. 그에 대한 응답으로 서버는 taskId를 보내는데 정규식으로 추출하여 필요한 아이디만 뽑아냈다. taskId로 현재 사용자가 요청한 플레이리스트를 받아볼 수 있다.
private fun startAsyncGeneration() {
val request = RecommendationRequest(
place = viewModel.englishPlace,
decibel = viewModel.decibel,
goal = viewModel.englishGoal
)
RetrofitClient.recommendationApi.sendPlaylistPolling(request).enqueue(object : Callback<BaseResponse<String>> {
override fun onResponse(call: Call<BaseResponse<String>>, response: Response<BaseResponse<String>>) {
if(response.isSuccessful) {
val message = response.body()?.data // "taskId: task_XXX..." 형태
if(message != null) {
// 정규식으로 taskId 추출
val regex = "taskId:\\s*([^\\s]+)".toRegex()
val matchResult = regex.find(message)
val taskId = matchResult?.groupValues?.get(1)
if (taskId != null) startPollingLoop(taskId) // 폴링 시작
}
}
}
// ... 생략
})
}
2) 코루틴을 활용한 폴링 루프
2초 간격으로 서버에 플레이리스트를 받기 위해 요청을 보낸다. 무한정으로 기다릴 수는 없기 때문에 타임아웃을 20초로 두었다.
private fun startPollingLoop(taskId: String) {
pollingJob = viewLifecycleOwner.lifecycleScope.launch {
val startTime = System.currentTimeMillis()
var isFinished = false
while (isActive && !isFinished) {
// 20초 타임아웃 체크
if (System.currentTimeMillis() - startTime > 20000L) {
handleErrorAndExit("생성 시간이 초과되었습니다.")
return@launch
}
val response = kotlinx.coroutines.withContext(Dispatchers.IO) {
RetrofitClient.recommendationApi.getPlaylistPolling(taskId).execute()
}
if (response.isSuccessful) {
val pollingData = response.body()?.data
if (pollingData?.status == "COMPLETED") {
isFinished = true
onPlaylistReady(pollingData.playlistInfo!!) // 결과 처리로 이동
}
}
if (!isFinished) delay(2000) // 2초 대기 후 다시 확인
}
}
}
3) 최종 결과 처리 및 로컬 DB 저장
데이터를 성공적으로 받으면 나중에 홈 화면에서 '최근 추천 기록'을 보여주기 위해 RecommendationManager(싱글톤)와 Room Database에 저장한다.
private fun onPlaylistReady(resultData: RecommendationResponse) {
isDataLoaded = true
// 1. 메모리 및 공유 저장소 저장
RecommendationManager.cachedPlaylist = resultData
viewModel.currentPlaylist.value = resultData
// 2. Room Database에 최근 기록 저장 (비동기)
saveToRoomHistory(requireContext(), viewModel.place, viewModel.goal, resultData.playlistId)
// 3. UI 업데이트 (완료 상태로 변경)
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
updateUIForCompletion()
}
}
Ⅳ. 마무리
이번 기능을 구현하면서 비동기 작업을 처리하는 방식에 대해 고민할 수 있었다. 처음에는 단순히 API 요청 보내면 응답이 올 때까지 대기했지만, 플레이리스트를 만드는 데 시간이 걸리기 때문에 안정적인 구조가 아니었다.
그래서 Polling 방식을 사용했고 실제 안정적으로 잘 작동했다. 이번 경험을 통해 단순히 API를 호출하는 것이 아니라 작업 흐름을 이해하고 설계하는 경험을 할 수 있었다. 이런 비동기 처리 방식으로 WebSocketdlsk SSE 라는 것도 있던데 나중에 공부해 보면 좋을 것 같다.
또한 예외 처리에서도 네트워크 오류나 타임아웃 시 튕기는 것이 아니라 토스트와 함께 이전 화면으로 돌려보내는 식으로 처리했고, 이 과정에서 예외처리 UX에 대해 고민해볼 수 있었다.
👾👉 DIP Android Github: https://github.com/choisio2/DIP_android
GitHub - choisio2/DIP_android: Tave 16th team 리듬타고가는중 - 후반기 프로젝트 DIP (위치와 소음 데이터를
Tave 16th team 리듬타고가는중 - 후반기 프로젝트 DIP (위치와 소음 데이터를 분석해 맥락 기반 음악을 추천하는 안드로이드 앱) - choisio2/DIP_android
github.com
📽️👉 풀시연영상 보러가기: https://youtu.be/HjFp9eipC_4
'TAVE-16th' 카테고리의 다른 글
| [DIP/FE] 라이브러리 - Spotify에서 4분할 커버 가져오기 & 플리 삭제하기 (0) | 2026.03.08 |
|---|---|
| [DIP/FE] Playlist 추천 결과 - 리스트형 & 갤러리형 보기(ViewPager2) (0) | 2026.03.08 |
| [DIP/FE] 노래 추천받기(1) - 장소 선택, 데시벨 측정, 목표 설정 (0) | 2026.03.07 |
| [DIP/FE] 온보딩 구현 - 문자열 검사 및 Spotify API로 아티스트 목록 가져오기 (0) | 2026.03.07 |
| [DIP/FE] 프로젝트 소개 & Kakao Oauth 구현 (0) | 2026.02.23 |
