마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

sealed 클래스 사용에서 부모(sealed)클래스에도 프로퍼티가 필요할까요..

0 추천

리사이클러뷰 대신에 Epoxy를 사용해서 버튼 클릭시 동적으로 아이템을 추가 및 삭제하는 로직을 작성하고 있는데요,

아이템을 추가 및 삭제하기위해서 position으로 구분하기 위한것이 아닌 id 값을 주어 위치를 구분하여

추가 삭제하려고 합니다.

두개의 타입의 아이템(뷰)들이 존재하는데, sealed 클래스로 관리합니다.

그런데 이전에 작성해주신 참조 코드를 봤을때 ( android - Epoxy 라이브러리 사용해보신분 계신가요? - 안드로이드 Q&A (masterqna.com) )

selead class ListItem {
    val id: String,
    val title: String
 
   class Parent(
         id: String,
         title: String
    ): ListItem(id, title)
 
   class Child(
         id: String,
         title: String
    ): ListItem(id, title)
}
 

이러한 형태로 작성하여 참고하라 하셨습니다.

그래서

sealed class RoutineItem(
    val id: String // 필요한가?
){
    data class RoutineModel(
        val id2: String, // id
        val workout: String, // 운동 종목
        val unit: String, // 무게 단위 (kg or lbs)
        var routineDetail: List<DetailModel> = listOf()
    ) : RoutineItem(id2)

    data class DetailModel(
        val set: String, // 세트
        val reps: String = "1",
        val weight: String
    ) : RoutineItem(set)
}

대충 이러한 형태로 작성하였는데..

궁금한것이 외부 sealed 클래스에도 id를 위한 프로퍼티가 필요한가 입니다..

 

이렇게 작성후에 ㅅ디버깅을 해보니 sealed 클래스 내의 data 클래스가 sealed 클래스를 상속하다보니,

id 값도 그대로 상속되어 id 와 id2 두개가 동시에 RoutineModel에 존재하게 되는걸 확인했습니다.

왜 필요한가요?

 

 

-------------------------------------

RoutineItem

sealed class RoutineItem(
    val id: String
) {
    class RoutineModel(
        id: String, // id
        val workout: String, // 운동 종목
        val unit: String, // 무게 단위 (kg or lbs)
        var routineDetail: List<DetailModel> = listOf()
    ) : RoutineItem(id)

    class DetailModel(
        val set: String, // 세트
        val reps: String = "1",
        val weight: String
    ) : RoutineItem(set)
}

 

RoutineModel

data class RoutineModel2(
    val id: String, // 고유 id (UUID 사용)
    val workout: String, // 운동 종목
    val unit: String, // 무게 단위 (kg or lbs)
    var routineDetail: ArrayList<RoutineDetailModel> = arrayListOf()
) {
    init {
        // 루틴 생성시 최소 1개의 상세 아이템을 가지고 있게 하기 위함
        val detail = RoutineDetailModel("1","test","33")
        routineDetail.toMutableList().add(detail)
    }

    fun getSubItemList() : ArrayList<RoutineDetailModel> = routineDetail

    fun getSubItemSize() = routineDetail.size

    fun addSubItem(item: RoutineDetailModel) {
        routineDetail.toMutableList().add(item)
    }

    fun deleteSubItem() {
        routineDetail.toMutableList().removeLast()
    }
}

 

RoutineDetailModel

data class RoutineDetailModel(
    val set: String, // 세트
    val reps: String = "1",
    val weight: String) {
}

 

ViewModel

class WriteRoutineViewModel : ViewModel() {
    private var _items: MutableLiveData<List<RoutineItem>> = MutableLiveData(listOf())
    private val routines = mutableListOf<RoutineItem>()
    private val rmList = arrayListOf<RoutineModel>()

    val items: LiveData<List<RoutineItem>> = _items

    fun addRoutine(workout: String) {

        routines.add(RoutineItem.RoutineModel(UUID.randomUUID().toString(), workout, "TEST"))
        routines.add(RoutineItem.DetailModel("1","3","3"))

        _items.postValue(routines)
    }

    fun getListForRecyclerView() {
        //TODO: 리사이클러뷰에 뿌릴 리스트(rmList)를 가공하기
    }
}

 

codeslave (3,940 포인트) 님이 2021년 10월 24일 질문
codeslave님이 2021년 10월 29일 수정

1개의 답변

0 추천

아니오, 중복된 property는 없는 게 맞아요. 아마도 예제코드를 수정하는게 좋을 것 같은데

