Search

이지은 멘토링 신청

분류
안드로이드
프로그램오류
담당멘토
양민욱
멘토링 요청시간
2023/09/01 20:00
멘토링 시간
2023/09/01 20:00-21:00
멘토링방
멘토링룸1
배정상태
해결완료
비용지급
시트 반영
번호
0
신청팀
개인
최종프로젝트2팀
소요시간
1
작성자
이지은

질문(예시)

1.
제가 수업 때 MVVM 패턴을 배운 이후 개인적으로 공부도 하고 예제에 적용도 시켜보면서 나름 잘 사용하고 있다고 생각했는데 쇼핑몰 프로젝트를 진행할때 작성한 코드들에서 파이어베이스 realtime db에 저장된 값을 받아오는 과정에서 addOnCompleteListener등의 비동기 처리 메서드 내에서 뷰모델의 초기화가 이루어지도록 코드를 작성해서 그런지, recyclerView에서 해당 뷰모델(mutableList형)을 사용하려고 하니까 자꾸 nullPointerException이나 IndexOutOfBoundsException이 나더라구요…. 다른 이유에서 오류가 발생했을 때도 있지만 대부분 비동기로 받은 값을 초기화되기 전에 사용해서 오류가 발생하는 것 같았습니다. 그래서 MVVM 아키텍처를 코드에 적용할 때 널 오류 없이 안전하게 뷰모델을 초기화 하는 방식이 있다면 알고 싶습니다.
2.
1번에서 작성한 비동기처리 메서드를 중첩해서 사용할 경우 안쪽에 종속된 메서드에서 동기처리가 안되어서 원하는 대로 값이 나오지 않는 현상이 있어서 runBlocking{} 이라는 코루틴 스코프를 사용했는데 정확히 무슨 역할을 하는건지 설명해주시면 감사하겠습니다
3.
저희가 쇼핑몰 프로젝트 때 디렉토리 구조를 Model / Repository / UI / ViewModel 이렇게 네 분류로 나누어 작업을 진행했습니다. repository 에서 파이어베이스 참조 객체에 쿼리를 통해 데이터를 받아오는 함수들을 작성해놓고 viewmodel 에서 함수를 호출해 값을 받아와 livedata에 저장하는 형태로 사용을 했었는데, 이 경우에 아키텍처 요소의 용도에 맞게 사용한 게 맞을까요?
*참고용 2팀 쇼핑몰 프로젝트 깃허브 레포지토리 주소

화면캡쳐(없을 경우 생략)