// Option1
sealed class RoutineItem(
    val id: String // 필요한가?
){
   class RoutineModel(
        id: String, // --> val 키워드 없
       ...
    ) : RoutineItem(id)
 
   class DetailModel(
        set: String, 
        ...
    ) : RoutineItem(set)
}

// Options2
sealed class RoutineItem {
    abstract val id: String
    data class RoutineModel(
       override val id: String,
       ...
    ) : RoutineItem()
 
    data class DetailModel(
       override  val id: String,
        ...
    ) : RoutineItem()
}

Option1 처럼,  sealed class 프로퍼티를 정의하고, 상속받는 클래스에는 val이 없이 사용하거나,

Option2와 같이, sealed class의 프로퍼티를 abstract으로 정의하고 상속받는 클래스에  val을 사용할 수도 있습니다.

첫번째 옵션은 val 프로퍼티를 사용하지 않기 때문에, data class를 사용할 수 없고, 두번째 옵션에서는 사용할 수 있습니다.
대신, 첫번째 옵션은

val item: RoutineItem = ....
item.id

처럼, sealed class타입으로 해서 id를 바로 접근할 수 있지만, 두번째 옵션에서는 이거는 안됩니다. 대신 두번째 옵션은 data class의 기능인 copy나 destruction등을 사용할 수가 있죠. 상황에 맞게 적절하게 선택하시면 될 것 같아요.

spark (226,380 포인트) 님이 2021년 10월 24일 답변
아 옵션1같은 경우는 상속을 해서 RoutineModel과 DetailModel 둘다 id를 가지계 되는거군요..옵션2도 abstract를 구현?해야 하므로 id를 받기떄문에 비슷?한거 같긴하지만요..

그런데 고민이..제 경우에 DetailModel에 id가 필요한지 의문입니다. 사실상 DetailModel을 추가 및 삭제할때는 RoutineModel의 끝에다가 추가 삭제가 되기때문에 굳이 id로 구분할 필요가 없거든요. RoutineModel의 경우야 버튼을 클릭했을때 위치를 찾아야하기때문에 필요하긴 하지만요.

그래서 포인트가 DetailModel에 id가 필요없는데 RoutineItem에 프로퍼티로 id를 선언해서 굳이 DetailModel에까지 상속 혹은 override 하도록 해야하는가 입니다..

이런 경우는 굳이 RoutineItem에 id를 넣지않고 RoutineModel에만 id를 넣어 주는게 좋을까요?

아니면 뭐 나중에 혹시나 있을 파이어베이스같은거.. 사용할때(아직 사용방법은 모르지만..) 필요한 경우라도 있을까요?


+그리고 추가로 궁금한것이요, 본문 링크에 이전에 선생님이 달아주셨던 코드에서요. sealed 클래스를 정의하고 그 안에 Parent와 Child를 정의해 주셨잖아요,

그런데 그 밑에 data 클래스(ParentVo, ChildVo)를 또 정의한 이유가 궁금합니다. 다시 정의하시고 리스트도 (arrayListOf<ParentVo>())를 사용하시던데 이유가 궁금합니다.

그냥 arrayListOf<ListItem.Parent> 이렇게 선언 후 for문을 사용하면 안되나요?



**번외로 id는 UUID를 사용합니다 UUID.randomUUID().toString()
ID사용은 필요하시면 사용하시고, 그렇지 않다면 사용하지 않으시면 되겠죠 데이터베이스를 사용할 계획이 있다면, 좀 더 고민을 해보세요. 보통 업무에서는 이런 데이터의 종류에서 ID는 기본적으로 사용하는 편입니다. 그리고  set 필드가 ID와 동일하다면
val id : String get() = set
와 같이 Readonly 프로퍼티를 사용하는 옵션도 있습니다.

그리고 List<RoutineItem>을 사용하는 이유는 리사이클러뷰에 있는 데이터를 nested 구조 없이 parent, child를 모두 포함한 멀티뷰타입을 지원하기 위해서 입니다.
arrayListOf<RoutineItem> (
    RoutineItem.RoutineModel(),
    RoutineItem.DetailModel(),
    RoutineItem.DetailModel(),
    RoutineItem.DetailModel(),
    RoutineItem.RoutineModel(),
    RoutineItem.DetailModel(),
    RoutineItem.DetailModel(),
)
set을 인자로 보낸것은 일단 실행이 잘되는지 또 디버깅을하려고 임시방편으로 그렇게했던것이라.. 말씀대로 RoutineModel에만 서용할지 둘다 id를 사용할지 고민해야겠네요

그리고 sealed 클래스인 ListItem를 사용해서 내부에 RoutineModel과 DetailModel을 사용하는것은 단순히 리사이클러뷰를 사용하기 위함이지, data class를 따로 정의하는것을 대체할수는 없는건가요? data class를 따로 정의해줘야만 하는지요
ViewModel에서 사용하는 데이터는 뷰를 위한 데이터입니다. 님이 말하는 데이터는 비지니스 로직에 사용하는 데이터 구조이구요. 그렇기 때문에 MVVM에서 말하는 M(Model)은 비지니스 로직에 해당하고 ViewModel은 Model에서 데이터를 가져와서 V(View)에서 사용하기 적합하게 가공해서 전달해주어야 합니다. View에는 데이터를 가공하는 로직이 들어가지 않습니다. 즉,
data class Routine(
   val id: String,
   val workout: Workout,
   val details: List<RoutineDetail>
)

data class Workout {

}

data class RoutineDetail {

}

val routines: List <Routine> = model.getRoutines()
val listItems = converRoutinesToListItem(routines)

과 같이 Model서 가져온 데이터를 ViewModel에서 화면에 보여주기 좋은 형태로 바꾸어주어야 합니다.
이게 ViewModel 의 주된 역할이기도 하구요.
감사합니다 그렇다면 제 코드를 예를들면 비지니스 로직에 해당하는 모델(data class)을 통해서 데이터를 가공한 후(가져오거나 데이터를 값을 변경하거나 하는 행위)에 Epoxy(리사이클러뷰)에 뿌려주기 위해 List<RoutineItem> 에 가공한 데이터를 넣어서 이 리스트를 뷰에 뿌리는 방법이라는 말씀인거죠?

sealed 클래스인 RoutineItem과 그 내의 두개의 클래스는 가공된 데이터를 뷰(화면)에 뿌려주기위한 리사이클러뷰를 위한 용도 그이상 그이하도 아니구요..?
네. 맞아요. 참고로 MVVM의 M은 data class와 같은 단순한 클래스가 아니라 비지니스 로직을 처리하는 그런 클래스를 말합니다. 용어가 헷갈릴 수 있습니다. 안드로이드에서는 보통  개인의 스타일이나 상황에 따라 UseCase, Interactor, Repository 중 하나를 많이 사용합니다.
님의 경우는, Routine을 가져오고, 추가, 수정, 삭제하는 실제적인 동작은 이 비지니스 레이어에서 처리하게 되는 겁니다. 주된 이유는 Single Resposibility와 Separation of Concern이라는 클린코드의 원칙에 따른 것으로, 클래스나 함수는 한가지 역할에만 집중하는 것이 좋고, 관심사 또는 역할이 명확히 다르다면 분리하는 것이 권장됩니다. 기본적으로 이런 모든 원칙은 나중에 코드를 변경할 때 가장 적은 임팩트를 가질 수 있도록 하기 위한 겁니다. 모델과 ViewModel을 분리하시면 여러가지 이유로 ViewModel 을 자주 손댈 필요가 없을 겁니다.
아아 감사합니다. 선생님 그러면 제가 지금 제 현재 코드를 추가했는데요,
ViewModel 클래스의 RoutineModel을 추가하는 addRoutine()에서 현재 루틴을 추가할때 sealed class의 RoutineModel을 사용하는데 사실 이것보다 data class의 RoutineModel을 리스트에 추가한후에 이것을 (sealed 클래스형태로) 가공하여 리스트에 저장하여 보내주는것이 더 바람직한건가요?
그러니까 ArrayList<RoutineModel> 과 MutableLiveData<List<RoutineItem>> 이 두개가 같이 있고 ArrayList에 저장후에 다시 화면에 보여주기 위한 LiveData에 이걸 가공해서 넣어야하는지..

코드를 보시면 아시겠지만 현재 저는 data class의 RoutineModel을 사용하지 않고 바로 sealed 클래스의 RoutineItem.RoutineModel을 사용하여 바로 리스트에 저장하는 형태로 하고 있거든요.
프로젝트가 복잡하지 않고 해당 뷰모델이 단순하다면 뷰모델에서 처리해도 크게 문제는 없을 겁니다. 분리를 하게 되면 좀 더 코드를 작성해야 하고 코드는 좀 더 구조화되겠구요. 이런 아키텍쳐와 관련된 결정은 프로젝트의 크기, 복잡도, 개발자의 수준, 예산과 시간 등등 여러가지를 고려해서 결정해야 합니다. 모든 경우에 들어맞는 정답은 없습니다. 모든 선택에는 얻는게 있고 잃는게 존재합니다. 비교해서 득이 많은 쪽을 선택하세요.
공부가 목적이라면 역할을 분리하시는 걸 추천드리구요.
...