1번 문제점 예시 상황
ProductRepository.kt
fun getAllProductData(callback1: (Task<DataSnapshot>) -> Unit){ val database = FirebaseDatabase.getInstance() val productRef = database.getReference("ProductData") productRef.orderByChild("productId").get().addOnCompleteListener(callback1) }
Kotlin
복사
ProductViewModel.kt
val productList = MutableLiveData<MutableList<ProductModel>>() init { productList.value = mutableListOf<ProductModel>() }fun getAllProductData(callback1: (Task<DataSnapshot>) -> Unit){ val database = FirebaseDatabase.getInstance() val productRef = database.getReference("ProductData") productRef.orderByChild("productId").get().addOnCompleteListener(callback1) } // 전체 상품 가져오기 fun getAllProductData(){ val tempList = mutableListOf<ProductModel>() ProductRepository.getAllProductData { for(c1 in it.result.children){ val productId = c1.child("productId").value as Long val productSellerId = c1.child("productSellerId").value as String val productName = c1.child("productName").value as String val productPrice = c1.child("productPrice").value as Long val productImage = c1.child("productImage").value as String val productInfo = c1.child("productInfo").value as String val productCount = c1.child("productCount").value as Long val productSellingStatus = c1.child("productSellingStatus").value as Boolean val productDiscountRate = c1.child("productDiscountRate").value as Long val productRecommendationCount = c1.child("productRecommendationCount").value as Long val productBrand = c1.child("productBrand").value as String val productKeyword = c1.child("productKeywordList").value as HashMap<String, Boolean> val productCategory = c1.child("productCategory").value as String val product = ProductModel(productId, productSellerId, productName, productPrice, productImage, productInfo, productCount, productSellingStatus, productDiscountRate, productRecommendationCount, productBrand, productKeyword, productCategory) tempList.add(product) } productList.value = tempList } }
Kotlin
복사
ShoppingProductFragment.kt
lateinit var productViewModel: ProductViewModel override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { productViewModel = ViewModelProvider(mainActivity)[ProductViewModel::class.java] productViewModel.run { productList.observe(mainActivity) { fragmentShoppingBinding.recyclerViewShoppingProduct.adapter?.notifyDataSetChanged() productCheckedList.clear() for(i in 0 until productList.value?.size!!){ productCheckedList.add(false) } } } // 리사이클러뷰 recyclerViewShoppingProduct.run { productViewModel.getAllProductData() adapter = ShoppingProductAdapter() layoutManager = GridLayoutManager(context, 3) } ***이후 해당 상품 리스트를 RecyclerView에서 사용하기 위해 호출하면 NullPointerException 이나 IndexOutOfBoundsExceptioin이 자주 발생... }
Kotlin
복사
저희가 프로젝트에서 DB에 접근해 값을 받아와 뷰모델에 저장하고 뷰모델을 사용하는 과정이 전부 위랑 비슷한 방식으로 작성을 했습니다! 이 방식으로 코드를 짜면 뷰모델이 초기화 되지 않은 시점에서 recycler View 어댑터가 상품 리스트에 접근하려고 할때 오류가 발생하는 것으로 추측되어서 뷰모델이 완전히 초기화 되었을 때 값에 접근하려면 어떤 방식으로 코드를 수정해야할지 조언 부탁드립니다!
2번 문제점 예시 상황
CartViewModel.kt
var cartDataList = MutableLiveData<MutableList<CartModel>>() // 상품 정보 목록 var cartProductList = MutableLiveData<MutableList<CartProductModel>>() init { cartDataList.value = mutableListOf<CartModel>() cartProductList.value = mutableListOf<CartProductModel>() } // 장바구니 목록 화면 fun getCartData(cartUserId: String) { val tempList = mutableListOf<CartModel>() val tempList2 = mutableListOf<CartProductModel>() CartRepository.getAllCartData(cartUserId) { if(it.result.exists() == true) { for (c1 in it.result.children) { val cartUserId = c1.child("cartUserId").value as String val cartProductId = c1.child("cartProductId").value as Long val cartProductCount = c1.child("cartProductCount").value as Long val cartProduct = CartModel(cartUserId, cartProductId, cartProductCount) tempList.add(cartProduct) //runBlocking을 사용해 getProductData를 호출 runBlocking { getProductData(cartProductId, tempList2) } } cartDataList.value = tempList } } } fun getProductData(cartProductId: Long, tempList2: MutableList<CartProductModel>) { CartRepository.getProductData(cartProductId) { if(it.result.exists() == true) { for (c2 in it.result.children) { val productName = c2.child("productName").value as String val productPrice = c2.child("productPrice").value as Long val productImage = c2.child("productImage").value as String val productInfo = c2.child("productInfo").value as String val cartProductModel = CartProductModel(productName, productPrice, productImage, productInfo) tempList2.add(cartProductModel) } } cartProductList.value = tempList2 } }
Kotlin
복사
CartRepository.kt
/ 모든 장바구니 상품 가져오는 함수 fun getAllCartData(cartUserId: String, callback1: (Task<DataSnapshot>) -> Unit) { val database = FirebaseDatabase.getInstance() val cartRef = database.getReference("CartData") cartRef.orderByChild("cartUserId").equalTo(cartUserId).get() .addOnCompleteListener(callback1) } // 상품 정보 가져오는 함수 fun getProductData(cartProductId: Long, callback1: (Task<DataSnapshot>) -> Unit) { val database = FirebaseDatabase.getInstance() val productRef = database.getReference("ProductData") productRef.orderByChild("productId").equalTo(cartProductId.toDouble()).get() .addOnCompleteListener(callback1) }
Kotlin
복사

멘토 답변

제가 수업 때 MVVM 패턴을 배운 이후 개인적으로 공부도 하고 예제에 적용도 시켜보면서 나름 잘 사용하고 있다고 생각했는데 쇼핑몰 프로젝트를 진행할때 작성한 코드들에서 파이어베이스 realtime db에 저장된 값을 받아오는 과정에서 addOnCompleteListener등의 비동기 처리 메서드 내에서 뷰모델의 초기화가 이루어지도록 코드를 작성해서 그런지, recyclerView에서 해당 뷰모델(mutableList형)을 사용하려고 하니까 자꾸 nullPointerException이나 IndexOutOfBoundsException이 나더라구요…. 다른 이유에서 오류가 발생했을 때도 있지만 대부분 비동기로 받은 값을 초기화되기 전에 사용해서 오류가 발생하는 것 같았습니다. 그래서 MVVM 아키텍처를 코드에 적용할 때 널 오류 없이 안전하게 뷰모델을 초기화 하는 방식이 있다면 알고 싶습니다.
nullPointerException과 IndexOutOfBoundsException의 발생 원인은 뷰모델를 직접 참조하는 UI 코드에 있음을 설명드렸습니다.
// 상품을 보여주는 리사이클러뷰 inner class ShoppingProductAdapter : RecyclerView.Adapter<ShoppingProductAdapter.ShoppingProductViewHolder>(){ ... override fun getItemCount(): Int { return productViewModel.productList.value?.size!! }
Kotlin
복사
위 코드를 보면 Adapter가 직접 뷰모델의 LiveData의 값을 참조해서 접근하고 있었고, LiveData의 초기 value 값은 null이므로 !! 문에서 npe가 발생함을 설명드렸습니다. - 해결 방법 (1) !! 제거
// 상품을 보여주는 리사이클러뷰 inner class ShoppingProductAdapter : RecyclerView.Adapter<ShoppingProductAdapter.ShoppingProductViewHolder>(){ ... override fun getItemCount(): Int { return productViewModel.productList.value?.size ?: 0 }
Kotlin
복사
멘토링 내에서 다루지 못했지만 가장 쉬운 해결 방법은 !! 사용으로 인해 발생한 npe 이기 때문에 셀비스 연사자로 Null Check를 해주면 해결될 코드로 파악됩니다! 하지만 productViewModel.productList 는 뷰모델에서 상시 변경될 수 있는 값이기 때문에 UI를 그리는 도중 productList의 사이즈가 변경됨에 따라 IndexOutOfBoundsException 이 발생할 위험이 있습니다.
Adapter, 즉 UI가 ViewModel의 값을 직접 참조하는 것은 MVVM 관계에 어긋하는 구조로 좀더 유연성있는 구조를 가져가면 좋습니다 cc.(2) (2) ViewModel 의존성 유연하게 가져가기
// 상품을 보여주는 리사이클러뷰 inner class ShoppingProductAdapter : RecyclerView.Adapter<ShoppingProductAdapter.ShoppingProductViewHolder>(){ ... private var itemList: List<ProductModel> = emptyList() // Adapter 내부에서 ItemList를 관리 fun updateItemList(newList: List<ProductModel>) { this.itemList = newList notifyDataSetChanged() // 갱신 } override fun getItemCount(): Int { return itemList.size }
Kotlin
복사
Adapter 자체적으로 내부에서 리스트를 관리하는 방법입니다. emptyList() 로 값을 초기화를 해두고 이후 ViewModel 데이터가 변경되면 updateItemList 함수를 통해 내부 리스트를 변경해줍니다.
productViewModel.run { productList.observe(mainActivity) { itemList -> if (itemList != null) { // LiveData observe 람다 값은 플랫폼 타입으로 null이 올 수 있다! (fragmentShoppingBinding.recyclerViewShoppingProduct.adapter as? ShoppingProductAdapter) .updateItemList(it) productCheckedList.clear() for(i in 0 until itemList.size) { // !! 제거 productCheckedList.add(false) } } }
Kotlin
복사
플랫폼 타입을 설명드리며 null check 하면 좋음을 설명드렸습니다.
1번에서 작성한 비동기처리 메서드를 중첩해서 사용할 경우 안쪽에 종속된 메서드에서 동기처리가 안되어서 원하는 대로 값이 나오지 않는 현상이 있어서 runBlocking{} 이라는 코루틴 스코프를 사용했는데 정확히 무슨 역할을 하는건지 설명해주시면 감사하겠습니다
runBlocking은 코루틴 스코프의 종류로 람다 내부에서 실행되는 코루틴이 모두 종료할 때까지 호출한 쓰레드를 Blocking 함을 설명드렸습니다. Blocking이라는 뜻은 실행 흐름을 막는 것으로 Main Thread에서 runBlocking을 사용할 경우 UI Thread를 정지시켜 사용자의 액션을 일정 시간동안 받지 못해 ANR이 발생할 수 있습니다. 그런데 실제 멘티님 프로젝트 코드에서는 runBlocking 내부에서 코루틴을 사용하고 있지 않는 점을 발견했고, await 함수를 알려드렸습니다.
val result = imageRef.downloadUrl.get().await() // suspend function인 await()를 호출 // 이 라인부터는 값이 무조건 있음을 보장
Kotlin
복사
await 함수는 해당 값이 올 때까지 코루틴이 suspend 되기 때문에 동기를 보장해줍니다. 대신 Exception이 그대로 Throws 되기 때문에 try-catch 로 에러 핸들링 필요성을 설명드렸습니다. 그리고 Callback 방식이 아닌 동기식으로 작성할 수 있음을 추가로 설명드렸습니다.
저희가 쇼핑몰 프로젝트 때 디렉토리 구조를 Model / Repository / UI / ViewModel 이렇게 네 분류로 나누어 작업을 진행했습니다. repository 에서 파이어베이스 참조 객체에 쿼리를 통해 데이터를 받아오는 함수들을 작성해놓고 viewmodel 에서 함수를 호출해 값을 받아와 livedata에 저장하는 형태로 사용을 했었는데, 이 경우에 아키텍처 요소의 용도에 맞게 사용한 게 맞을까요?
전체적으로 Model, View, ViewModel 역할에 맞게 잘 구성하고 있음을 말씀드리며 작은 개선 사항들을 몇가지 알려드렸습니다. 1. ViewModel Mutable Field를 내부에 감추기
private val _data = MutableLiveData(..) val data: LiveData(..) = _data
Kotlin
복사
2.
Fragment에서 observe를 할 때는 viewLifecycleOwner 를 사용
현재 대부분 Fragment의 뷰를 재사용하고 있지 않아서 viewLifecycleOwner 대신 Fragment LifecycleOnwer를 사용해도 좋다.
Activity LifecycleOnwer → Fragment LifecycleOnwer
class MyFragment: Fragment...{ fun observeViewModel() { viewModel.data.observe(this) { // Fragment LifecycleOwner 사용 } } }
Kotlin
복사
3.
ViewModel 초기화할 때 activity-ktx 라이브러리 사용해도 좋다.
class MyFragment: Fragment...{ private val myViewModel: MyViewModel by viewModels() // 뷰모델 지연 초기화 기능 제공 }
Kotlin
복사
하지만 현재 ViewModelProvider를 사용하면서 내부 동작 원리를 이해하는 점은 매우 좋습니다!